Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 705c665c authored by Nick Chameyev's avatar Nick Chameyev
Browse files

[Unfold animation] Start Launcher animation preemptively to synchronize the first frame

Starts unfold animation in Launcher right after receiving
configuration change for the unfolded screen.
This makes sure that before we unblock the screen we
have the first frame of the unfold animation ready
in Launcher (transformations are applied).

Bug: 271099882
Test: atest com.android.systemui.unfold.util.PreemptiveUnfoldTransitionProgressProviderTest
Test: manual testing fold/unfold, checking perfetto traces
Test: test with flag enabled/disabled
Change-Id: Icb8f91f9264248600d4bed14811445f50aac99c7
parent 955555a3
Loading
Loading
Loading
Loading
+10 −1
Original line number Diff line number Diff line
@@ -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) {
+52 −8
Original line number Diff line number Diff line
@@ -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;

        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
+2 −0
Original line number Diff line number Diff line
@@ -48,6 +48,8 @@ public class UnfoldMoveFromCenterHotseatAnimator extends BaseUnfoldMoveFromCente
            View child = hotseatIcons.getChildAt(i);
            registerViewForAnimation(child);
        }

        super.onPrepareViewsForAnimation();
    }

    @Override
+2 −0
Original line number Diff line number Diff line
@@ -58,6 +58,8 @@ public class UnfoldMoveFromCenterWorkspaceAnimator extends BaseUnfoldMoveFromCen

        setClipChildren(workspace, false);
        setClipToPadding(workspace, true);

        super.onPrepareViewsForAnimation();
    }

    @Override
+161 −0
Original line number Diff line number Diff line
/*
 * 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
Loading