Merge "[Sb chip] Fix chip layout when fullscreen or rotating" into udc-qpr-dev am: 811ef53bfd
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23874234 Change-Id: Ie0face5a19a105ce6c544bda57103dd9642a5627 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -30,9 +30,11 @@ import androidx.core.animation.Animator
|
|||||||
import androidx.core.animation.AnimatorListenerAdapter
|
import androidx.core.animation.AnimatorListenerAdapter
|
||||||
import androidx.core.animation.AnimatorSet
|
import androidx.core.animation.AnimatorSet
|
||||||
import androidx.core.animation.ValueAnimator
|
import androidx.core.animation.ValueAnimator
|
||||||
|
import com.android.internal.annotations.VisibleForTesting
|
||||||
import com.android.systemui.R
|
import com.android.systemui.R
|
||||||
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.statusbar.phone.StatusBarContentInsetsChangedListener
|
||||||
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
|
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
|
||||||
import com.android.systemui.statusbar.window.StatusBarWindowController
|
import com.android.systemui.statusbar.window.StatusBarWindowController
|
||||||
import com.android.systemui.util.animation.AnimationUtil.Companion.frames
|
import com.android.systemui.util.animation.AnimationUtil.Companion.frames
|
||||||
@@ -46,7 +48,7 @@ class SystemEventChipAnimationController @Inject constructor(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val statusBarWindowController: StatusBarWindowController,
|
private val statusBarWindowController: StatusBarWindowController,
|
||||||
private val contentInsetsProvider: StatusBarContentInsetsProvider,
|
private val contentInsetsProvider: StatusBarContentInsetsProvider,
|
||||||
private val featureFlags: FeatureFlags
|
private val featureFlags: FeatureFlags,
|
||||||
) : SystemStatusAnimationCallback {
|
) : SystemStatusAnimationCallback {
|
||||||
|
|
||||||
private lateinit var animationWindowView: FrameLayout
|
private lateinit var animationWindowView: FrameLayout
|
||||||
@@ -56,7 +58,8 @@ class SystemEventChipAnimationController @Inject constructor(
|
|||||||
|
|
||||||
// Left for LTR, Right for RTL
|
// Left for LTR, Right for RTL
|
||||||
private var animationDirection = LEFT
|
private var animationDirection = LEFT
|
||||||
private var chipBounds = Rect()
|
|
||||||
|
@VisibleForTesting var chipBounds = Rect()
|
||||||
private val chipWidth get() = chipBounds.width()
|
private val chipWidth get() = chipBounds.width()
|
||||||
private val chipRight get() = chipBounds.right
|
private val chipRight get() = chipBounds.right
|
||||||
private val chipLeft get() = chipBounds.left
|
private val chipLeft get() = chipBounds.left
|
||||||
@@ -69,7 +72,7 @@ class SystemEventChipAnimationController @Inject constructor(
|
|||||||
private var animRect = Rect()
|
private var animRect = Rect()
|
||||||
|
|
||||||
// TODO: move to dagger
|
// TODO: move to dagger
|
||||||
private var initialized = false
|
@VisibleForTesting var initialized = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Give the chip controller a chance to inflate and configure the chip view before we start
|
* Give the chip controller a chance to inflate and configure the chip view before we start
|
||||||
@@ -98,23 +101,7 @@ class SystemEventChipAnimationController @Inject constructor(
|
|||||||
View.MeasureSpec.makeMeasureSpec(
|
View.MeasureSpec.makeMeasureSpec(
|
||||||
(animationWindowView.parent as View).height, AT_MOST))
|
(animationWindowView.parent as View).height, AT_MOST))
|
||||||
|
|
||||||
// decide which direction we're animating from, and then set some screen coordinates
|
updateChipBounds(it, contentInsetsProvider.getStatusBarContentAreaForCurrentRotation())
|
||||||
val contentRect = contentInsetsProvider.getStatusBarContentAreaForCurrentRotation()
|
|
||||||
val chipTop = ((animationWindowView.parent as View).height - it.view.measuredHeight) / 2
|
|
||||||
val chipBottom = chipTop + it.view.measuredHeight
|
|
||||||
val chipRight: Int
|
|
||||||
val chipLeft: Int
|
|
||||||
when (animationDirection) {
|
|
||||||
LEFT -> {
|
|
||||||
chipRight = contentRect.right
|
|
||||||
chipLeft = contentRect.right - it.chipWidth
|
|
||||||
}
|
|
||||||
else /* RIGHT */ -> {
|
|
||||||
chipLeft = contentRect.left
|
|
||||||
chipRight = contentRect.left + it.chipWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chipBounds = Rect(chipLeft, chipTop, chipRight, chipBottom)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,16 +240,67 @@ class SystemEventChipAnimationController @Inject constructor(
|
|||||||
return animSet
|
return animSet
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun init() {
|
fun init() {
|
||||||
initialized = true
|
initialized = true
|
||||||
themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
|
themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
|
||||||
animationWindowView = LayoutInflater.from(themedContext)
|
animationWindowView = LayoutInflater.from(themedContext)
|
||||||
.inflate(R.layout.system_event_animation_window, null) as FrameLayout
|
.inflate(R.layout.system_event_animation_window, null) as FrameLayout
|
||||||
val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
// Matches status_bar.xml
|
||||||
lp.gravity = Gravity.END or Gravity.CENTER_VERTICAL
|
val height = themedContext.resources.getDimensionPixelSize(R.dimen.status_bar_height)
|
||||||
|
val lp = FrameLayout.LayoutParams(MATCH_PARENT, height)
|
||||||
|
lp.gravity = Gravity.END or Gravity.TOP
|
||||||
statusBarWindowController.addViewToWindow(animationWindowView, lp)
|
statusBarWindowController.addViewToWindow(animationWindowView, lp)
|
||||||
animationWindowView.clipToPadding = false
|
animationWindowView.clipToPadding = false
|
||||||
animationWindowView.clipChildren = false
|
animationWindowView.clipChildren = false
|
||||||
|
|
||||||
|
// Use contentInsetsProvider rather than configuration controller, since we only care
|
||||||
|
// about status bar dimens
|
||||||
|
contentInsetsProvider.addCallback(object : StatusBarContentInsetsChangedListener {
|
||||||
|
override fun onStatusBarContentInsetsChanged() {
|
||||||
|
val newContentArea = contentInsetsProvider
|
||||||
|
.getStatusBarContentAreaForCurrentRotation()
|
||||||
|
updateDimens(newContentArea)
|
||||||
|
|
||||||
|
// If we are currently animating, we have to re-solve for the chip bounds. If we're
|
||||||
|
// not animating then [prepareChipAnimation] will take care of it for us
|
||||||
|
currentAnimatedView?.let {
|
||||||
|
updateChipBounds(it, newContentArea)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDimens(contentArea: Rect) {
|
||||||
|
val lp = animationWindowView.layoutParams as FrameLayout.LayoutParams
|
||||||
|
lp.height = contentArea.height()
|
||||||
|
|
||||||
|
animationWindowView.layoutParams = lp
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the current status bar content area and the current chip's measured size to update
|
||||||
|
* the animation rect and chipBounds. This method can be called at any time and will update
|
||||||
|
* the current animation values properly during e.g. a rotation.
|
||||||
|
*/
|
||||||
|
private fun updateChipBounds(chip: BackgroundAnimatableView, contentArea: Rect) {
|
||||||
|
// decide which direction we're animating from, and then set some screen coordinates
|
||||||
|
val chipTop = (contentArea.bottom - chip.view.measuredHeight) / 2
|
||||||
|
val chipBottom = chipTop + chip.view.measuredHeight
|
||||||
|
val chipRight: Int
|
||||||
|
val chipLeft: Int
|
||||||
|
|
||||||
|
when (animationDirection) {
|
||||||
|
LEFT -> {
|
||||||
|
chipRight = contentArea.right
|
||||||
|
chipLeft = contentArea.right - chip.chipWidth
|
||||||
|
}
|
||||||
|
else /* RIGHT */ -> {
|
||||||
|
chipLeft = contentArea.left
|
||||||
|
chipRight = contentArea.left + chip.chipWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chipBounds = Rect(chipLeft, chipTop, chipRight, chipBottom)
|
||||||
|
animRect.set(chipBounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun layoutParamsDefault(marginEnd: Int): FrameLayout.LayoutParams =
|
private fun layoutParamsDefault(marginEnd: Int): FrameLayout.LayoutParams =
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* 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.statusbar.events
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.util.Pair
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.android.systemui.flags.FakeFeatureFlags
|
||||||
|
import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
|
||||||
|
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
|
||||||
|
import com.android.systemui.statusbar.window.StatusBarWindowController
|
||||||
|
import com.android.systemui.util.mockito.any
|
||||||
|
import com.android.systemui.util.mockito.argumentCaptor
|
||||||
|
import com.android.systemui.util.mockito.capture
|
||||||
|
import com.android.systemui.util.mockito.whenever
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
class SystemEventChipAnimationControllerTest : SysuiTestCase() {
|
||||||
|
private lateinit var controller: SystemEventChipAnimationController
|
||||||
|
|
||||||
|
@Mock private lateinit var sbWindowController: StatusBarWindowController
|
||||||
|
@Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider
|
||||||
|
|
||||||
|
private var testView = TestView(mContext)
|
||||||
|
private var viewCreator: ViewCreator = { testView }
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
|
||||||
|
// StatusBarWindowController is mocked. The addViewToWindow function needs to be mocked to
|
||||||
|
// ensure that the chip view is added to a parent view
|
||||||
|
whenever(sbWindowController.addViewToWindow(any(), any())).then {
|
||||||
|
val statusbarFake = FrameLayout(mContext)
|
||||||
|
statusbarFake.layout(
|
||||||
|
portraitArea.left,
|
||||||
|
portraitArea.top,
|
||||||
|
portraitArea.right,
|
||||||
|
portraitArea.bottom,
|
||||||
|
)
|
||||||
|
statusbarFake.addView(
|
||||||
|
it.arguments[0] as View,
|
||||||
|
it.arguments[1] as FrameLayout.LayoutParams
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
whenever(insetsProvider.getStatusBarContentInsetsForCurrentRotation())
|
||||||
|
.thenReturn(Pair(insets, insets))
|
||||||
|
whenever(insetsProvider.getStatusBarContentAreaForCurrentRotation())
|
||||||
|
.thenReturn(portraitArea)
|
||||||
|
|
||||||
|
controller =
|
||||||
|
SystemEventChipAnimationController(
|
||||||
|
context = mContext,
|
||||||
|
statusBarWindowController = sbWindowController,
|
||||||
|
contentInsetsProvider = insetsProvider,
|
||||||
|
featureFlags = FakeFeatureFlags(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun prepareChipAnimation_lazyInitializes() {
|
||||||
|
// Until Dagger can do our initialization, make sure that the first chip animation calls
|
||||||
|
// init()
|
||||||
|
assertFalse(controller.initialized)
|
||||||
|
controller.prepareChipAnimation(viewCreator)
|
||||||
|
assertTrue(controller.initialized)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun prepareChipAnimation_positionsChip() {
|
||||||
|
controller.prepareChipAnimation(viewCreator)
|
||||||
|
val chipRect = controller.chipBounds
|
||||||
|
|
||||||
|
// SB area = 10, 0, 990, 100
|
||||||
|
// chip size = 0, 0, 100, 50
|
||||||
|
assertThat(chipRect).isEqualTo(Rect(890, 25, 990, 75))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun prepareChipAnimation_rotation_repositionsChip() {
|
||||||
|
controller.prepareChipAnimation(viewCreator)
|
||||||
|
|
||||||
|
// Chip has been prepared, and is located at (890, 25, 990, 75)
|
||||||
|
// Rotation should put it into its landscape location:
|
||||||
|
// SB area = 10, 0, 1990, 80
|
||||||
|
// chip size = 0, 0, 100, 50
|
||||||
|
|
||||||
|
whenever(insetsProvider.getStatusBarContentAreaForCurrentRotation())
|
||||||
|
.thenReturn(landscapeArea)
|
||||||
|
getInsetsListener().onStatusBarContentInsetsChanged()
|
||||||
|
|
||||||
|
val chipRect = controller.chipBounds
|
||||||
|
assertThat(chipRect).isEqualTo(Rect(1890, 15, 1990, 65))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** regression test for (b/289378932) */
|
||||||
|
@Test
|
||||||
|
fun fullScreenStatusBar_positionsChipAtTop_withTopGravity() {
|
||||||
|
// In the case of a fullscreen status bar window, the content insets area is still correct
|
||||||
|
// (because it uses the dimens), but the window can be full screen. This seems to happen
|
||||||
|
// when launching an app from the ongoing call chip.
|
||||||
|
|
||||||
|
// GIVEN layout the status bar window fullscreen portrait
|
||||||
|
whenever(sbWindowController.addViewToWindow(any(), any())).then {
|
||||||
|
val statusbarFake = FrameLayout(mContext)
|
||||||
|
statusbarFake.layout(
|
||||||
|
fullScreenSb.left,
|
||||||
|
fullScreenSb.top,
|
||||||
|
fullScreenSb.right,
|
||||||
|
fullScreenSb.bottom,
|
||||||
|
)
|
||||||
|
|
||||||
|
val lp = it.arguments[1] as FrameLayout.LayoutParams
|
||||||
|
assertThat(lp.gravity and Gravity.VERTICAL_GRAVITY_MASK).isEqualTo(Gravity.TOP)
|
||||||
|
|
||||||
|
statusbarFake.addView(
|
||||||
|
it.arguments[0] as View,
|
||||||
|
lp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GIVEN insets provider gives the correct content area
|
||||||
|
whenever(insetsProvider.getStatusBarContentAreaForCurrentRotation())
|
||||||
|
.thenReturn(portraitArea)
|
||||||
|
|
||||||
|
// WHEN the controller lays out the chip in a fullscreen window
|
||||||
|
controller.prepareChipAnimation(viewCreator)
|
||||||
|
|
||||||
|
// THEN it still aligns the chip to the content area provided by the insets provider
|
||||||
|
val chipRect = controller.chipBounds
|
||||||
|
assertThat(chipRect).isEqualTo(Rect(890, 25, 990, 75))
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestView(context: Context) : View(context), BackgroundAnimatableView {
|
||||||
|
override val view: View
|
||||||
|
get() = this
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
setMeasuredDimension(100, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int) {
|
||||||
|
setLeftTopRightBottom(l, t, r, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getInsetsListener(): StatusBarContentInsetsChangedListener {
|
||||||
|
val callbackCaptor = argumentCaptor<StatusBarContentInsetsChangedListener>()
|
||||||
|
verify(insetsProvider).addCallback(capture(callbackCaptor))
|
||||||
|
return callbackCaptor.value!!
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val portraitArea = Rect(10, 0, 990, 100)
|
||||||
|
private val landscapeArea = Rect(10, 0, 1990, 80)
|
||||||
|
private val fullScreenSb = Rect(10, 0, 990, 2000)
|
||||||
|
|
||||||
|
// 10px insets on both sides
|
||||||
|
private const val insets = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user