Merge "Update AppSwitcher visuals" into udc-dev

This commit is contained in:
Anton Potapov
2023-03-24 17:25:32 +00:00
committed by Android (Google) Code Review
14 changed files with 332 additions and 72 deletions

View File

@@ -23,7 +23,7 @@
<item
android:drawable="@drawable/ic_ksh_key_down"
android:gravity="end|bottom"
android:paddingBottom="6dp"
android:bottom="4dp"
android:width="24dp"
android:height="24dp"
android:end="12dp" />

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/transparent" />
<corners android:radius="@dimen/control_popup_corner_radius" />
</shape>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="@dimen/control_popup_item_corner_radius" />
<solid android:color="#303030" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="@dimen/control_popup_item_corner_radius" />
<solid android:color="#1f1f1f" />
</shape>
</item>
</selector>

View File

@@ -13,33 +13,28 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="@dimen/control_spinner_padding_vertical"
android:paddingHorizontal="@dimen/control_spinner_padding_horizontal">
android:layout_height="@dimen/control_popup_item_height"
android:background="@drawable/controls_popup_item_background"
android:gravity="center_vertical|start"
android:orientation="horizontal"
android:paddingStart="@dimen/control_popup_item_padding"
android:paddingEnd="@dimen/control_popup_item_padding">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
<ImageView
android:id="@+id/app_icon"
android:layout_width="@dimen/controls_header_app_icon_size"
android:layout_height="@dimen/controls_header_app_icon_size"
android:layout_marginEnd="@dimen/control_popup_item_padding"
android:contentDescription="@null"
tools:src="@drawable/ic_android" />
<TextView
android:id="@+id/controls_spinner_item"
style="@style/Control.Spinner.Item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center">
<ImageView
android:id="@+id/app_icon"
android:layout_gravity="center"
android:layout_width="@dimen/controls_header_app_icon_size"
android:layout_height="@dimen/controls_header_app_icon_size"
android:contentDescription="@null"
android:layout_marginEnd="10dp" />
<TextView
style="@style/Control.Spinner.Item"
android:id="@+id/controls_spinner_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</LinearLayout>
tools:text="Android" />
</LinearLayout>

View File

@@ -50,11 +50,9 @@
<LinearLayout
android:id="@+id/controls_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:minHeight="48dp"
android:orientation="horizontal">
<TextView
@@ -64,7 +62,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:clickable="false"
tools:text="Test app" />
tools:text="@tools:sample/lorem" />
</LinearLayout>
<ImageView

View File

@@ -206,6 +206,7 @@
<color name="control_thumbnail_tint">#33000000</color>
<color name="control_thumbnail_shadow_color">@*android:color/black</color>
<color name="controls_task_view_bg">#CC191C1D</color>
<color name="control_popup_dim">#8A000000</color>
<!-- Keyboard backlight indicator-->
<color name="backlight_indicator_step_filled">#F6E388</color>

View File

@@ -1200,6 +1200,13 @@
<dimen name="control_menu_item_min_height">56dp</dimen>
<dimen name="control_menu_vertical_padding">12dp</dimen>
<dimen name="control_menu_horizontal_padding">16dp</dimen>
<dimen name="control_popup_item_corner_radius">4dp</dimen>
<dimen name="control_popup_item_height">56dp</dimen>
<dimen name="control_popup_item_padding">16dp</dimen>
<dimen name="control_popup_items_divider_height">1dp</dimen>
<dimen name="control_popup_max_width">380dp</dimen>
<dimen name="control_popup_corner_radius">28dp</dimen>
<dimen name="control_popup_horizontal_margin">16dp</dimen>
<dimen name="control_spinner_padding_vertical">24dp</dimen>
<dimen name="control_spinner_padding_horizontal">20dp</dimen>
<dimen name="control_text_size">14sp</dimen>

