diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt new file mode 100644 index 0000000000000..4bb4eb9c020ea --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt @@ -0,0 +1,146 @@ +/* + * 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.systemui.shared.animation + +import android.graphics.Point +import android.util.MathUtils.lerp +import android.view.Surface +import android.view.View +import android.view.WindowManager +import com.android.unfold.UnfoldTransitionProgressProvider +import java.lang.ref.WeakReference + +/** + * Creates an animation where all registered views are moved into their final location + * by moving from the center of the screen to the sides + */ +class UnfoldMoveFromCenterAnimator( + private val windowManager: WindowManager, + /** + * Allows to set custom translation applier + * Could be useful when a view could be translated from + * several sources and we want to set the translation + * using custom methods instead of [View.setTranslationX] or + * [View.setTranslationY] + */ + var translationApplier: TranslationApplier = object : TranslationApplier {} +) : UnfoldTransitionProgressProvider.TransitionProgressListener { + + private val screenSize = Point() + private var isVerticalFold = false + + private val animatedViews: MutableList = arrayListOf() + private val tmpArray = IntArray(2) + + /** + * Updates display properties in order to calculate the initial position for the views + * Must be called before [registerViewForAnimation] + */ + fun updateDisplayProperties() { + windowManager.defaultDisplay.getSize(screenSize) + + // Simple implementation to get current fold orientation, + // this might not be correct on all devices + // TODO: use JetPack WindowManager library to get the fold orientation + isVerticalFold = windowManager.defaultDisplay.rotation == Surface.ROTATION_0 || + windowManager.defaultDisplay.rotation == Surface.ROTATION_180 + } + + /** + * Registers a view to be animated, the view should be measured and layouted + * After finishing the animation it is necessary to clear + * the views using [clearRegisteredViews] + */ + fun registerViewForAnimation(view: View) { + val animatedView = createAnimatedView(view) + animatedViews.add(animatedView) + } + + /** + * Unregisters all registered views and resets their translation + */ + fun clearRegisteredViews() { + onTransitionProgress(1f) + animatedViews.clear() + } + + override fun onTransitionProgress(progress: Float) { + animatedViews.forEach { + it.view.get()?.let { view -> + translationApplier.apply( + view = view, + x = lerp(it.startTranslationX, it.finishTranslationX, progress), + y = lerp(it.startTranslationY, it.finishTranslationY, progress) + ) + } + } + } + + private fun createAnimatedView(view: View): AnimatedView { + val viewLocation = tmpArray + view.getLocationOnScreen(viewLocation) + + val viewX = viewLocation[0].toFloat() + val viewY = viewLocation[1].toFloat() + + val viewCenterX = viewX + view.width / 2 + val viewCenterY = viewY + view.height / 2 + + val translationXDiff: Float + val translationYDiff: Float + + if (isVerticalFold) { + val distanceFromScreenCenterToViewCenter = screenSize.x / 2 - viewCenterX + translationXDiff = distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE + translationYDiff = 0f + } else { + val distanceFromScreenCenterToViewCenter = screenSize.y / 2 - viewCenterY + translationXDiff = 0f + translationYDiff = distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE + } + + return AnimatedView( + view = WeakReference(view), + startTranslationX = view.translationX + translationXDiff, + startTranslationY = view.translationY + translationYDiff, + finishTranslationX = view.translationX, + finishTranslationY = view.translationY + ) + } + + /** + * Interface that allows to use custom logic to apply translation to view + */ + interface TranslationApplier { + /** + * Called when we need to apply [x] and [y] translation to [view] + */ + fun apply(view: View, x: Float, y: Float) { + view.translationX = x + view.translationY = y + } + } + + private class AnimatedView( + val view: WeakReference, + val startTranslationX: Float, + val startTranslationY: Float, + val finishTranslationX: Float, + val finishTranslationY: Float + ) +} + +private const val TRANSLATION_PERCENTAGE = 0.3f diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl index de9558ebca47b..8bd0f910dac3d 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl @@ -90,4 +90,9 @@ oneway interface IOverviewProxy { * Sent when behavior changes. See WindowInsetsController#@Behavior */ void onSystemBarAttributesChanged(int displayId, int behavior) = 20; + + /** + * Sent when screen turned on and ready to use (blocker scrim is hidden) + */ + void onScreenTurnedOn() = 21; } diff --git a/packages/SystemUI/shared/src/com/android/unfold/UnfoldTransitionProgressProvider.kt b/packages/SystemUI/shared/src/com/android/unfold/UnfoldTransitionProgressProvider.kt index 2ddb49c2e693f..4a6a9ac266f25 100644 --- a/packages/SystemUI/shared/src/com/android/unfold/UnfoldTransitionProgressProvider.kt +++ b/packages/SystemUI/shared/src/com/android/unfold/UnfoldTransitionProgressProvider.kt @@ -31,8 +31,8 @@ interface UnfoldTransitionProgressProvider : CallbackController oneHandedOptional, BroadcastDispatcher broadcastDispatcher, ShellTransitions shellTransitions, + ScreenLifecycle screenLifecycle, Optional startingSurface, SmartspaceTransitionController smartspaceTransitionController) { super(broadcastDispatcher); @@ -608,6 +610,13 @@ public class OverviewProxyService extends CurrentUserTracker implements // Listen for user setup startTracking(); + screenLifecycle.addObserver(new ScreenLifecycle.Observer() { + @Override + public void onScreenTurnedOn() { + notifyScreenTurnedOn(); + } + }); + // Connect to the service updateEnabledState(); startConnectionToCurrentUser(); @@ -900,6 +909,21 @@ public class OverviewProxyService extends CurrentUserTracker implements } } + /** + * Notifies the Launcher that screen turned on and ready to use + */ + public void notifyScreenTurnedOn() { + try { + if (mOverviewProxy != null) { + mOverviewProxy.onScreenTurnedOn(); + } else { + Log.e(TAG_OPS, "Failed to get overview proxy for screen turned on event."); + } + } catch (RemoteException e) { + Log.e(TAG_OPS, "Failed to call notifyScreenTurnedOn()", e); + } + } + void notifyToggleRecentApps() { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onToggleRecentApps(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimatorTest.kt new file mode 100644 index 0000000000000..a9c6a53533501 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimatorTest.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2018 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.shared.animation + +import android.graphics.Point +import android.test.suitebuilder.annotation.SmallTest +import android.testing.AndroidTestingRunner +import android.view.Display +import android.view.Surface.ROTATION_0 +import android.view.Surface.ROTATION_90 +import android.view.View +import android.view.WindowManager +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.any +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import org.mockito.junit.MockitoJUnit +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class UnfoldMoveFromCenterAnimatorTest : SysuiTestCase() { + + @Mock + private lateinit var windowManager: WindowManager + + @get:Rule + val mockito = MockitoJUnit.rule() + + private lateinit var animator: UnfoldMoveFromCenterAnimator + + @Before + fun before() { + animator = UnfoldMoveFromCenterAnimator(windowManager) + } + + @Test + fun testRegisterViewOnTheLeftOfVerticalFold_halfProgress_viewTranslatedToTheRight() { + givenScreen(width = 100, height = 100, rotation = ROTATION_0) + val view = createView(x = 20) + animator.registerViewForAnimation(view) + animator.onTransitionStarted() + + animator.onTransitionProgress(0.5f) + + // Positive translationX -> translated to the right + assertThat(view.translationX).isWithin(0.1f).of(3.75f) + } + + @Test + fun testRegisterViewOnTheLeftOfVerticalFold_zeroProgress_viewTranslatedToTheRight() { + givenScreen(width = 100, height = 100, rotation = ROTATION_0) + val view = createView(x = 20) + animator.registerViewForAnimation(view) + animator.onTransitionStarted() + + animator.onTransitionProgress(0f) + + // Positive translationX -> translated to the right + assertThat(view.translationX).isWithin(0.1f).of(7.5f) + } + + @Test + fun testRegisterViewOnTheLeftOfVerticalFold_fullProgress_viewTranslatedToTheOriginalPosition() { + givenScreen(width = 100, height = 100, rotation = ROTATION_0) + val view = createView(x = 20) + animator.registerViewForAnimation(view) + animator.onTransitionStarted() + + animator.onTransitionProgress(1f) + + assertThat(view.translationX).isEqualTo(0f) + } + + @Test + fun testRegisterViewAndUnregister_halfProgress_viewIsNotUpdated() { + givenScreen(width = 100, height = 100, rotation = ROTATION_0) + val view = createView(x = 20) + animator.registerViewForAnimation(view) + animator.onTransitionStarted() + animator.clearRegisteredViews() + + animator.onTransitionProgress(0.5f) + + assertThat(view.translationX).isEqualTo(0f) + } + + @Test + fun testRegisterViewUpdateProgressAndUnregister_halfProgress_viewIsNotUpdated() { + givenScreen(width = 100, height = 100, rotation = ROTATION_0) + val view = createView(x = 20) + animator.registerViewForAnimation(view) + animator.onTransitionStarted() + animator.onTransitionProgress(0.2f) + animator.clearRegisteredViews() + + animator.onTransitionProgress(0.5f) + + assertThat(view.translationX).isEqualTo(0f) + } + + @Test + fun testRegisterViewOnTheTopOfHorizontalFold_halfProgress_viewTranslatedToTheBottom() { + givenScreen(width = 100, height = 100, rotation = ROTATION_90) + val view = createView(y = 20) + animator.registerViewForAnimation(view) + animator.onTransitionStarted() + + animator.onTransitionProgress(0.5f) + + // Positive translationY -> translated to the bottom + assertThat(view.translationY).isWithin(0.1f).of(3.75f) + } + + private fun createView( + x: Int = 0, + y: Int = 0, + width: Int = 10, + height: Int = 10, + translationX: Float = 0f, + translationY: Float = 0f + ): View { + val view = spy(View(context)) + doAnswer { + val location = (it.arguments[0] as IntArray) + location[0] = x + location[1] = y + Unit + }.`when`(view).getLocationOnScreen(any()) + + whenever(view.width).thenReturn(width) + whenever(view.height).thenReturn(height) + + return view.apply { + setTranslationX(translationX) + setTranslationY(translationY) + } + } + + private fun givenScreen(width: Int = 100, + height: Int = 100, + rotation: Int = ROTATION_0) { + val display = mock(Display::class.java) + whenever(display.getSize(any())).thenAnswer { + val size = (it.arguments[0] as Point) + size.set(width, height) + Unit + } + whenever(display.rotation).thenReturn(rotation) + whenever(windowManager.defaultDisplay).thenReturn(display) + + animator.updateDisplayProperties() + } +}