Merge "Add unfold animation to launcher icons and widgets" into sc-v2-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
ef994701aa
@@ -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
|
||||
*/
|
||||
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()
|
||||
|
||||
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) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ import com.android.internal.util.ScreenshotHelper;
|
||||
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;
|
||||
@@ -543,6 +544,7 @@ public class OverviewProxyService extends CurrentUserTracker implements
|
||||
Optional<OneHanded> oneHandedOptional,
|
||||
BroadcastDispatcher broadcastDispatcher,
|
||||
ShellTransitions shellTransitions,
|
||||
ScreenLifecycle screenLifecycle,
|
||||
Optional<StartingSurface> 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();
|
||||
|
||||
@@ -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