From 1ef16a76608fa96e15228118311795cc63a6b6db Mon Sep 17 00:00:00 2001 From: Anton Potapov Date: Thu, 23 Feb 2023 16:21:28 +0000 Subject: [PATCH] Update AppSwitcher visuals Switched from using GlobalActionsPopupMenu in Home Controls to a custom one due to the amount of customization required Test: manual on tablet, foldable, and phone. RTL and LTR layouts Fixes: 266407603 Change-Id: I58808fc663855a7ae0f37a2edc8587c939d03ca1 --- .../drawable/control_spinner_background.xml | 2 +- .../res/drawable/controls_popup_bg.xml | 22 ++++ .../controls_popup_item_background.xml | 31 +++++ .../res/layout/controls_spinner_item.xml | 47 ++++---- .../res/layout/controls_with_favorites.xml | 6 +- packages/SystemUI/res/values/colors.xml | 1 + packages/SystemUI/res/values/dimens.xml | 7 ++ .../systemui/controls/ui/ControlsPopupMenu.kt | 88 ++++++++++++++ .../controls/ui/ControlsUiControllerImpl.kt | 31 +++-- packages/SystemUI/tests/AndroidManifest.xml | 8 ++ .../controls/ui/ControlsPopupMenuTest.kt | 109 ++++++++++++++++++ .../ui/ControlsUiControllerImplTest.kt | 4 +- .../SystemUI/tests/utils/AndroidManifest.xml | 23 ---- .../systemui/activity/EmptyTestActivity.kt | 25 ++++ 14 files changed, 332 insertions(+), 72 deletions(-) create mode 100644 packages/SystemUI/res/drawable/controls_popup_bg.xml create mode 100644 packages/SystemUI/res/drawable/controls_popup_item_background.xml create mode 100644 packages/SystemUI/src/com/android/systemui/controls/ui/ControlsPopupMenu.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsPopupMenuTest.kt delete mode 100644 packages/SystemUI/tests/utils/AndroidManifest.xml create mode 100644 packages/SystemUI/tests/utils/src/com/android/systemui/activity/EmptyTestActivity.kt diff --git a/packages/SystemUI/res/drawable/control_spinner_background.xml b/packages/SystemUI/res/drawable/control_spinner_background.xml index 46a9dad29fecf..8416f9db93926 100644 --- a/packages/SystemUI/res/drawable/control_spinner_background.xml +++ b/packages/SystemUI/res/drawable/control_spinner_background.xml @@ -23,7 +23,7 @@ diff --git a/packages/SystemUI/res/drawable/controls_popup_bg.xml b/packages/SystemUI/res/drawable/controls_popup_bg.xml new file mode 100644 index 0000000000000..34dd6e5f70d92 --- /dev/null +++ b/packages/SystemUI/res/drawable/controls_popup_bg.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/packages/SystemUI/res/drawable/controls_popup_item_background.xml b/packages/SystemUI/res/drawable/controls_popup_item_background.xml new file mode 100644 index 0000000000000..799218007b395 --- /dev/null +++ b/packages/SystemUI/res/drawable/controls_popup_item_background.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/controls_spinner_item.xml b/packages/SystemUI/res/layout/controls_spinner_item.xml index 574aed62ea867..4048d0371aa80 100644 --- a/packages/SystemUI/res/layout/controls_spinner_item.xml +++ b/packages/SystemUI/res/layout/controls_spinner_item.xml @@ -13,33 +13,28 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - + 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"> - + + - - - - - - + tools:text="Android" /> diff --git a/packages/SystemUI/res/layout/controls_with_favorites.xml b/packages/SystemUI/res/layout/controls_with_favorites.xml index 71561c07ebd3a..b1259e48f7f7f 100644 --- a/packages/SystemUI/res/layout/controls_with_favorites.xml +++ b/packages/SystemUI/res/layout/controls_with_favorites.xml @@ -50,11 +50,9 @@ + tools:text="@tools:sample/lorem" /> #33000000 @*android:color/black #CC191C1D + #8A000000 #F6E388 diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index a52a2b7863bcb..88bed2e2d080d 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1200,6 +1200,13 @@ 56dp 12dp 16dp + 4dp + 56dp + 16dp + 1dp + 380dp + 28dp + 16dp 24dp 20dp 14sp diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsPopupMenu.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsPopupMenu.kt new file mode 100644 index 0000000000000..d08bc48e945d0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsPopupMenu.kt @@ -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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index 09ba3738be34e..c20af074c71ed 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -604,7 +604,7 @@ class ControlsUiControllerImpl @Inject constructor ( setCompoundDrawablesRelative(selected.icon, null, null, null) } - val anchor = parent.requireViewById(R.id.controls_header) + val anchor = parent.requireViewById(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(parentContext, resource) { +private class ItemAdapter(parentContext: Context, val resource: Int) : + ArrayAdapter(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(R.id.controls_spinner_item).apply { - setText(item.getTitle()) - } - view.requireViewById(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) + } } diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml index 29680d8345362..302a53068fb04 100644 --- a/packages/SystemUI/tests/AndroidManifest.xml +++ b/packages/SystemUI/tests/AndroidManifest.xml @@ -138,6 +138,14 @@ android:finishOnCloseSystemDialogs="true" android:excludeFromRecents="true" /> + + + + + + + + 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) } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index 330a1e457807f..845848071f549 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -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() } diff --git a/packages/SystemUI/tests/utils/AndroidManifest.xml b/packages/SystemUI/tests/utils/AndroidManifest.xml deleted file mode 100644 index cbef5f6036ab0..0000000000000 --- a/packages/SystemUI/tests/utils/AndroidManifest.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/activity/EmptyTestActivity.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/EmptyTestActivity.kt new file mode 100644 index 0000000000000..22ac3d72ae486 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/EmptyTestActivity.kt @@ -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()