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

Commit 519ced7d authored by Gustav Sennton's avatar Gustav Sennton
Browse files

Fix desktop launch (unminimize) animation scaling.

Before this CL the scaling in window WindowAnimator was only working
correctly for closing/minimizing windows. For opening/unminimizing
windows the scale would be 1 during the entire animation.

Also
1. animate over the window position (PointF) instead of the window
   bounds (Rect) to make it clear that we're not actually using the
   bounds themselves during the animation, and
2. update WindowAnimatorTest to check the position and scale of the
   actual transaction rather than the animated rectangle values.

Test: WindowAnimatorTest
Bug: 327428659
Flag: com.android.window.flags.enable_desktop_app_launch_alttab_transitions
Change-Id: Ibf58ddb044c5e2078beb6fc26fedbb5b7df3ba25
parent 80cd1380
Loading
Loading
Loading
Loading
+31 −20
Original line number Diff line number Diff line
@@ -16,8 +16,9 @@

package com.android.wm.shell.shared.animation

import android.animation.RectEvaluator
import android.animation.PointFEvaluator
import android.animation.ValueAnimator
import android.graphics.PointF
import android.graphics.Rect
import android.util.DisplayMetrics
import android.util.TypedValue
@@ -52,46 +53,56 @@ object WindowAnimator {
        change: TransitionInfo.Change,
        transaction: SurfaceControl.Transaction,
    ): ValueAnimator {
        val startBounds =
            createBounds(
        val startPos =
            getPosition(
                displayMetrics,
                change.startAbsBounds,
                change.endAbsBounds,
                boundsAnimDef.startScale,
                boundsAnimDef.startOffsetYDp,
            )
        val leash = change.leash
        val endBounds =
            createBounds(
        val endPos =
            getPosition(
                displayMetrics,
                change.startAbsBounds,
                change.endAbsBounds,
                boundsAnimDef.endScale,
                boundsAnimDef.endOffsetYDp,
            )
        return ValueAnimator.ofObject(RectEvaluator(), startBounds, endBounds).apply {
        return ValueAnimator.ofObject(PointFEvaluator(), startPos, endPos).apply {
            duration = boundsAnimDef.durationMs
            interpolator = boundsAnimDef.interpolator
            addUpdateListener { animation ->
                val animBounds = animation.animatedValue as Rect
                val animScale = 1 - (1 - boundsAnimDef.endScale) * animation.animatedFraction
                val animPos = animation.animatedValue as PointF
                val animScale =
                    interpolate(
                        boundsAnimDef.startScale,
                        boundsAnimDef.endScale,
                        animation.animatedFraction
                    )
                transaction
                    .setPosition(leash, animBounds.left.toFloat(), animBounds.top.toFloat())
                    .setPosition(leash, animPos.x, animPos.y)
                    .setScale(leash, animScale, animScale)
                    .apply()
            }
        }
    }

    private fun createBounds(
    private fun interpolate(startVal: Float, endVal: Float, fraction: Float): Float {
        require(fraction in 0.0f..1.0f)
        return startVal + (endVal - startVal) * fraction
    }

    private fun getPosition(
        displayMetrics: DisplayMetrics,
        origBounds: Rect,
        bounds: Rect,
        scale: Float,
        offsetYDp: Float
    ) = Rect(origBounds).apply {
            check(scale in 0.0..1.0)
    ) = PointF(bounds.left.toFloat(), bounds.top.toFloat()).apply {
            check(scale in 0.0f..1.0f)
            // Scale the bounds down with an anchor in the center
            inset(
                (origBounds.width().toFloat() * (1 - scale) / 2).toInt(),
                (origBounds.height().toFloat() * (1 - scale) / 2).toInt(),
            offset(
                (bounds.width().toFloat() * (1 - scale) / 2),
                (bounds.height().toFloat() * (1 - scale) / 2),
            )
            val offsetYPx =
                TypedValue.applyDimension(
@@ -100,6 +111,6 @@ object WindowAnimator {
                        displayMetrics,
                    )
                    .toInt()
            offset(/* dx= */ 0, offsetYPx)
            offset(/* dx= */ 0f, offsetYPx.toFloat())
        }
}
+78 −27
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@

package com.android.wm.shell.shared.animation

import android.animation.ValueAnimator
import android.graphics.PointF
import android.graphics.Rect
import android.util.DisplayMetrics
import android.view.SurfaceControl
@@ -31,6 +31,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyFloat
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

@@ -44,12 +45,33 @@ class WindowAnimatorTest {

    private val displayMetrics = DisplayMetrics().apply { density = 1f }

    private val positionXArgumentCaptor = argumentCaptor<Float>()
    private val positionYArgumentCaptor = argumentCaptor<Float>()
    private val scaleXArgumentCaptor = argumentCaptor<Float>()
    private val scaleYArgumentCaptor = argumentCaptor<Float>()

    @Before
    fun setup() {
        whenever(change.leash).thenReturn(leash)
        whenever(change.startAbsBounds).thenReturn(START_BOUNDS)
        whenever(change.endAbsBounds).thenReturn(END_BOUNDS)
        whenever(transaction.setPosition(any(), anyFloat(), anyFloat())).thenReturn(transaction)
        whenever(transaction.setScale(any(), anyFloat(), anyFloat())).thenReturn(transaction)
        whenever(
            transaction.setPosition(
                any(),
                positionXArgumentCaptor.capture(),
                positionYArgumentCaptor.capture(),
            )
        )
            .thenReturn(transaction)
        whenever(
            transaction.setScale(
                any(),
                scaleXArgumentCaptor.capture(),
                scaleYArgumentCaptor.capture(),
            )
        )
            .thenReturn(transaction)
    }

    @Test
@@ -67,16 +89,18 @@ class WindowAnimatorTest {
                change,
                transaction
            )
        valueAnimator.start()

        assertThat(valueAnimator.duration).isEqualTo(100L)
        assertThat(valueAnimator.interpolator).isEqualTo(Interpolators.STANDARD_ACCELERATE)
        assertStartAndEndBounds(valueAnimator, startBounds = START_BOUNDS, endBounds = START_BOUNDS)
        val expectedPosition = PointF(END_BOUNDS.left.toFloat(), END_BOUNDS.top.toFloat())
        assertTransactionParams(expectedPosition, expectedScale = PointF(1f, 1f))
    }

    @Test
    fun createBoundsAnimator_startScaleAndOffset_returnsCorrectBounds() = runOnUiThread {
    fun createBoundsAnimator_startScaleAndOffset_correctPosAndScale() = runOnUiThread {
        val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400)
        whenever(change.startAbsBounds).thenReturn(bounds)
        whenever(change.endAbsBounds).thenReturn(bounds)
        val boundsAnimParams =
            WindowAnimator.BoundsAnimationParams(
                durationMs = 100L,
@@ -92,19 +116,18 @@ class WindowAnimatorTest {
                change,
                transaction
            )
        valueAnimator.start()

        assertStartAndEndBounds(
            valueAnimator,
            startBounds =
                Rect(/* left= */ 150, /* top= */ 260, /* right= */ 250, /* bottom= */ 360),
            endBounds = bounds,
        assertTransactionParams(
            expectedPosition = PointF(150f, 260f),
            expectedScale = PointF(0.5f, 0.5f),
        )
    }

    @Test
    fun createBoundsAnimator_endScaleAndOffset_returnsCorrectBounds() = runOnUiThread {
    fun createBoundsAnimator_endScaleAndOffset_correctPosAndScale() = runOnUiThread {
        val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400)
        whenever(change.startAbsBounds).thenReturn(bounds)
        whenever(change.endAbsBounds).thenReturn(bounds)
        val boundsAnimParams =
            WindowAnimator.BoundsAnimationParams(
                durationMs = 100L,
@@ -120,28 +143,56 @@ class WindowAnimatorTest {
                change,
                transaction
            )
        valueAnimator.start()
        valueAnimator.end()

        assertStartAndEndBounds(
            valueAnimator,
            startBounds = bounds,
            endBounds = Rect(/* left= */ 150, /* top= */ 260, /* right= */ 250, /* bottom= */ 360),
        assertTransactionParams(
            expectedPosition = PointF(150f, 260f),
            expectedScale = PointF(0.5f, 0.5f),
        )
    }

    private fun assertStartAndEndBounds(
        valueAnimator: ValueAnimator,
        startBounds: Rect,
        endBounds: Rect,
    ) {
        valueAnimator.start()
        valueAnimator.animatedValue
        assertThat(valueAnimator.animatedValue).isEqualTo(startBounds)
        valueAnimator.end()
        assertThat(valueAnimator.animatedValue).isEqualTo(endBounds)
    @Test
    fun createBoundsAnimator_middleOfAnimation_correctPosAndScale() = runOnUiThread {
        val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400)
        whenever(change.endAbsBounds).thenReturn(bounds)
        val boundsAnimParams =
            WindowAnimator.BoundsAnimationParams(
                durationMs = 100L,
                endOffsetYDp = 10f,
                startScale = 0.5f,
                endScale = 0.9f,
                interpolator = Interpolators.LINEAR,
            )

        val valueAnimator =
            WindowAnimator.createBoundsAnimator(
                displayMetrics,
                boundsAnimParams,
                change,
                transaction
            )
        valueAnimator.currentPlayTime = 50

        assertTransactionParams(
            // We should have a window of size 140x140, which we centre by placing at pos 130, 230.
            // Then add 10*0.5 as y-offset
            expectedPosition = PointF(130f, 235f),
            expectedScale = PointF(0.7f, 0.7f),
        )
    }

    private fun assertTransactionParams(expectedPosition: PointF, expectedScale: PointF) {
        assertThat(positionXArgumentCaptor.lastValue).isWithin(TOLERANCE).of(expectedPosition.x)
        assertThat(positionYArgumentCaptor.lastValue).isWithin(TOLERANCE).of(expectedPosition.y)
        assertThat(scaleXArgumentCaptor.lastValue).isWithin(TOLERANCE).of(expectedScale.x)
        assertThat(scaleYArgumentCaptor.lastValue).isWithin(TOLERANCE).of(expectedScale.y)
    }

    companion object {
        private val START_BOUNDS =
        private val END_BOUNDS =
            Rect(/* left= */ 10, /* top= */ 20, /* right= */ 30, /* bottom= */ 40)

        private const val TOLERANCE = 1e-3f
    }
}