Merge "Add unfold animation to launcher icons and widgets" into sc-v2-dev am: ef994701aa
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/15390554 Change-Id: Ib609d03616f7b0fccffc0ea48a73bb6741655d7c
This commit is contained in:
@@ -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
|
||||||
@@ -90,4 +90,9 @@ oneway interface IOverviewProxy {
|
|||||||
* Sent when behavior changes. See WindowInsetsController#@Behavior
|
* Sent when behavior changes. See WindowInsetsController#@Behavior
|
||||||
*/
|
*/
|
||||||
void onSystemBarAttributesChanged(int displayId, int behavior) = 20;
|
void onSystemBarAttributesChanged(int displayId, int behavior) = 20;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sent when screen turned on and ready to use (blocker scrim is hidden)
|
||||||
|
*/
|
||||||
|
void onScreenTurnedOn() = 21;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ interface UnfoldTransitionProgressProvider : CallbackController<TransitionProgre
|
|||||||
fun destroy()
|
fun destroy()
|
||||||
|
|
||||||
interface TransitionProgressListener {
|
interface TransitionProgressListener {
|
||||||
fun onTransitionStarted()
|
fun onTransitionStarted() {}
|
||||||
fun onTransitionFinished()
|
fun onTransitionFinished() {}
|
||||||
fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float)
|
fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ import com.android.internal.util.ScreenshotHelper;
|
|||||||
import com.android.systemui.Dumpable;
|
import com.android.systemui.Dumpable;
|
||||||
import com.android.systemui.broadcast.BroadcastDispatcher;
|
import com.android.systemui.broadcast.BroadcastDispatcher;
|
||||||
import com.android.systemui.dagger.SysUISingleton;
|
import com.android.systemui.dagger.SysUISingleton;
|
||||||
|
import com.android.systemui.keyguard.ScreenLifecycle;
|
||||||
import com.android.systemui.model.SysUiState;
|
import com.android.systemui.model.SysUiState;
|
||||||
import com.android.systemui.navigationbar.NavigationBar;
|
import com.android.systemui.navigationbar.NavigationBar;
|
||||||
import com.android.systemui.navigationbar.NavigationBarController;
|
import com.android.systemui.navigationbar.NavigationBarController;
|
||||||
@@ -543,6 +544,7 @@ public class OverviewProxyService extends CurrentUserTracker implements
|
|||||||
Optional<OneHanded> oneHandedOptional,
|
Optional<OneHanded> oneHandedOptional,
|
||||||
BroadcastDispatcher broadcastDispatcher,
|
BroadcastDispatcher broadcastDispatcher,
|
||||||
ShellTransitions shellTransitions,
|
ShellTransitions shellTransitions,
|
||||||
|
ScreenLifecycle screenLifecycle,
|
||||||
Optional<StartingSurface> startingSurface,
|
Optional<StartingSurface> startingSurface,
|
||||||
SmartspaceTransitionController smartspaceTransitionController) {
|
SmartspaceTransitionController smartspaceTransitionController) {
|
||||||
super(broadcastDispatcher);
|
super(broadcastDispatcher);
|
||||||
@@ -608,6 +610,13 @@ public class OverviewProxyService extends CurrentUserTracker implements
|
|||||||
// Listen for user setup
|
// Listen for user setup
|
||||||
startTracking();
|
startTracking();
|
||||||
|
|
||||||
|
screenLifecycle.addObserver(new ScreenLifecycle.Observer() {
|
||||||
|
@Override
|
||||||
|
public void onScreenTurnedOn() {
|
||||||
|
notifyScreenTurnedOn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Connect to the service
|
// Connect to the service
|
||||||
updateEnabledState();
|
updateEnabledState();
|
||||||
startConnectionToCurrentUser();
|
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() {
|
void notifyToggleRecentApps() {
|
||||||
for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
|
for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
|
||||||
mConnectionCallbacks.get(i).onToggleRecentApps();
|
mConnectionCallbacks.get(i).onToggleRecentApps();
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user