Add unfold animation to launcher icons and widgets
Adds unfold animation to launcher which translates
icons and widgets from the center to the sides on
foldable devices.
Bug: 193794541
Test: manual
Change-Id: I35ca08d0fa45d09e9104f4324f9879206e806caf
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 0000000..4bb4eb9
--- /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<AnimatedView> = 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<View>,
+ 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 de9558e..8bd0f91 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 @@
* 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 2ddb49c..4a6a9ac 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 @@
fun destroy()
interface TransitionProgressListener {
- fun onTransitionStarted()
- fun onTransitionFinished()
- fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float)
+ fun onTransitionStarted() {}
+ fun onTransitionFinished() {}
+ fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index eb72296..a5fc5ab 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -78,6 +78,7 @@
import com.android.systemui.Dumpable;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationBar;
import com.android.systemui.navigationbar.NavigationBarController;
@@ -530,6 +531,7 @@
Optional<OneHanded> oneHandedOptional,
BroadcastDispatcher broadcastDispatcher,
ShellTransitions shellTransitions,
+ ScreenLifecycle screenLifecycle,
Optional<StartingSurface> startingSurface,
SmartspaceTransitionController smartspaceTransitionController) {
super(broadcastDispatcher);
@@ -588,6 +590,13 @@
// Listen for user setup
startTracking();
+ screenLifecycle.addObserver(new ScreenLifecycle.Observer() {
+ @Override
+ public void onScreenTurnedOn() {
+ notifyScreenTurnedOn();
+ }
+ });
+
// Connect to the service
updateEnabledState();
startConnectionToCurrentUser();
@@ -880,6 +889,21 @@
}
}
+ /**
+ * 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 0000000..a9c6a53
--- /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()
+ }
+}