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

Commit 2b87c416 authored by Ivan Tkachenko's avatar Ivan Tkachenko
Browse files

Physics based animations for drag to desktop transition

Updates:
- Added physics based spring animations on end drag to desktop transition.
  - Dragged view position and size are animated with springs with
    different stiffness and dumping.
  - The initial velocity is calculated based on the user input.
- Added veil fade out animation when user commits to drag to desktop.
- Exisiting desktop views are presented with fade-in animation that
  starts at 50% progresss of the end drag to desktop transition.

Screen recording:
https://drive.google.com/file/d/15RBEAZVZagjDXbtbCZLMChAkIi15Mw_h/view

Bug: 331163734
Test: manual
Flag: com.android.window.flags.enable_desktop_windowing_transitions
Change-Id: I7a67fff2216f51caebc2e701e898aa9867d4d418
parent f2873f4b
Loading
Loading
Loading
Loading
+13 −2
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.RectEvaluator;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.WindowConfiguration;
import android.content.Context;
@@ -262,12 +263,22 @@ public class DesktopModeVisualIndicator {

    /**
     * Fade out indicator without fully releasing it. Animator fades it out while shrinking bounds.
     *
     * @param finishCallback called when animation ends or gets cancelled
     */
    private void fadeOutIndicator() {
    void fadeOutIndicator(@Nullable Runnable finishCallback) {
        final VisualIndicatorAnimator animator = VisualIndicatorAnimator
                .fadeBoundsOut(mView, mCurrentType,
                        mDisplayController.getDisplayLayout(mTaskInfo.displayId));
        animator.start();
        if (finishCallback != null) {
            animator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    finishCallback.run();
                }
            });
        }
        mCurrentType = IndicatorType.NO_INDICATOR;
    }