View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.controls.ui
import android.content.Context
import android.content.res.Resources
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.view.Gravity
import android.view.View
import android.widget.ListPopupWindow
import android.widget.PopupWindow
import com.android.systemui.R
class ControlsPopupMenu(context: Context) : ListPopupWindow(context) {
private val resources: Resources = context.resources
private val listDividerHeight: Int =
resources.getDimensionPixelSize(R.dimen.control_popup_items_divider_height)
private val horizontalMargin: Int =
resources.getDimensionPixelSize(R.dimen.control_popup_horizontal_margin)
private val maxWidth: Int = resources.getDimensionPixelSize(R.dimen.control_popup_max_width)
private val dialogBackground: Drawable = resources.getDrawable(R.drawable.controls_popup_bg)!!
private val dimDrawable: Drawable = ColorDrawable(resources.getColor(R.color.control_popup_dim))
private var dismissListener: PopupWindow.OnDismissListener? = null
init {
setBackgroundDrawable(dialogBackground)
inputMethodMode = INPUT_METHOD_NOT_NEEDED
isModal = true
setDropDownGravity(Gravity.START)
// dismiss method isn't called when popup is hidden by outside touch. So we need to
// override a listener to remove a dimming foreground
super.setOnDismissListener {
anchorView?.rootView?.foreground = null
dismissListener?.onDismiss()
}
}
override fun show() {
// need to call show() first in order to construct the listView
super.show()
val paddedWidth = resources.displayMetrics.widthPixels - 2 * horizontalMargin
width = maxWidth.coerceAtMost(paddedWidth)
anchorView?.let {
horizontalOffset = -width / 2 + it.width / 2
verticalOffset = -it.height / 2
if (it.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
horizontalOffset = -horizontalOffset
}
it.rootView.foreground = dimDrawable
}
with(listView!!) {
clipToOutline = true
background = dialogBackground
dividerHeight = listDividerHeight
}
// actual show takes into account updated ListView specs
super.show()
}
override fun setOnDismissListener(listener: PopupWindow.OnDismissListener?) {
dismissListener = listener
}
}

View File

