Loading packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt +61 −23 Original line number Diff line number Diff line Loading @@ -30,9 +30,11 @@ import androidx.core.animation.Animator import androidx.core.animation.AnimatorListenerAdapter import androidx.core.animation.AnimatorSet import androidx.core.animation.ValueAnimator import com.android.internal.annotations.VisibleForTesting import com.android.systemui.R import com.android.systemui.flags.FeatureFlags 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.window.StatusBarWindowController import com.android.systemui.util.animation.AnimationUtil.Companion.frames Loading @@ -46,7 +48,7 @@ class SystemEventChipAnimationController @Inject constructor( private val context: Context, private val statusBarWindowController: StatusBarWindowController, private val contentInsetsProvider: StatusBarContentInsetsProvider, private val featureFlags: FeatureFlags private val featureFlags: FeatureFlags, ) : SystemStatusAnimationCallback { private lateinit var animationWindowView: FrameLayout Loading @@ -56,7 +58,8 @@ class SystemEventChipAnimationController @Inject constructor( // Left for LTR, Right for RTL private var animationDirection = LEFT private var chipBounds = Rect() @VisibleForTesting var chipBounds = Rect() private val chipWidth get() = chipBounds.width() private val chipRight get() = chipBounds.right private val chipLeft get() = chipBounds.left Loading @@ -69,7 +72,7 @@ class SystemEventChipAnimationController @Inject constructor( private var animRect = Rect() // 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 Loading Loading @@ -98,23 +101,7 @@ class SystemEventChipAnimationController @Inject constructor( View.MeasureSpec.makeMeasureSpec( (animationWindowView.parent as View).height, AT_MOST)) // decide which direction we're animating from, and then set some screen coordinates 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) updateChipBounds(it, contentInsetsProvider.getStatusBarContentAreaForCurrentRotation()) } } Loading Loading @@ -253,16 +240,67 @@ class SystemEventChipAnimationController @Inject constructor( return animSet } private fun init() { fun init() { initialized = true themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) animationWindowView = LayoutInflater.from(themedContext) .inflate(R.layout.system_event_animation_window, null) as FrameLayout val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) lp.gravity = Gravity.END or Gravity.CENTER_VERTICAL // Matches status_bar.xml 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) animationWindowView.clipToPadding = 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 = Loading packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt 0 → 100644 +189 −0 Original line number Diff line number Diff line /* * 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 } } Loading
packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt +61 −23 Original line number Diff line number Diff line Loading @@ -30,9 +30,11 @@ import androidx.core.animation.Animator import androidx.core.animation.AnimatorListenerAdapter import androidx.core.animation.AnimatorSet import androidx.core.animation.ValueAnimator import com.android.internal.annotations.VisibleForTesting import com.android.systemui.R import com.android.systemui.flags.FeatureFlags 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.window.StatusBarWindowController import com.android.systemui.util.animation.AnimationUtil.Companion.frames Loading @@ -46,7 +48,7 @@ class SystemEventChipAnimationController @Inject constructor( private val context: Context, private val statusBarWindowController: StatusBarWindowController, private val contentInsetsProvider: StatusBarContentInsetsProvider, private val featureFlags: FeatureFlags private val featureFlags: FeatureFlags, ) : SystemStatusAnimationCallback { private lateinit var animationWindowView: FrameLayout Loading @@ -56,7 +58,8 @@ class SystemEventChipAnimationController @Inject constructor( // Left for LTR, Right for RTL private var animationDirection = LEFT private var chipBounds = Rect() @VisibleForTesting var chipBounds = Rect() private val chipWidth get() = chipBounds.width() private val chipRight get() = chipBounds.right private val chipLeft get() = chipBounds.left Loading @@ -69,7 +72,7 @@ class SystemEventChipAnimationController @Inject constructor( private var animRect = Rect() // 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 Loading Loading @@ -98,23 +101,7 @@ class SystemEventChipAnimationController @Inject constructor( View.MeasureSpec.makeMeasureSpec( (animationWindowView.parent as View).height, AT_MOST)) // decide which direction we're animating from, and then set some screen coordinates 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) updateChipBounds(it, contentInsetsProvider.getStatusBarContentAreaForCurrentRotation()) } } Loading Loading @@ -253,16 +240,67 @@ class SystemEventChipAnimationController @Inject constructor( return animSet } private fun init() { fun init() { initialized = true themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) animationWindowView = LayoutInflater.from(themedContext) .inflate(R.layout.system_event_animation_window, null) as FrameLayout val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) lp.gravity = Gravity.END or Gravity.CENTER_VERTICAL // Matches status_bar.xml 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) animationWindowView.clipToPadding = 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 = Loading
packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt 0 → 100644 +189 −0 Original line number Diff line number Diff line /* * 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 } }