Merge "[Unfold animation] Start Launcher animation preemptively to synchronize the first frame" into udc-dev

This commit is contained in:
Nick Chameyev
2023-05-04 09:36:04 +00:00
committed by Android (Google) Code Review
7 changed files with 494 additions and 9 deletions
@@ -43,6 +43,10 @@ public abstract class BaseUnfoldMoveFromCenterAnimator implements TransitionProg
new UnfoldMoveFromCenterRotationListener();
private boolean mAnimationInProgress = false;
// Save the last transition progress so we can re-apply it in case we re-register the view for
// the animation (by calling onPrepareViewsForAnimation)
private Float mLastTransitionProgress = null;
public BaseUnfoldMoveFromCenterAnimator(WindowManager windowManager,
RotationChangeProvider rotationChangeProvider) {
mMoveFromCenterAnimation = new UnfoldMoveFromCenterAnimator(windowManager,
@@ -63,11 +67,13 @@ public abstract class BaseUnfoldMoveFromCenterAnimator implements TransitionProg
@Override
public void onTransitionProgress(float progress) {
mMoveFromCenterAnimation.onTransitionProgress(progress);
mLastTransitionProgress = progress;
}
@CallSuper
@Override
public void onTransitionFinished() {
mLastTransitionProgress = null;
mAnimationInProgress = false;
mRotationChangeProvider.removeCallback(mRotationListener);
mMoveFromCenterAnimation.onTransitionFinished();
@@ -93,8 +99,11 @@ public abstract class BaseUnfoldMoveFromCenterAnimator implements TransitionProg
mOriginalClipToPadding.clear();
}
@CallSuper
protected void onPrepareViewsForAnimation() {
if (mLastTransitionProgress != null) {
mMoveFromCenterAnimation.onTransitionProgress(mLastTransitionProgress);
}
}
protected void registerViewForAnimation(View view) {
@@ -27,10 +27,15 @@ import android.view.WindowManager;
import androidx.core.view.OneShotPreDrawListener;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.launcher3.Hotseat;
import com.android.launcher3.Launcher;
import com.android.launcher3.Workspace;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.util.HorizontalInsettableView;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.unfold.PreemptiveUnfoldTransitionProgressProvider;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener;
import com.android.systemui.unfold.updates.RotationChangeProvider;
@@ -40,7 +45,7 @@ import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
/**
* Controls animations that are happening during unfolding foldable devices
*/
public class LauncherUnfoldAnimationController {
public class LauncherUnfoldAnimationController implements OnDeviceProfileChangeListener {
// Percentage of the width of the quick search bar that will be reduced
// from the both sides of the bar when progress is 0
@@ -55,9 +60,11 @@ public class LauncherUnfoldAnimationController {
private final NaturalRotationUnfoldProgressProvider mNaturalOrientationProgressProvider;
private final UnfoldMoveFromCenterHotseatAnimator mUnfoldMoveFromCenterHotseatAnimator;
private final UnfoldMoveFromCenterWorkspaceAnimator mUnfoldMoveFromCenterWorkspaceAnimator;
private PreemptiveUnfoldTransitionProgressProvider mPreemptiveProgressProvider = null;
private Boolean mIsTablet = null;
private static final String TRACE_WAIT_TO_HANDLE_UNFOLD_TRANSITION =
"waitingOneFrameBeforeHandlingUnfoldAnimation";
"LauncherUnfoldAnimationController#waitingForTheNextFrame";
@Nullable
private HorizontalInsettableView mQsbInsettable;
@@ -68,8 +75,19 @@ public class LauncherUnfoldAnimationController {
UnfoldTransitionProgressProvider unfoldTransitionProgressProvider,
RotationChangeProvider rotationChangeProvider) {
mLauncher = launcher;
mProgressProvider = new ScopedUnfoldTransitionProgressProvider(
unfoldTransitionProgressProvider);
if (FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) {
mPreemptiveProgressProvider = new PreemptiveUnfoldTransitionProgressProvider(
unfoldTransitionProgressProvider, launcher.getMainThreadHandler());
mPreemptiveProgressProvider.init();
mProgressProvider = new ScopedUnfoldTransitionProgressProvider(
mPreemptiveProgressProvider);
} else {
mProgressProvider = new ScopedUnfoldTransitionProgressProvider(
unfoldTransitionProgressProvider);
}
mUnfoldMoveFromCenterHotseatAnimator = new UnfoldMoveFromCenterHotseatAnimator(launcher,
windowManager, rotationChangeProvider);
mUnfoldMoveFromCenterWorkspaceAnimator = new UnfoldMoveFromCenterWorkspaceAnimator(launcher,
@@ -85,6 +103,8 @@ public class LauncherUnfoldAnimationController {
// Animated only in natural orientation
mNaturalOrientationProgressProvider.addCallback(new QsbAnimationListener());
mNaturalOrientationProgressProvider.addCallback(mUnfoldMoveFromCenterHotseatAnimator);
mLauncher.addOnDeviceProfileChangeListener(this);
}
/**
@@ -96,17 +116,21 @@ public class LauncherUnfoldAnimationController {
mQsbInsettable = (HorizontalInsettableView) hotseat.getQsb();
}
handleTransitionOnNextFrame();
mProgressProvider.setReadyToHandleTransition(true);
}
private void handleTransitionOnNextFrame() {
private void preemptivelyStartAnimationOnNextFrame() {
Trace.asyncTraceBegin(Trace.TRACE_TAG_APP,
TRACE_WAIT_TO_HANDLE_UNFOLD_TRANSITION, /* cookie= */ 0);
// Start the animation (and apply the transformations) in pre-draw listener to make sure
// that the views are laid out as some transformations depend on the view sizes and position
OneShotPreDrawListener.add(mLauncher.getWorkspace(),
() -> {
Trace.asyncTraceEnd(Trace.TRACE_TAG_APP,
TRACE_WAIT_TO_HANDLE_UNFOLD_TRANSITION, /* cookie= */ 0);
mProgressProvider.setReadyToHandleTransition(true);
mPreemptiveProgressProvider.preemptivelyStartTransition(
/* initialProgress= */ 0f);
});
}
@@ -124,14 +148,34 @@ public class LauncherUnfoldAnimationController {
public void onDestroy() {
mProgressProvider.destroy();
mNaturalOrientationProgressProvider.destroy();
mLauncher.removeOnDeviceProfileChangeListener(this);
}
/** Called when launcher finished binding its items. */
/**
* Called when launcher has finished binding its items
*/
public void updateRegisteredViewsIfNeeded() {
mUnfoldMoveFromCenterHotseatAnimator.updateRegisteredViewsIfNeeded();
mUnfoldMoveFromCenterWorkspaceAnimator.updateRegisteredViewsIfNeeded();
}
@Override
public void onDeviceProfileChanged(DeviceProfile dp) {
if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) {
return;
}
if (mIsTablet != null && dp.isTablet != mIsTablet) {
if (dp.isTablet && SystemUiProxy.INSTANCE.get(mLauncher).isActive()) {
// Preemptively start the unfold animation to make sure that we have drawn
// the first frame of the animation before the screen gets unblocked
preemptivelyStartAnimationOnNextFrame();
}
}
mIsTablet = dp.isTablet;
}
private class QsbAnimationListener implements TransitionProgressListener {
@Override
@@ -48,6 +48,8 @@ public class UnfoldMoveFromCenterHotseatAnimator extends BaseUnfoldMoveFromCente
View child = hotseatIcons.getChildAt(i);
registerViewForAnimation(child);
}
super.onPrepareViewsForAnimation();
}
@Override
@@ -58,6 +58,8 @@ public class UnfoldMoveFromCenterWorkspaceAnimator extends BaseUnfoldMoveFromCen
setClipChildren(workspace, false);
setClipToPadding(workspace, true);
super.onPrepareViewsForAnimation();
}
@Override
@@ -0,0 +1,161 @@
/*
* Copyright (C) 2023 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.quickstep.util.unfold
import android.os.Handler
import android.os.Trace
import android.util.Log
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
/**
* Transition progress provider wrapper that can preemptively start the transition on demand
* without relying on the source provider. When the source provider has started the animation
* it switches to it.
*
* This might be useful when we want to synchronously start the unfold animation and render
* the first frame during turning on the screen. For example, this is used in Launcher where
* we need to render the first frame of the animation immediately after receiving a configuration
* change event so Window Manager will wait for this frame to be rendered before unblocking
* the screen. We can't rely on the original transition progress as it starts the animation
* after the screen fully turned on (and unblocked), at this moment it is already too late to
* start the animation.
*
* Using this provider we could render the first frame preemptively by sending 'transition started'
* and '0' transition progress before the original progress provider sends these events.
*/
class PreemptiveUnfoldTransitionProgressProvider(
private val source: UnfoldTransitionProgressProvider,
private val handler: Handler
) : UnfoldTransitionProgressProvider, TransitionProgressListener {
private val timeoutRunnable = Runnable {
if (isRunning) {
listeners.forEach { it.onTransitionFinished() }
onPreemptiveStartFinished()
Log.wtf(TAG, "Timeout occurred when waiting for the source transition to start")
}
}
private val listeners = arrayListOf<TransitionProgressListener>()
private var isPreemptivelyRunning = false
private var isSourceRunning = false
private val isRunning: Boolean
get() = isPreemptivelyRunning || isSourceRunning
private val sourceListener =
object : TransitionProgressListener {
override fun onTransitionStarted() {
handler.removeCallbacks(timeoutRunnable)
if (!isRunning) {
listeners.forEach { it.onTransitionStarted() }
}
onPreemptiveStartFinished()
isSourceRunning = true
}
override fun onTransitionProgress(progress: Float) {
if (isRunning) {
listeners.forEach { it.onTransitionProgress(progress) }
isSourceRunning = true
}
}
override fun onTransitionFinishing() {
if (isRunning) {
listeners.forEach { it.onTransitionFinishing() }
isSourceRunning = true
}
}
override fun onTransitionFinished() {
if (isRunning) {
listeners.forEach { it.onTransitionFinished() }
}
isSourceRunning = false
onPreemptiveStartFinished()
handler.removeCallbacks(timeoutRunnable)
}
}
fun init() {
source.addCallback(sourceListener)
}
/**
* Starts the animation preemptively.
*
* - If the source provider is already running, this method won't change any behavior
* - If the source provider has not started running yet, it will call onTransitionStarted
* for all listeners and optionally onTransitionProgress(initialProgress) if supplied.
* When the source provider starts the animation it will switch to send progress and finished
* events from it.
* If the source provider won't start the animation within a timeout, the animation will be
* cancelled and onTransitionFinished will be delivered to the current listeners.
*/
@JvmOverloads
fun preemptivelyStartTransition(initialProgress: Float? = null) {
if (!isRunning) {
Trace.beginAsyncSection("$TAG#startedPreemptively", 0)
listeners.forEach { it.onTransitionStarted() }
initialProgress?.let { progress ->
listeners.forEach { it.onTransitionProgress(progress) }
}
handler.removeCallbacks(timeoutRunnable)
handler.postDelayed(timeoutRunnable, PREEMPTIVE_UNFOLD_TIMEOUT_MS)
}
isPreemptivelyRunning = true
}
fun cancelPreemptiveStart() {
handler.removeCallbacks(timeoutRunnable)
if (isRunning) {
listeners.forEach { it.onTransitionFinished() }
}
onPreemptiveStartFinished()
}
private fun onPreemptiveStartFinished() {
if (isPreemptivelyRunning) {
Trace.endAsyncSection("$TAG#startedPreemptively", 0)
isPreemptivelyRunning = false
}
}
override fun destroy() {
handler.removeCallbacks(timeoutRunnable)
source.removeCallback(sourceListener)
source.destroy()
}
override fun addCallback(listener: TransitionProgressListener) {
listeners += listener
}
override fun removeCallback(listener: TransitionProgressListener) {
listeners -= listener
}
}
const val TAG = "PreemptiveUnfoldTransitionProgressProvider"
const val PREEMPTIVE_UNFOLD_TIMEOUT_MS = 1700L
@@ -0,0 +1,261 @@
/*
* Copyright (C) 2023 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.quickstep.util.unfold
import android.os.Handler
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import android.util.Log
import androidx.test.filters.SmallTest
import com.android.launcher3.util.any
import com.android.launcher3.util.mock
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.anyFloat
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class PreemptiveUnfoldTransitionProgressProviderTest {
private lateinit var testableLooper: TestableLooper
private lateinit var source: TransitionProgressListener
private lateinit var handler: Handler
private lateinit var oldWtfHandler: Log.TerribleFailureHandler
private val listener: TransitionProgressListener = mock()
private val testWtfHandler: Log.TerribleFailureHandler = mock()
private lateinit var provider: PreemptiveUnfoldTransitionProgressProvider
@Before
fun before() {
testableLooper = TestableLooper.get(this)
handler = Handler(testableLooper.looper)
val testSource = createSource()
source = testSource as TransitionProgressListener
oldWtfHandler = Log.setWtfHandler(testWtfHandler)
provider = PreemptiveUnfoldTransitionProgressProvider(testSource, handler)
provider.init()
provider.addCallback(listener)
}
@After
fun after() {
Log.setWtfHandler(oldWtfHandler)
}
@Test
fun preemptiveStartInitialProgressNull_transitionStarts() {
provider.preemptivelyStartTransition(initialProgress = null)
verify(listener).onTransitionStarted()
verify(listener, never()).onTransitionProgress(anyFloat())
}
@Test
fun preemptiveStartWithInitialProgress_startsAnimationAndSendsProgress() {
provider.preemptivelyStartTransition(initialProgress = 0.5f)
verify(listener).onTransitionStarted()
verify(listener).onTransitionProgress(0.5f)
}
@Test
fun preemptiveStartAndCancel_finishesAnimation() {
provider.preemptivelyStartTransition()
provider.cancelPreemptiveStart()
with(inOrder(listener)) {
verify(listener).onTransitionStarted()
verify(listener).onTransitionFinished()
}
}
@Test
fun preemptiveStartAndThenSourceStartsTransition_transitionStarts() {
provider.preemptivelyStartTransition()
source.onTransitionStarted()
verify(listener).onTransitionStarted()
}
@Test
fun preemptiveStartAndThenSourceStartsAndFinishesTransition_transitionFinishes() {
provider.preemptivelyStartTransition()
source.onTransitionStarted()
source.onTransitionFinished()
with(inOrder(listener)) {
verify(listener).onTransitionStarted()
verify(listener).onTransitionFinished()
}
}
@Test
fun preemptiveStartAndThenSourceStartsAnimationAndSendsProgress_sendsProgress() {
provider.preemptivelyStartTransition()
source.onTransitionStarted()
source.onTransitionProgress(0.4f)
verify(listener).onTransitionProgress(0.4f)
}
@Test
fun preemptiveStartAndThenSourceSendsProgress_sendsProgress() {
provider.preemptivelyStartTransition()
source.onTransitionProgress(0.4f)
verify(listener).onTransitionProgress(0.4f)
}
@Test
fun preemptiveStartAfterTransitionRunning_transitionStarted() {
source.onTransitionStarted()
provider.preemptivelyStartTransition()
verify(listener).onTransitionStarted()
}
@Test
fun preemptiveStartAfterTransitionRunningAndThenFinished_transitionFinishes() {
source.onTransitionStarted()
provider.preemptivelyStartTransition()
source.onTransitionFinished()
with(inOrder(listener)) {
verify(listener).onTransitionStarted()
verify(listener).onTransitionFinished()
}
}
@Test
fun preemptiveStart_transitionDoesNotFinishAfterTimeout_finishesTransition() {
provider.preemptivelyStartTransition()
testableLooper.moveTimeForward(PREEMPTIVE_UNFOLD_TIMEOUT_MS + 1)
testableLooper.processAllMessages()
with(inOrder(listener)) {
verify(listener).onTransitionStarted()
verify(listener).onTransitionFinished()
}
}
@Test
fun preemptiveStart_transitionFinishAfterTimeout_logsWtf() {
provider.preemptivelyStartTransition()
testableLooper.moveTimeForward(PREEMPTIVE_UNFOLD_TIMEOUT_MS + 1)
testableLooper.processAllMessages()
verify(testWtfHandler).onTerribleFailure(any(), any(), anyBoolean())
}
@Test
fun preemptiveStart_transitionDoesNotFinishBeforeTimeout_doesNotFinishTransition() {
provider.preemptivelyStartTransition()
testableLooper.moveTimeForward(PREEMPTIVE_UNFOLD_TIMEOUT_MS - 1)
testableLooper.processAllMessages()
verify(listener).onTransitionStarted()
}
@Test
fun preemptiveStart_transitionStarted_timeoutHappened_doesNotFinishTransition() {
provider.preemptivelyStartTransition()
source.onTransitionStarted()
testableLooper.moveTimeForward(PREEMPTIVE_UNFOLD_TIMEOUT_MS + 1)
testableLooper.processAllMessages()
verify(listener).onTransitionStarted()
}
@Test
fun noPreemptiveStart_transitionStarted_startsTransition() {
source.onTransitionStarted()
verify(listener).onTransitionStarted()
}
@Test
fun noPreemptiveStart_transitionProgress_sendsProgress() {
source.onTransitionStarted()
source.onTransitionProgress(0.5f)
verify(listener).onTransitionProgress(0.5f)
}
@Test
fun noPreemptiveStart_transitionFinishes_finishesTransition() {
source.onTransitionStarted()
source.onTransitionProgress(0.5f)
source.onTransitionFinished()
with(inOrder(listener)) {
verify(listener).onTransitionStarted()
verify(listener).onTransitionFinished()
}
}
private fun createSource(): UnfoldTransitionProgressProvider =
object : TransitionProgressListener, UnfoldTransitionProgressProvider {
private val listeners = arrayListOf<TransitionProgressListener>()
override fun addCallback(listener: TransitionProgressListener) {
listeners += listener
}
override fun removeCallback(listener: TransitionProgressListener) {
listeners -= listener
}
override fun destroy() {}
override fun onTransitionStarted() =
listeners.forEach(TransitionProgressListener::onTransitionStarted)
override fun onTransitionFinishing() =
listeners.forEach(TransitionProgressListener::onTransitionFinishing)
override fun onTransitionFinished() =
listeners.forEach(TransitionProgressListener::onTransitionFinished)
override fun onTransitionProgress(progress: Float) =
listeners.forEach { it.onTransitionProgress(progress) }
}
}
@@ -314,6 +314,12 @@ public final class FeatureFlags {
"Enables receiving unfold animation events from sysui instead of calculating "
+ "them in launcher process using hinge sensor values.");
public static final BooleanFlag PREEMPTIVE_UNFOLD_ANIMATION_START = getDebugFlag(270397209,
"PREEMPTIVE_UNFOLD_ANIMATION_START", ENABLED,
"Enables starting the unfold animation preemptively when unfolding, without"
+ "waiting for SystemUI and then merging the SystemUI progress whenever we "
+ "start receiving the events");
// TODO(Block 23): Clean up flags
public static final BooleanFlag ENABLE_GRID_ONLY_OVERVIEW = getDebugFlag(270397206,
"ENABLE_GRID_ONLY_OVERVIEW", DISABLED,