Merge "Updates home controls overflow menu visuals" into udc-dev

This commit is contained in:
Anton Potapov
2023-04-13 13:33:21 +00:00
committed by Android (Google) Code Review
8 changed files with 178 additions and 46 deletions

View File

@@ -13,13 +13,17 @@
~ See the License for the specific language governing permissions and ~ See the License for the specific language governing permissions and
~ limitations under the License. ~ limitations under the License.
--> -->
<TextView <TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/controls_more_item_text"
style="@style/Control.MenuItem" style="@style/Control.MenuItem"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="@dimen/control_menu_item_height"
android:layout_gravity="start" android:layout_gravity="center_vertical"
android:background="@drawable/controls_popup_item_background"
android:paddingStart="@dimen/control_menu_horizontal_padding" android:paddingStart="@dimen/control_menu_horizontal_padding"
android:paddingEnd="@dimen/control_menu_horizontal_padding" android:paddingEnd="@dimen/control_menu_horizontal_padding"
android:textDirection="locale"/> android:textDirection="locale"
android:textSize="@dimen/control_item_text_size"
tools:fontFamily="@null"
tools:text="@tools:sample/lorem/random" />

View File

@@ -16,18 +16,18 @@
<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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/control_popup_item_height" android:layout_height="@dimen/control_apps_popup_item_height"
android:background="@drawable/controls_popup_item_background" android:background="@drawable/controls_popup_item_background"
android:gravity="center_vertical|start" android:gravity="center_vertical|start"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="@dimen/control_popup_item_padding" android:paddingStart="@dimen/control_menu_horizontal_padding"
android:paddingEnd="@dimen/control_popup_item_padding"> android:paddingEnd="@dimen/control_menu_horizontal_padding">
<ImageView <ImageView
android:id="@+id/app_icon" android:id="@+id/app_icon"
android:layout_width="@dimen/controls_header_app_icon_size" android:layout_width="@dimen/controls_header_app_icon_size"
android:layout_height="@dimen/controls_header_app_icon_size" android:layout_height="@dimen/controls_header_app_icon_size"
android:layout_marginEnd="@dimen/control_popup_item_padding" android:layout_marginEnd="@dimen/control_menu_horizontal_padding"
android:contentDescription="@null" android:contentDescription="@null"
tools:src="@drawable/ic_android" /> tools:src="@drawable/ic_android" />

View File

@@ -1199,19 +1199,17 @@
<dimen name="controls_top_margin">48dp</dimen> <dimen name="controls_top_margin">48dp</dimen>
<dimen name="controls_content_margin_horizontal">0dp</dimen> <dimen name="controls_content_margin_horizontal">0dp</dimen>
<dimen name="control_header_text_size">24sp</dimen> <dimen name="control_header_text_size">24sp</dimen>
<dimen name="control_item_text_size">16sp</dimen> <dimen name="control_item_text_size">14sp</dimen>
<dimen name="control_menu_item_text_size">16sp</dimen> <dimen name="control_menu_item_text_size">16sp</dimen>
<dimen name="control_menu_item_min_height">56dp</dimen> <dimen name="control_menu_item_height">54dp</dimen>
<dimen name="control_menu_vertical_padding">12dp</dimen> <dimen name="control_menu_vertical_padding">12dp</dimen>
<dimen name="control_menu_horizontal_padding">16dp</dimen> <dimen name="control_menu_horizontal_padding">@dimen/notification_side_paddings</dimen>
<dimen name="control_popup_item_corner_radius">4dp</dimen> <dimen name="control_apps_popup_item_height">56dp</dimen>
<dimen name="control_popup_item_height">56dp</dimen> <dimen name="control_popup_item_corner_radius">@dimen/notification_corner_radius_small</dimen>
<dimen name="control_popup_item_padding">16dp</dimen> <dimen name="control_popup_items_divider_height">@dimen/controls_app_divider_height</dimen>
<dimen name="control_popup_items_divider_height">1dp</dimen>
<dimen name="control_popup_max_width">380dp</dimen> <dimen name="control_popup_max_width">380dp</dimen>
<dimen name="control_popup_corner_radius">28dp</dimen> <dimen name="control_popup_corner_radius">@dimen/notification_corner_radius</dimen>
<dimen name="control_popup_horizontal_margin">16dp</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_spinner_padding_horizontal">20dp</dimen>
<dimen name="control_text_size">14sp</dimen> <dimen name="control_text_size">14sp</dimen>
<dimen name="control_icon_size">24dp</dimen> <dimen name="control_icon_size">24dp</dimen>