@@ -282,7 +293,7 @@ public class DesktopModeVisualIndicator {
        if (mCurrentType == IndicatorType.NO_INDICATOR) {
            fadeInIndicator(newType);
        } else if (newType == IndicatorType.NO_INDICATOR) {
            fadeOutIndicator();
            fadeOutIndicator(null /* finishCallback */);
        } else {
            final VisualIndicatorAnimator animator = VisualIndicatorAnimator.animateIndicatorType(
                    mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType,
+6 −4
Original line number Diff line number Diff line
@@ -163,10 +163,12 @@ class DesktopTasksController(
            }

            private fun removeVisualIndicator(tx: SurfaceControl.Transaction) {
                visualIndicator?.fadeOutIndicator {
                    visualIndicator?.releaseVisualIndicator(tx)
                    visualIndicator = null
                }
            }
        }

    /** Task id of the task currently being dragged from fullscreen/split. */
    val draggingTaskId
@@ -193,7 +195,7 @@ class DesktopTasksController(
        )
        transitions.addHandler(this)
        taskRepository.addVisibleTasksListener(taskVisibilityListener, mainExecutor)
        dragToDesktopTransitionHandler.setDragToDesktopStateListener(dragToDesktopStateListener)
        dragToDesktopTransitionHandler.dragToDesktopStateListener = dragToDesktopStateListener
        recentsTransitionHandler.addTransitionStateListener(
            object : RecentsTransitionStateListener {
                override fun onAnimationStateChanged(running: Boolean) {
@@ -213,7 +215,7 @@ class DesktopTasksController(
    fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) {
        toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
        enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
        dragToDesktopTransitionHandler.setOnTaskResizeAnimatorListener(listener)
        dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener
    }

    fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) {
+143 −17
Original line number Diff line number Diff line
@@ -28,17 +28,20 @@ import android.window.TransitionInfo
import android.window.TransitionInfo.Change
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import androidx.dynamicanimation.animation.SpringForce
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.animation.FloatProperties
import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition
import com.android.wm.shell.protolog.ShellProtoLogGroup
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.animation.PhysicsAnimator
import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP
@@ -49,6 +52,7 @@ import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
import com.android.wm.shell.windowdecor.MoveToDesktopAnimator.Companion.DRAG_FREEFORM_SCALE
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
import java.util.function.Supplier
import kotlin.math.max

/**
 * Handles the transition to enter desktop from fullscreen by dragging on the handle bar. It also
@@ -64,17 +68,15 @@ sealed class DragToDesktopTransitionHandler(
    private val context: Context,
    private val transitions: Transitions,
    private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
    private val interactionJankMonitor: InteractionJankMonitor,
    private val transactionSupplier: Supplier<SurfaceControl.Transaction>,
    protected val interactionJankMonitor: InteractionJankMonitor,
    protected val transactionSupplier: Supplier<SurfaceControl.Transaction>,
) : TransitionHandler {

    private val rectEvaluator = RectEvaluator(Rect())
    protected val rectEvaluator = RectEvaluator(Rect())
    private val launchHomeIntent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)

    private var dragToDesktopStateListener: DragToDesktopStateListener? = null
    private lateinit var splitScreenController: SplitScreenController
    private var transitionState: TransitionState? = null
    private lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener

    /** Whether a drag-to-desktop transition is in progress. */
    val inProgress: Boolean
@@ -83,20 +85,18 @@ sealed class DragToDesktopTransitionHandler(
    /** The task id of the task currently being dragged from fullscreen/split. */
    val draggingTaskId: Int
        get() = transitionState?.draggedTaskId ?: INVALID_TASK_ID
    /** Sets a listener to receive callback about events during the transition animation. */
    fun setDragToDesktopStateListener(listener: DragToDesktopStateListener) {
        dragToDesktopStateListener = listener
    }

    /** Listener to receive callback about events during the transition animation. */
    var dragToDesktopStateListener: DragToDesktopStateListener? = null

    /** Task listener for animation start, task bounds resize, and the animation finish */
    lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener

    /** Setter needed to avoid cyclic dependency. */
    fun setSplitScreenController(controller: SplitScreenController) {
        splitScreenController = controller
    }

    fun setOnTaskResizeAnimatorListener(listener: OnTaskResizeAnimationListener) {
        onTaskResizeAnimationListener = listener
    }

    /**
     * Starts a transition that performs a transient launch of Home so that Home is brought to the
     * front while still keeping the currently focused task that is being dragged resumed. This
@@ -503,6 +503,7 @@ sealed class DragToDesktopTransitionHandler(
        finishTransaction: SurfaceControl.Transaction
    ) {
        val state = requireTransitionState()
        val freeformTaskChanges = mutableListOf<Change>()
        info.changes.forEachIndexed { i, change ->
            when {
                state is TransitionState.FromSplit &&
@@ -527,12 +528,15 @@ sealed class DragToDesktopTransitionHandler(
                            ?: error("Expected dragged leash to be non-null")
                    startTransaction.setRelativeLayer(change.leash, draggedTaskLeash, -i)
                    finishTransaction.setRelativeLayer(change.leash, draggedTaskLeash, -i)
                    freeformTaskChanges.add(change)
                }
            }
        }

        state.freeformTaskChanges = freeformTaskChanges
    }

    private fun animateEndDragToDesktop(
    protected open fun animateEndDragToDesktop(
        startTransaction: SurfaceControl.Transaction,
        startTransitionFinishCb: Transitions.TransitionFinishCallback
    ) {
@@ -597,7 +601,7 @@ sealed class DragToDesktopTransitionHandler(
                    object : AnimatorListenerAdapter() {
                        override fun onAnimationEnd(animation: Animator) {
                            onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId)
                            startTransitionFinishCb.onTransitionFinished(null /* null */)
                            startTransitionFinishCb.onTransitionFinished(/* wct = */ null)
                            clearState()
                            interactionJankMonitor.end(
                                CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
@@ -728,7 +732,7 @@ sealed class DragToDesktopTransitionHandler(
        wct.restoreTransientOrder(homeWc)
    }

    private fun clearState() {
    protected fun clearState() {
        transitionState = null
    }

@@ -778,6 +782,7 @@ sealed class DragToDesktopTransitionHandler(
        abstract var cancelTransitionToken: IBinder?
        abstract var homeChange: Change?
        abstract var draggedTaskChange: Change?
        abstract var freeformTaskChanges: List<Change>
        abstract var cancelState: CancelState
        abstract var startAborted: Boolean

@@ -790,6 +795,7 @@ sealed class DragToDesktopTransitionHandler(
            override var cancelTransitionToken: IBinder? = null,
            override var homeChange: Change? = null,
            override var draggedTaskChange: Change? = null,
            override var freeformTaskChanges: List<Change> = emptyList(),
            override var cancelState: CancelState = CancelState.NO_CANCEL,
            override var startAborted: Boolean = false,
            var otherRootChanges: MutableList<Change> = mutableListOf()
@@ -804,6 +810,7 @@ sealed class DragToDesktopTransitionHandler(
            override var cancelTransitionToken: IBinder? = null,
            override var homeChange: Change? = null,
            override var draggedTaskChange: Change? = null,
            override var freeformTaskChanges: List<Change> = emptyList(),
            override var cancelState: CancelState = CancelState.NO_CANCEL,
            override var startAborted: Boolean = false,
            var splitRootChange: Change? = null,
@@ -825,7 +832,7 @@ sealed class DragToDesktopTransitionHandler(

    companion object {
        /** The duration of the animation to commit or cancel the drag-to-desktop gesture. */
        private const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L
        internal const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L
    }
}

@@ -885,6 +892,15 @@ constructor(
        transactionSupplier
    ) {

    private val positionSpringConfig =
        PhysicsAnimator.SpringConfig(
            SpringForce.STIFFNESS_LOW,
            SpringForce.DAMPING_RATIO_LOW_BOUNCY
        )

    private val sizeSpringConfig =
        PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY)

    /**
     * @return layers in order:
     * - appLayers - below everything z < 0, effectively hides the leash
@@ -911,5 +927,115 @@ constructor(
        val homeLeash = state.homeChange?.leash ?: error("Expects home leash to be non-null")
        // Hide home on finish to prevent flickering when wallpaper activity flag is enabled
        finishTransaction.hide(homeLeash)
        // Setup freeform tasks before animation
        state.freeformTaskChanges.forEach { change ->
            val startScale = DRAG_TO_DESKTOP_FREEFORM_TASK_INITIAL_SCALE
            val startX =
                change.endAbsBounds.left + change.endAbsBounds.width() * (1 - startScale) / 2
            val startY =
                change.endAbsBounds.top + change.endAbsBounds.height() * (1 - startScale) / 2
            startTransaction.setPosition(change.leash, startX, startY)
            startTransaction.setScale(change.leash, startScale, startScale)
            startTransaction.setAlpha(change.leash, 0f)
        }
    }

    override fun animateEndDragToDesktop(
        startTransaction: SurfaceControl.Transaction,
        startTransitionFinishCb: Transitions.TransitionFinishCallback
    ) {
        val state = requireTransitionState()
        val draggedTaskChange =
            state.draggedTaskChange ?: error("Expected non-null change of dragged task")
        val draggedTaskLeash = draggedTaskChange.leash
        val freeformTaskChanges = state.freeformTaskChanges
        val startBounds = draggedTaskChange.startAbsBounds
        val endBounds = draggedTaskChange.endAbsBounds
        val currentVelocity = state.dragAnimator.computeCurrentVelocity()

        // Cancel any animation that may be currently playing; we will use the relevant
        // details of that animation here.
        state.dragAnimator.cancelAnimator()
        // We still apply scale to task bounds; as we animate the bounds to their
        // end value, animate scale to 1.
        val startScale = state.dragAnimator.scale
        val startPosition = state.dragAnimator.position
        val startBoundsWithOffset =
            Rect(startBounds).apply { offset(startPosition.x.toInt(), startPosition.y.toInt()) }

        dragToDesktopStateListener?.onCommitToDesktopAnimationStart(startTransaction)
        // Accept the merge by applying the merging transaction (applied by #showResizeVeil)
        // and finish callback. Show the veil and position the task at the first frame before
        // starting the final animation.
        onTaskResizeAnimationListener.onAnimationStart(
            state.draggedTaskId,
            startTransaction,
            startBoundsWithOffset
        )

        val tx: SurfaceControl.Transaction = transactionSupplier.get()
        PhysicsAnimator.getInstance(startBoundsWithOffset)
            .spring(
                FloatProperties.RECT_X,
                endBounds.left.toFloat(),
                currentVelocity.x,
                positionSpringConfig
            )
            .spring(
                FloatProperties.RECT_Y,
                endBounds.top.toFloat(),
                currentVelocity.y,
                positionSpringConfig
            )
            .spring(FloatProperties.RECT_WIDTH, endBounds.width().toFloat(), sizeSpringConfig)
            .spring(FloatProperties.RECT_HEIGHT, endBounds.height().toFloat(), sizeSpringConfig)
            .addUpdateListener { animBounds, _ ->
                val animFraction =
                    (animBounds.width() - startBounds.width()).toFloat() /
                        (endBounds.width() - startBounds.width())
                val animScale = startScale + animFraction * (1 - startScale)
                // Freeform animation starts 50% in the animation
                val freeformAnimFraction = max(animFraction - 0.5f, 0f) * 2f
                val freeformStartScale = DRAG_TO_DESKTOP_FREEFORM_TASK_INITIAL_SCALE
                val freeformAnimScale =
                    freeformStartScale + freeformAnimFraction * (1 - freeformStartScale)
                tx.apply {
                    // Update dragged task
                    setScale(draggedTaskLeash, animScale, animScale)
                    setPosition(
                        draggedTaskLeash,
                        animBounds.left.toFloat(),
                        animBounds.top.toFloat()
                    )
                    // Update freeform tasks
                    freeformTaskChanges.forEach {
                        val startX =
                            it.endAbsBounds.left +
                                it.endAbsBounds.width() * (1 - freeformAnimScale) / 2
                        val startY =
                            it.endAbsBounds.top +
                                it.endAbsBounds.height() * (1 - freeformAnimScale) / 2
                        setPosition(it.leash, startX, startY)
                        setScale(it.leash, freeformAnimScale, freeformAnimScale)
                        setAlpha(it.leash, freeformAnimFraction)
                    }
                }
                onTaskResizeAnimationListener.onBoundsChange(state.draggedTaskId, tx, animBounds)
            }
            .withEndActions({
                onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId)
                startTransitionFinishCb.onTransitionFinished(/* wct = */ null)
                clearState()
                interactionJankMonitor.end(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE)
            })
            .start()
    }

    companion object {
        /**
         * The initial scale of the freeform tasks in the animation to commit the drag-to-desktop
         * gesture.
         */
        private const val DRAG_TO_DESKTOP_FREEFORM_TASK_INITIAL_SCALE = 0.9f
    }
}
+12 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ import android.graphics.PointF
import android.graphics.Rect
import android.view.MotionEvent
import android.view.SurfaceControl
import android.view.VelocityTracker
import com.android.wm.shell.R

/**
@@ -34,6 +35,7 @@ class MoveToDesktopAnimator @JvmOverloads constructor(
    val scale: Float
        get() = dragToDesktopAnimator.animatedValue as Float
    private val mostRecentInput = PointF()
    private val velocityTracker = VelocityTracker.obtain()
    private val dragToDesktopAnimator: ValueAnimator = ValueAnimator.ofFloat(1f,
            DRAG_FREEFORM_SCALE)
            .setDuration(ANIMATION_DURATION.toLong())
@@ -90,6 +92,7 @@ class MoveToDesktopAnimator @JvmOverloads constructor(
        if (!allowSurfaceChangesOnMove || dragToDesktopAnimator.isRunning) {
            return
        }
        velocityTracker.addMovement(ev)
        setTaskPosition(ev.rawX, ev.rawY)
        val t = transactionFactory()
        t.setPosition(taskSurface, position.x, position.y)
@@ -109,6 +112,15 @@ class MoveToDesktopAnimator @JvmOverloads constructor(
     * Cancels the animation, intended to be used when another animator will take over.
     */
    fun cancelAnimator() {
        velocityTracker.clear()
        dragToDesktopAnimator.cancel()
    }

    /**
     * Computes the current velocity per second based on the points that have been collected.
     */
    fun computeCurrentVelocity(): PointF {
        velocityTracker.computeCurrentVelocity(/* units = */ 1000)
        return PointF(velocityTracker.xVelocity, velocityTracker.yVelocity)
    }
}
 No newline at end of file
+3 −2
Original line number Diff line number Diff line
@@ -304,7 +304,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
        val task = createTask()
        val startTransition =
            startDrag(defaultHandler, task, finishTransaction = playingFinishTransaction)
        defaultHandler.setOnTaskResizeAnimatorListener(mock())
        defaultHandler.onTaskResizeAnimationListener = mock()

        defaultHandler.mergeAnimation(
            transition = mock(),
@@ -327,13 +327,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {

    @Test
    fun mergeAnimation_endTransition_springHandler_hidesHome() {
        whenever(dragAnimator.computeCurrentVelocity()).thenReturn(PointF())
        val playingFinishTransaction = mock<SurfaceControl.Transaction>()
        val mergedStartTransaction = mock<SurfaceControl.Transaction>()
        val finishCallback = mock<Transitions.TransitionFinishCallback>()
        val task = createTask()
        val startTransition =
            startDrag(springHandler, task, finishTransaction = playingFinishTransaction)
        springHandler.setOnTaskResizeAnimatorListener(mock())
        springHandler.onTaskResizeAnimationListener = mock()

        springHandler.mergeAnimation(
            transition = mock(),