@@ -604,7 +604,7 @@ class ControlsUiControllerImpl @Inject constructor (
setCompoundDrawablesRelative(selected.icon, null, null, null)
}
val anchor = parent.requireViewById<ViewGroup>(R.id.controls_header)
val anchor = parent.requireViewById<View>(R.id.app_or_structure_spinner)
if (items.size == 1) {
spinner.setBackground(null)
anchor.setOnClickListener(null)
@@ -617,10 +617,7 @@ class ControlsUiControllerImpl @Inject constructor (
anchor.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) {
popup = GlobalActionsPopupMenu(
popupThemedContext,
true /* isDropDownMode */
).apply {
popup = ControlsPopupMenu(popupThemedContext).apply {
setAnchorView(anchor)
setAdapter(adapter)
@@ -868,22 +865,24 @@ internal data class SelectionItem(
}
}
private class ItemAdapter(
val parentContext: Context,
val resource: Int
) : ArrayAdapter<SelectionItem>(parentContext, resource) {
private class ItemAdapter(parentContext: Context, val resource: Int) :
ArrayAdapter<SelectionItem>(parentContext, resource) {
val layoutInflater = LayoutInflater.from(context)
private val layoutInflater = LayoutInflater.from(context)!!
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val item = getItem(position)
val item: SelectionItem = getItem(position)!!
val view = convertView ?: layoutInflater.inflate(resource, parent, false)
view.requireViewById<TextView>(R.id.controls_spinner_item).apply {
setText(item.getTitle())
}
view.requireViewById<ImageView>(R.id.app_icon).apply {
setImageDrawable(item.icon)
with(view.tag as? ViewHolder ?: ViewHolder(view).also { view.tag = it }) {
titleView.text = item.getTitle()
iconView.setImageDrawable(item.icon)
}
return view
}
private class ViewHolder(itemView: View) {
val titleView: TextView = itemView.requireViewById(R.id.controls_spinner_item)
val iconView: ImageView = itemView.requireViewById(R.id.app_icon)
}
}

View File

@@ -138,6 +138,14 @@
android:finishOnCloseSystemDialogs="true"
android:excludeFromRecents="true" />
<activity android:name="com.android.systemui.activity.EmptyTestActivity"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<provider
android:name="androidx.startup.InitializationProvider"
tools:replace="android:authorities"

View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.controls.ui
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ShapeDrawable
import android.testing.AndroidTestingRunner
import android.util.DisplayMetrics
import android.view.View
import android.widget.PopupWindow.OnDismissListener
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.activity.EmptyTestActivity
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
@SmallTest
@RunWith(AndroidTestingRunner::class)
open class ControlsPopupMenuTest : SysuiTestCase() {
private companion object {
const val DISPLAY_WIDTH_NARROW = 100
const val DISPLAY_WIDTH_WIDE = 1000
const val MAX_WIDTH = 380
const val HORIZONTAL_MARGIN = 16
}
@Rule @JvmField val activityScenarioRule = ActivityScenarioRule(EmptyTestActivity::class.java)
private val testDisplayMetrics: DisplayMetrics = DisplayMetrics()
@Test
fun testDismissListenerWorks() = testPopup { popupMenu ->
val listener = mock(OnDismissListener::class.java)
popupMenu.setOnDismissListener(listener)
popupMenu.show()
popupMenu.dismissImmediate()
verify(listener).onDismiss()
}
@Test
fun testPopupDoesntExceedMaxWidth() = testPopup { popupMenu ->
testDisplayMetrics.widthPixels = DISPLAY_WIDTH_WIDE
popupMenu.show()
assertThat(popupMenu.width).isEqualTo(MAX_WIDTH)
}
@Test
fun testPopupMarginsWidthLessMax() = testPopup { popupMenu ->
testDisplayMetrics.widthPixels = DISPLAY_WIDTH_NARROW
popupMenu.show()
assertThat(popupMenu.width).isEqualTo(DISPLAY_WIDTH_NARROW - 2 * HORIZONTAL_MARGIN)
}
private fun testPopup(test: (popup: ControlsPopupMenu) -> Unit) {
activityScenarioRule.scenario.onActivity { activity ->
val testActivity = setupActivity(activity)
test(ControlsPopupMenu(testActivity).apply { anchorView = View(testActivity) })
}
}
private fun setupActivity(real: Activity): Activity {
val resources =
spy(real.resources).apply {
whenever(getDimensionPixelSize(R.dimen.control_popup_items_divider_height))
.thenReturn(1)
whenever(getDimensionPixelSize(R.dimen.control_popup_horizontal_margin))
.thenReturn(HORIZONTAL_MARGIN)
whenever(getDimensionPixelSize(R.dimen.control_popup_max_width))
.thenReturn(MAX_WIDTH)
whenever(getDrawable(R.drawable.controls_popup_bg)).thenReturn(ShapeDrawable())
whenever(getColor(R.color.control_popup_dim)).thenReturn(Color.WHITE)
whenever(displayMetrics).thenAnswer { testDisplayMetrics }
}
return spy(real).also { whenever(it.resources).thenReturn(resources) }
}
}

View File

@@ -234,7 +234,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
val serviceInfo2 = setUpPanel(panel2)
`when`(authorizedPanelsRepository.getAuthorizedPanels())
.thenReturn(setOf(packageName1, packageName2))
.thenReturn(setOf(packageName1, packageName2))
underTest.show(parent, {}, context)
@@ -245,7 +245,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() {
captor.value.onServicesUpdated(listOf(serviceInfo1, serviceInfo2))
FakeExecutor.exhaustExecutors(uiExecutor, bgExecutor)
val header: View = parent.requireViewById(R.id.controls_header)
val header: View = parent.requireViewById(R.id.app_or_structure_spinner)
assertThat(header.isClickable).isTrue()
assertThat(header.hasOnClickListeners()).isTrue()
}

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2022 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.systemui.tests.utils">
</manifest>

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.activity
import android.app.Activity
/**
* This activity does nothing. You can use it with [ActivityScenario] or [ActivityScenarioRule] to
* run activity-independent tests
*/
class EmptyTestActivity : Activity()