Loading libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/WindowAnimator.kt +31 −20 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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( Loading @@ -100,6 +111,6 @@ object WindowAnimator { displayMetrics, ) .toInt() offset(/* dx= */ 0, offsetYPx) offset(/* dx= */ 0f, offsetYPx.toFloat()) } } libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/WindowAnimatorTest.kt +78 −27 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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, Loading @@ -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, Loading @@ -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 } } Loading
libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/WindowAnimator.kt +31 −20 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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( Loading @@ -100,6 +111,6 @@ object WindowAnimator { displayMetrics, ) .toInt() offset(/* dx= */ 0, offsetYPx) offset(/* dx= */ 0f, offsetYPx.toFloat()) } }
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/WindowAnimatorTest.kt +78 −27 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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, Loading @@ -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, Loading @@ -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 } }