View File

@@ -895,7 +895,7 @@
<item name="android:textColor">@color/control_primary_text</item> <item name="android:textColor">@color/control_primary_text</item>
<item name="android:singleLine">true</item> <item name="android:singleLine">true</item>
<item name="android:gravity">center_vertical</item> <item name="android:gravity">center_vertical</item>
<item name="android:minHeight">@dimen/control_menu_item_min_height</item> <item name="android:minHeight">@dimen/control_menu_item_height</item>
</style> </style>
<style name="Control.Spinner"> <style name="Control.Spinner">

View File

@@ -20,11 +20,18 @@ import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.Gravity import android.view.Gravity.END
import android.view.Gravity.GravityFlags
import android.view.Gravity.NO_GRAVITY
import android.view.Gravity.START
import android.view.View import android.view.View
import android.view.View.MeasureSpec
import android.view.ViewGroup
import android.widget.ListPopupWindow import android.widget.ListPopupWindow
import android.widget.ListView
import android.widget.PopupWindow import android.widget.PopupWindow
import com.android.systemui.R import com.android.systemui.R
import kotlin.math.max
class ControlsPopupMenu(context: Context) : ListPopupWindow(context) { class ControlsPopupMenu(context: Context) : ListPopupWindow(context) {
@@ -40,13 +47,13 @@ class ControlsPopupMenu(context: Context) : ListPopupWindow(context) {
private val dimDrawable: Drawable = ColorDrawable(resources.getColor(R.color.control_popup_dim)) private val dimDrawable: Drawable = ColorDrawable(resources.getColor(R.color.control_popup_dim))
private var dismissListener: PopupWindow.OnDismissListener? = null private var dismissListener: PopupWindow.OnDismissListener? = null
@GravityFlags private var dropDownGravity: Int = NO_GRAVITY
init { init {
setBackgroundDrawable(dialogBackground) setBackgroundDrawable(dialogBackground)
inputMethodMode = INPUT_METHOD_NOT_NEEDED inputMethodMode = INPUT_METHOD_NOT_NEEDED
isModal = true isModal = true
setDropDownGravity(Gravity.START)
// dismiss method isn't called when popup is hidden by outside touch. So we need to // dismiss method isn't called when popup is hidden by outside touch. So we need to
// override a listener to remove a dimming foreground // override a listener to remove a dimming foreground
@@ -59,30 +66,68 @@ class ControlsPopupMenu(context: Context) : ListPopupWindow(context) {
override fun show() { override fun show() {
// need to call show() first in order to construct the listView // need to call show() first in order to construct the listView
super.show() super.show()
updateWidth()
val paddedWidth = resources.displayMetrics.widthPixels - 2 * horizontalMargin
width = maxWidth.coerceAtMost(paddedWidth)
anchorView?.let { anchorView?.let {
horizontalOffset = -width / 2 + it.width / 2 positionPopup(it)
verticalOffset = -it.height / 2
if (it.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
horizontalOffset = -horizontalOffset
}
it.rootView.foreground = dimDrawable it.rootView.foreground = dimDrawable
} }
with(listView!!) { with(listView!!) {
clipToOutline = true clipToOutline = true
background = dialogBackground background = dialogBackground
dividerHeight = listDividerHeight dividerHeight = listDividerHeight
} }
// actual show takes into account updated ListView specs // actual show takes into account updated ListView specs
super.show() super.show()
} }
override fun setDropDownGravity(@GravityFlags gravity: Int) {
super.setDropDownGravity(gravity)
dropDownGravity = gravity
}
override fun setOnDismissListener(listener: PopupWindow.OnDismissListener?) { override fun setOnDismissListener(listener: PopupWindow.OnDismissListener?) {
dismissListener = listener dismissListener = listener
} }
private fun updateWidth() {
val paddedWidth = resources.displayMetrics.widthPixels - 2 * horizontalMargin
val maxWidth = maxWidth.coerceAtMost(paddedWidth)
when (width) {
ViewGroup.LayoutParams.MATCH_PARENT -> {
width = maxWidth
}
ViewGroup.LayoutParams.WRAP_CONTENT -> {
width = listView!!.measureDesiredWidth(maxWidth).coerceAtMost(maxWidth)
}
}
}
private fun positionPopup(anchorView: View) {
when (dropDownGravity) {
NO_GRAVITY -> {
horizontalOffset = (-width + anchorView.width) / 2
if (anchorView.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
horizontalOffset = -horizontalOffset
}
}
END,
START -> {
horizontalOffset = 0
}
}
verticalOffset = -anchorView.height / 2
}
private fun ListView.measureDesiredWidth(maxWidth: Int): Int {
var maxItemWidth = 0
repeat(adapter.count) {
val view = adapter.getView(it, null, listView)
view.measure(
MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST),
MeasureSpec.UNSPECIFIED
)
maxItemWidth = max(maxItemWidth, view.measuredWidth)
}
return maxItemWidth
}
} }

View File

@@ -34,6 +34,7 @@ import android.service.controls.Control
import android.service.controls.ControlsProviderService import android.service.controls.ControlsProviderService
import android.util.Log import android.util.Log
import android.view.ContextThemeWrapper import android.view.ContextThemeWrapper
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -72,7 +73,6 @@ import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags import com.android.systemui.flags.Flags
import com.android.systemui.globalactions.GlobalActionsPopupMenu
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -540,12 +540,12 @@ class ControlsUiControllerImpl @Inject constructor (
val anchor = parent.requireViewById<ImageView>(R.id.controls_more) val anchor = parent.requireViewById<ImageView>(R.id.controls_more)
anchor.setOnClickListener(object : View.OnClickListener { anchor.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) { override fun onClick(v: View) {
popup = GlobalActionsPopupMenu( popup = ControlsPopupMenu(popupThemedContext).apply {
popupThemedContext, width = ViewGroup.LayoutParams.WRAP_CONTENT
false /* isDropDownMode */ anchorView = anchor
).apply { setDropDownGravity(Gravity.END)
setAnchorView(anchor)
setAdapter(adapter) setAdapter(adapter)
setOnItemClickListener(object : AdapterView.OnItemClickListener { setOnItemClickListener(object : AdapterView.OnItemClickListener {
override fun onItemClick( override fun onItemClick(
parent: AdapterView<*>, parent: AdapterView<*>,
@@ -618,7 +618,8 @@ class ControlsUiControllerImpl @Inject constructor (
anchor.setOnClickListener(object : View.OnClickListener { anchor.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) { override fun onClick(v: View) {
popup = ControlsPopupMenu(popupThemedContext).apply { popup = ControlsPopupMenu(popupThemedContext).apply {
setAnchorView(anchor) anchorView = anchor
width = ViewGroup.LayoutParams.MATCH_PARENT
setAdapter(adapter) setAdapter(adapter)
setOnItemClickListener(object : AdapterView.OnItemClickListener { setOnItemClickListener(object : AdapterView.OnItemClickListener {

View File

@@ -22,6 +22,7 @@ import android.graphics.drawable.ShapeDrawable
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.PopupWindow.OnDismissListener import android.widget.PopupWindow.OnDismissListener
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
@@ -29,13 +30,17 @@ import com.android.systemui.R
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.activity.EmptyTestActivity import com.android.systemui.activity.EmptyTestActivity
import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.whenever
import com.android.systemui.widget.FakeListAdapter
import com.android.systemui.widget.FakeListAdapter.FakeListAdapterItem
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.spy import org.mockito.Mockito.spy
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest @SmallTest
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
@@ -52,10 +57,16 @@ open class ControlsPopupMenuTest : SysuiTestCase() {
@Rule @JvmField val activityScenarioRule = ActivityScenarioRule(EmptyTestActivity::class.java) @Rule @JvmField val activityScenarioRule = ActivityScenarioRule(EmptyTestActivity::class.java)
private val testDisplayMetrics: DisplayMetrics = DisplayMetrics() private val testDisplayMetrics = DisplayMetrics()
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
}
@Test @Test
fun testDismissListenerWorks() = testPopup { popupMenu -> fun testDismissListenerWorks() = testPopup { activity, popupMenu ->
popupMenu.setAdapter(FakeListAdapter())
val listener = mock(OnDismissListener::class.java) val listener = mock(OnDismissListener::class.java)
popupMenu.setOnDismissListener(listener) popupMenu.setOnDismissListener(listener)
popupMenu.show() popupMenu.show()
@@ -66,7 +77,9 @@ open class ControlsPopupMenuTest : SysuiTestCase() {
} }
@Test @Test
fun testPopupDoesntExceedMaxWidth() = testPopup { popupMenu -> fun testPopupDoesntExceedMaxWidth() = testPopup { activity, popupMenu ->
popupMenu.setAdapter(FakeListAdapter())
popupMenu.width = ViewGroup.LayoutParams.MATCH_PARENT
testDisplayMetrics.widthPixels = DISPLAY_WIDTH_WIDE testDisplayMetrics.widthPixels = DISPLAY_WIDTH_WIDE
popupMenu.show() popupMenu.show()
@@ -75,7 +88,9 @@ open class ControlsPopupMenuTest : SysuiTestCase() {
} }
@Test @Test
fun testPopupMarginsWidthLessMax() = testPopup { popupMenu -> fun testPopupMarginsWidthLessMax() = testPopup { activity, popupMenu ->
popupMenu.setAdapter(FakeListAdapter())
popupMenu.width = ViewGroup.LayoutParams.MATCH_PARENT
testDisplayMetrics.widthPixels = DISPLAY_WIDTH_NARROW testDisplayMetrics.widthPixels = DISPLAY_WIDTH_NARROW
popupMenu.show() popupMenu.show()
@@ -83,10 +98,32 @@ open class ControlsPopupMenuTest : SysuiTestCase() {
assertThat(popupMenu.width).isEqualTo(DISPLAY_WIDTH_NARROW - 2 * HORIZONTAL_MARGIN) assertThat(popupMenu.width).isEqualTo(DISPLAY_WIDTH_NARROW - 2 * HORIZONTAL_MARGIN)
} }
private fun testPopup(test: (popup: ControlsPopupMenu) -> Unit) { @Test
fun testWrapContentDoesntExceedMax() = testPopup { activity, popupMenu ->
popupMenu.setAdapter(
FakeListAdapter(
listOf(
FakeListAdapterItem({ _, _, _ ->
View(activity).apply { minimumWidth = MAX_WIDTH + 1 }
})
)
)
)
popupMenu.width = ViewGroup.LayoutParams.WRAP_CONTENT
testDisplayMetrics.widthPixels = DISPLAY_WIDTH_NARROW
popupMenu.show()
assertThat(popupMenu.width).isEqualTo(DISPLAY_WIDTH_NARROW - 2 * HORIZONTAL_MARGIN)
}
private fun testPopup(test: (activity: Activity, popup: ControlsPopupMenu) -> Unit) {
activityScenarioRule.scenario.onActivity { activity -> activityScenarioRule.scenario.onActivity { activity ->
val testActivity = setupActivity(activity) val testActivity = setupActivity(activity)
test(ControlsPopupMenu(testActivity).apply { anchorView = View(testActivity) }) test(
testActivity,
ControlsPopupMenu(testActivity).apply { anchorView = View(testActivity) }
)
} }
} }

View File

@@ -0,0 +1,47 @@
/*
* 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.widget
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
class FakeListAdapter(private var items: List<FakeListAdapterItem> = emptyList()) : BaseAdapter() {
fun setItems(items: List<FakeListAdapterItem>) {
this.items = items
notifyDataSetChanged()
}
override fun getCount(): Int = items.size
override fun getItem(position: Int): Any = items[position].data
override fun getItemId(position: Int): Long = items[position].id
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View =
items[position].view(position, convertView, parent)
class FakeListAdapterItem(
/** Result returned in [Adapter#getView] */
val view: (position: Int, convertView: View?, parent: ViewGroup?) -> View,
/** Returned in [Adapter#getItemId] */
val id: Long = 0,
/** Returned in [Adapter#getItem] */
val data: Any = Unit,
)
}