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
This commit is contained in:
@@ -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" />
|
||||
|
||||
22
packages/SystemUI/res/drawable/controls_popup_bg.xml
Normal file
22
packages/SystemUI/res/drawable/controls_popup_bg.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user