Loading packages/SystemUI/res/values/dimens.xml +2 −0 Original line number Diff line number Diff line Loading @@ -1650,4 +1650,6 @@ <dimen name="qs_dialog_button_vertical_padding">8dp</dimen> <!-- The button will be 48dp tall, but the background needs to be 36dp tall --> <dimen name="qs_dialog_button_vertical_inset">6dp</dimen> <dimen name="keyguard_unfold_translation_x">16dp</dimen> </resources> packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt +67 −3 Original line number Diff line number Diff line Loading @@ -16,34 +16,98 @@ package com.android.keyguard import android.content.Context import android.view.View import android.view.ViewGroup import com.android.systemui.R import com.android.systemui.unfold.SysUIUnfoldScope import com.android.systemui.unfold.UnfoldTransitionProgressProvider import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider import javax.inject.Inject /** * Translates items away/towards the hinge when the device is opened/closed. * Translates items away/towards the hinge when the device is opened/closed. This is controlled by * the set of ids, which also dictact which direction to move and when, via a filter function. */ @SysUIUnfoldScope class KeyguardUnfoldTransition @Inject constructor( val unfoldProgressProvider: UnfoldTransitionProgressProvider val context: Context, val unfoldProgressProvider: NaturalRotationUnfoldProgressProvider ) { companion object { final val LEFT = -1 final val RIGHT = 1 } private val filterSplitShadeOnly = { !statusViewCentered } private val filterNever = { true } private val ids = setOf( Triple(R.id.keyguard_status_area, LEFT, filterNever), Triple(R.id.controls_button, LEFT, filterNever), Triple(R.id.lockscreen_clock_view_large, LEFT, filterSplitShadeOnly), Triple(R.id.lockscreen_clock_view, LEFT, filterNever), Triple(R.id.notification_stack_scroller, RIGHT, filterSplitShadeOnly), Triple(R.id.wallet_button, RIGHT, filterNever) ) private var parent: ViewGroup? = null private var views = listOf<Triple<View, Int, () -> Boolean>>() private var xTranslationMax = 0f /** * Certain views only need to move if they are not currently centered */ var statusViewCentered = false init { unfoldProgressProvider.addCallback( object : TransitionProgressListener { override fun onTransitionStarted() { findViews() } override fun onTransitionProgress(progress: Float) { translateViews(progress) } override fun onTransitionFinished() { translateViews(1f) } } ) } /** * Relies on the [parent] to locate views to translate */ fun setup(parent: ViewGroup) { this.parent = parent xTranslationMax = context.resources.getDimensionPixelSize( R.dimen.keyguard_unfold_translation_x).toFloat() } /** * Manually translate views based on set direction. At the moment * [UnfoldMoveFromCenterAnimator] exists but moves all views a dynamic distance * from their mid-point. This code instead will only ever translate by a fixed amount. */ private fun translateViews(progress: Float) { val xTrans = progress * xTranslationMax - xTranslationMax views.forEach { (view, direction, pred) -> if (pred()) { view.setTranslationX(xTrans * direction) } } } private fun findViews() { parent?.let { p -> views = ids.mapNotNull { (id, direction, pred) -> p.findViewById<View>(id)?.let { Triple(it, direction, pred) } } } } } packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +2 −2 Original line number Diff line number Diff line Loading @@ -905,7 +905,7 @@ public class NotificationPanelViewController extends PanelViewController { } mTapAgainViewController.init(); mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mNotificationContainerParent)); mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView)); } @Override Loading Loading @@ -1150,7 +1150,7 @@ public class NotificationPanelViewController extends PanelViewController { } setKeyguardBottomAreaVisibility(mBarState, false); mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mNotificationContainerParent)); mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView)); } private void attachSplitShadeMediaPlayerContainer(FrameLayout container) { Loading packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt +10 −4 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.unfold import com.android.keyguard.KeyguardUnfoldTransition import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.phone.StatusBarMoveFromCenterAnimationController Loading @@ -38,6 +39,7 @@ annotation class SysUIUnfoldScope * [@SysUIUnfoldScope]. Since [SysUIUnfoldComponent] depends upon: * * [Optional<UnfoldTransitionProgressProvider>] * * [Optional<ScopedUnfoldTransitionProgressProvider>] * * [Optional<NaturalRotationProgressProvider>] * no objects will get constructed if these parameters are empty. */ @Module(subcomponents = [SysUIUnfoldComponent::class]) Loading @@ -48,11 +50,14 @@ class SysUIUnfoldModule { @SysUISingleton fun provideSysUIUnfoldComponent( provider: Optional<UnfoldTransitionProgressProvider>, rotationProvider: Optional<NaturalRotationUnfoldProgressProvider>, @Named(UNFOLD_STATUS_BAR) scopedProvider: Optional<ScopedUnfoldTransitionProgressProvider>, factory: SysUIUnfoldComponent.Factory ) = provider.flatMap { p -> scopedProvider.map { sp -> factory.create(p, sp) } provider.flatMap { p1 -> rotationProvider.flatMap { p2 -> scopedProvider.map { p3 -> factory.create(p1, p2, p3) } } } } Loading @@ -63,8 +68,9 @@ interface SysUIUnfoldComponent { @Subcomponent.Factory interface Factory { fun create( @BindsInstance provider: UnfoldTransitionProgressProvider, @BindsInstance scopedProvider: ScopedUnfoldTransitionProgressProvider @BindsInstance p1: UnfoldTransitionProgressProvider, @BindsInstance p2: NaturalRotationUnfoldProgressProvider, @BindsInstance p3: ScopedUnfoldTransitionProgressProvider ): SysUIUnfoldComponent } Loading packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt 0 → 100644 +149 −0 Original line number Diff line number Diff line /* * Copyright (C) 2021 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.keyguard import android.testing.AndroidTestingRunner import android.view.View import android.view.ViewGroup import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUnfoldTransition.Companion.LEFT import com.android.keyguard.KeyguardUnfoldTransition.Companion.RIGHT import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider import com.android.systemui.util.mockito.capture import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` import org.mockito.Mockito.verify /** * Translates items away/towards the hinge when the device is opened/closed. This is controlled by * the set of ids, which also dictact which direction to move and when, via a filter fn. */ @SmallTest @RunWith(AndroidTestingRunner::class) class KeyguardUnfoldTransitionTest : SysuiTestCase() { @Mock private lateinit var progressProvider: NaturalRotationUnfoldProgressProvider @Captor private lateinit var progressListenerCaptor: ArgumentCaptor<TransitionProgressListener> @Mock private lateinit var parent: ViewGroup private lateinit var keyguardUnfoldTransition: KeyguardUnfoldTransition private lateinit var progressListener: TransitionProgressListener private var xTranslationMax = 0f @Before fun setup() { MockitoAnnotations.initMocks(this) xTranslationMax = context.resources.getDimensionPixelSize( R.dimen.keyguard_unfold_translation_x).toFloat() keyguardUnfoldTransition = KeyguardUnfoldTransition( getContext(), progressProvider ) verify(progressProvider).addCallback(capture(progressListenerCaptor)) progressListener = progressListenerCaptor.value keyguardUnfoldTransition.setup(parent) keyguardUnfoldTransition.statusViewCentered = false } @Test fun onTransition_noMatchingIds() { // GIVEN no views matching any ids // WHEN the transition starts progressListener.onTransitionStarted() progressListener.onTransitionProgress(.1f) // THEN nothing... no exceptions } @Test fun onTransition_oneMovesLeft() { // GIVEN one view with a matching id val view = View(getContext()) `when`(parent.findViewById<View>(R.id.keyguard_status_area)).thenReturn(view) moveAndValidate(listOf(view to LEFT)) } @Test fun onTransition_oneMovesLeftAndOneMovesRightMultipleTimes() { // GIVEN two views with a matching id val leftView = View(getContext()) val rightView = View(getContext()) `when`(parent.findViewById<View>(R.id.keyguard_status_area)).thenReturn(leftView) `when`(parent.findViewById<View>(R.id.notification_stack_scroller)).thenReturn(rightView) moveAndValidate(listOf(leftView to LEFT, rightView to RIGHT)) moveAndValidate(listOf(leftView to LEFT, rightView to RIGHT)) } @Test fun onTransition_centeredViewDoesNotMove() { keyguardUnfoldTransition.statusViewCentered = true val view = View(getContext()) `when`(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view) moveAndValidate(listOf(view to 0)) } private fun moveAndValidate(list: List<Pair<View, Int>>) { // Compare values as ints because -0f != 0f // WHEN the transition starts progressListener.onTransitionStarted() progressListener.onTransitionProgress(0f) list.forEach { (view, direction) -> assertEquals((-xTranslationMax * direction).toInt(), view.getTranslationX().toInt()) } // WHEN the transition progresses, translation is updated progressListener.onTransitionProgress(.5f) list.forEach { (view, direction) -> assertEquals( (-xTranslationMax / 2f * direction).toInt(), view.getTranslationX().toInt() ) } // WHEN the transition ends, translation is completed progressListener.onTransitionProgress(1f) progressListener.onTransitionFinished() list.forEach { (view, _) -> assertEquals(0, view.getTranslationX().toInt()) } } } Loading
packages/SystemUI/res/values/dimens.xml +2 −0 Original line number Diff line number Diff line Loading @@ -1650,4 +1650,6 @@ <dimen name="qs_dialog_button_vertical_padding">8dp</dimen> <!-- The button will be 48dp tall, but the background needs to be 36dp tall --> <dimen name="qs_dialog_button_vertical_inset">6dp</dimen> <dimen name="keyguard_unfold_translation_x">16dp</dimen> </resources>
packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt +67 −3 Original line number Diff line number Diff line Loading @@ -16,34 +16,98 @@ package com.android.keyguard import android.content.Context import android.view.View import android.view.ViewGroup import com.android.systemui.R import com.android.systemui.unfold.SysUIUnfoldScope import com.android.systemui.unfold.UnfoldTransitionProgressProvider import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider import javax.inject.Inject /** * Translates items away/towards the hinge when the device is opened/closed. * Translates items away/towards the hinge when the device is opened/closed. This is controlled by * the set of ids, which also dictact which direction to move and when, via a filter function. */ @SysUIUnfoldScope class KeyguardUnfoldTransition @Inject constructor( val unfoldProgressProvider: UnfoldTransitionProgressProvider val context: Context, val unfoldProgressProvider: NaturalRotationUnfoldProgressProvider ) { companion object { final val LEFT = -1 final val RIGHT = 1 } private val filterSplitShadeOnly = { !statusViewCentered } private val filterNever = { true } private val ids = setOf( Triple(R.id.keyguard_status_area, LEFT, filterNever), Triple(R.id.controls_button, LEFT, filterNever), Triple(R.id.lockscreen_clock_view_large, LEFT, filterSplitShadeOnly), Triple(R.id.lockscreen_clock_view, LEFT, filterNever), Triple(R.id.notification_stack_scroller, RIGHT, filterSplitShadeOnly), Triple(R.id.wallet_button, RIGHT, filterNever) ) private var parent: ViewGroup? = null private var views = listOf<Triple<View, Int, () -> Boolean>>() private var xTranslationMax = 0f /** * Certain views only need to move if they are not currently centered */ var statusViewCentered = false init { unfoldProgressProvider.addCallback( object : TransitionProgressListener { override fun onTransitionStarted() { findViews() } override fun onTransitionProgress(progress: Float) { translateViews(progress) } override fun onTransitionFinished() { translateViews(1f) } } ) } /** * Relies on the [parent] to locate views to translate */ fun setup(parent: ViewGroup) { this.parent = parent xTranslationMax = context.resources.getDimensionPixelSize( R.dimen.keyguard_unfold_translation_x).toFloat() } /** * Manually translate views based on set direction. At the moment * [UnfoldMoveFromCenterAnimator] exists but moves all views a dynamic distance * from their mid-point. This code instead will only ever translate by a fixed amount. */ private fun translateViews(progress: Float) { val xTrans = progress * xTranslationMax - xTranslationMax views.forEach { (view, direction, pred) -> if (pred()) { view.setTranslationX(xTrans * direction) } } } private fun findViews() { parent?.let { p -> views = ids.mapNotNull { (id, direction, pred) -> p.findViewById<View>(id)?.let { Triple(it, direction, pred) } } } } }
packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +2 −2 Original line number Diff line number Diff line Loading @@ -905,7 +905,7 @@ public class NotificationPanelViewController extends PanelViewController { } mTapAgainViewController.init(); mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mNotificationContainerParent)); mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView)); } @Override Loading Loading @@ -1150,7 +1150,7 @@ public class NotificationPanelViewController extends PanelViewController { } setKeyguardBottomAreaVisibility(mBarState, false); mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mNotificationContainerParent)); mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView)); } private void attachSplitShadeMediaPlayerContainer(FrameLayout container) { Loading
packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt +10 −4 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.unfold import com.android.keyguard.KeyguardUnfoldTransition import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.phone.StatusBarMoveFromCenterAnimationController Loading @@ -38,6 +39,7 @@ annotation class SysUIUnfoldScope * [@SysUIUnfoldScope]. Since [SysUIUnfoldComponent] depends upon: * * [Optional<UnfoldTransitionProgressProvider>] * * [Optional<ScopedUnfoldTransitionProgressProvider>] * * [Optional<NaturalRotationProgressProvider>] * no objects will get constructed if these parameters are empty. */ @Module(subcomponents = [SysUIUnfoldComponent::class]) Loading @@ -48,11 +50,14 @@ class SysUIUnfoldModule { @SysUISingleton fun provideSysUIUnfoldComponent( provider: Optional<UnfoldTransitionProgressProvider>, rotationProvider: Optional<NaturalRotationUnfoldProgressProvider>, @Named(UNFOLD_STATUS_BAR) scopedProvider: Optional<ScopedUnfoldTransitionProgressProvider>, factory: SysUIUnfoldComponent.Factory ) = provider.flatMap { p -> scopedProvider.map { sp -> factory.create(p, sp) } provider.flatMap { p1 -> rotationProvider.flatMap { p2 -> scopedProvider.map { p3 -> factory.create(p1, p2, p3) } } } } Loading @@ -63,8 +68,9 @@ interface SysUIUnfoldComponent { @Subcomponent.Factory interface Factory { fun create( @BindsInstance provider: UnfoldTransitionProgressProvider, @BindsInstance scopedProvider: ScopedUnfoldTransitionProgressProvider @BindsInstance p1: UnfoldTransitionProgressProvider, @BindsInstance p2: NaturalRotationUnfoldProgressProvider, @BindsInstance p3: ScopedUnfoldTransitionProgressProvider ): SysUIUnfoldComponent } Loading
packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt 0 → 100644 +149 −0 Original line number Diff line number Diff line /* * Copyright (C) 2021 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.keyguard import android.testing.AndroidTestingRunner import android.view.View import android.view.ViewGroup import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUnfoldTransition.Companion.LEFT import com.android.keyguard.KeyguardUnfoldTransition.Companion.RIGHT import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider import com.android.systemui.util.mockito.capture import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` import org.mockito.Mockito.verify /** * Translates items away/towards the hinge when the device is opened/closed. This is controlled by * the set of ids, which also dictact which direction to move and when, via a filter fn. */ @SmallTest @RunWith(AndroidTestingRunner::class) class KeyguardUnfoldTransitionTest : SysuiTestCase() { @Mock private lateinit var progressProvider: NaturalRotationUnfoldProgressProvider @Captor private lateinit var progressListenerCaptor: ArgumentCaptor<TransitionProgressListener> @Mock private lateinit var parent: ViewGroup private lateinit var keyguardUnfoldTransition: KeyguardUnfoldTransition private lateinit var progressListener: TransitionProgressListener private var xTranslationMax = 0f @Before fun setup() { MockitoAnnotations.initMocks(this) xTranslationMax = context.resources.getDimensionPixelSize( R.dimen.keyguard_unfold_translation_x).toFloat() keyguardUnfoldTransition = KeyguardUnfoldTransition( getContext(), progressProvider ) verify(progressProvider).addCallback(capture(progressListenerCaptor)) progressListener = progressListenerCaptor.value keyguardUnfoldTransition.setup(parent) keyguardUnfoldTransition.statusViewCentered = false } @Test fun onTransition_noMatchingIds() { // GIVEN no views matching any ids // WHEN the transition starts progressListener.onTransitionStarted() progressListener.onTransitionProgress(.1f) // THEN nothing... no exceptions } @Test fun onTransition_oneMovesLeft() { // GIVEN one view with a matching id val view = View(getContext()) `when`(parent.findViewById<View>(R.id.keyguard_status_area)).thenReturn(view) moveAndValidate(listOf(view to LEFT)) } @Test fun onTransition_oneMovesLeftAndOneMovesRightMultipleTimes() { // GIVEN two views with a matching id val leftView = View(getContext()) val rightView = View(getContext()) `when`(parent.findViewById<View>(R.id.keyguard_status_area)).thenReturn(leftView) `when`(parent.findViewById<View>(R.id.notification_stack_scroller)).thenReturn(rightView) moveAndValidate(listOf(leftView to LEFT, rightView to RIGHT)) moveAndValidate(listOf(leftView to LEFT, rightView to RIGHT)) } @Test fun onTransition_centeredViewDoesNotMove() { keyguardUnfoldTransition.statusViewCentered = true val view = View(getContext()) `when`(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view) moveAndValidate(listOf(view to 0)) } private fun moveAndValidate(list: List<Pair<View, Int>>) { // Compare values as ints because -0f != 0f // WHEN the transition starts progressListener.onTransitionStarted() progressListener.onTransitionProgress(0f) list.forEach { (view, direction) -> assertEquals((-xTranslationMax * direction).toInt(), view.getTranslationX().toInt()) } // WHEN the transition progresses, translation is updated progressListener.onTransitionProgress(.5f) list.forEach { (view, direction) -> assertEquals( (-xTranslationMax / 2f * direction).toInt(), view.getTranslationX().toInt() ) } // WHEN the transition ends, translation is completed progressListener.onTransitionProgress(1f) progressListener.onTransitionFinished() list.forEach { (view, _) -> assertEquals(0, view.getTranslationX().toInt()) } } }