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

Commit 1aa2383d authored by Nick Chameyev's avatar Nick Chameyev Committed by Android (Google) Code Review
Browse files

Merge "[Unfold animation] Start Launcher animation preemptively to synchronize...

Merge "[Unfold animation] Start Launcher animation preemptively to synchronize the first frame" into udc-dev
parents 37d8c938 705c665c
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