Loading libs/WindowManager/Shell/shared/res/values/dimen.xml +1 −0 Original line number Diff line number Diff line Loading @@ -38,6 +38,7 @@ <dimen name="drag_zone_v_split_from_expanded_view_height_fold_short">100dp</dimen> <!-- Bubble drop target dimensions --> <dimen name="drop_target_elevation">1dp</dimen> <dimen name="drop_target_full_screen_padding">20dp</dimen> <dimen name="drop_target_desktop_window_padding_small">100dp</dimen> <dimen name="drop_target_desktop_window_padding_large">130dp</dimen> Loading libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt +18 −6 Original line number Diff line number Diff line Loading @@ -31,29 +31,41 @@ sealed interface DragZone { /** The bounds of this drag zone. */ val bounds: Rect /** The bounds of the drop target associated with this drag zone. */ val dropTarget: Rect? fun contains(x: Int, y: Int) = bounds.contains(x, y) /** Represents the bubble drag area on the screen. */ sealed class Bubble(override val bounds: Rect) : DragZone { data class Left(override val bounds: Rect, val dropTarget: Rect) : Bubble(bounds) data class Right(override val bounds: Rect, val dropTarget: Rect) : Bubble(bounds) sealed class Bubble(override val bounds: Rect, override val dropTarget: Rect) : DragZone { data class Left(override val bounds: Rect, override val dropTarget: Rect) : Bubble(bounds, dropTarget) data class Right(override val bounds: Rect, override val dropTarget: Rect) : Bubble(bounds, dropTarget) } /** Represents dragging to Desktop Window. */ data class DesktopWindow(override val bounds: Rect, val dropTarget: Rect) : DragZone data class DesktopWindow(override val bounds: Rect, override val dropTarget: Rect) : DragZone /** Represents dragging to Full Screen. */ data class FullScreen(override val bounds: Rect, val dropTarget: Rect) : DragZone data class FullScreen(override val bounds: Rect, override val dropTarget: Rect) : DragZone /** Represents dragging to dismiss. */ data class Dismiss(override val bounds: Rect) : DragZone data class Dismiss(override val bounds: Rect) : DragZone { override val dropTarget: Rect? = null } /** Represents dragging to enter Split or replace a Split app. */ sealed class Split(override val bounds: Rect) : DragZone { override val dropTarget: Rect? = null data class Left(override val bounds: Rect) : Split(bounds) data class Right(override val bounds: Rect) : Split(bounds) data class Top(override val bounds: Rect) : Split(bounds) data class Bottom(override val bounds: Rect) : Split(bounds) } } libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt +97 −1 Original line number Diff line number Diff line Loading @@ -16,22 +16,54 @@ package com.android.wm.shell.shared.bubbles import android.content.Context import android.graphics.Rect import android.view.View import android.widget.FrameLayout import androidx.core.animation.Animator import androidx.core.animation.AnimatorListenerAdapter import androidx.core.animation.ValueAnimator /** * Manages animating drop targets in response to dragging bubble icons or bubble expanded views * across different drag zones. */ class DropTargetManager( context: Context, private val container: FrameLayout, private val isLayoutRtl: Boolean, private val dragZoneChangedListener: DragZoneChangedListener private val dragZoneChangedListener: DragZoneChangedListener, ) { private var state: DragState? = null private val dropTargetView = View(context) private var animator: ValueAnimator? = null private companion object { const val ANIMATION_DURATION_MS = 250L } /** Must be called when a drag gesture is starting. */ fun onDragStarted(draggedObject: DraggedObject, dragZones: List<DragZone>) { val state = DragState(dragZones, draggedObject) dragZoneChangedListener.onInitialDragZoneSet(state.initialDragZone) this.state = state animator?.cancel() setupDropTarget() } private fun setupDropTarget() { if (dropTargetView.parent != null) container.removeView(dropTargetView) container.addView(dropTargetView, 0) // TODO b/393173014: set elevation and background dropTargetView.alpha = 0f dropTargetView.scaleX = 1f dropTargetView.scaleY = 1f dropTargetView.translationX = 0f dropTargetView.translationY = 0f // the drop target is added with a width and height of 1 pixel. when it gets resized, we use // set its scale to the width and height of the bounds it should have to avoid layout passes dropTargetView.layoutParams = FrameLayout.LayoutParams(/* width= */ 1, /* height= */ 1) } /** Called when the user drags to a new location. */ Loading @@ -42,14 +74,67 @@ class DropTargetManager( state.currentDragZone = newDragZone if (oldDragZone != newDragZone) { dragZoneChangedListener.onDragZoneChanged(from = oldDragZone, to = newDragZone) updateDropTarget() } } /** Called when the drag ended. */ fun onDragEnded() { startFadeAnimation(from = dropTargetView.alpha, to = 0f) { container.removeView(dropTargetView) } state = null } private fun updateDropTarget() { val currentDragZone = state?.currentDragZone ?: return val dropTargetBounds = currentDragZone.dropTarget when { dropTargetBounds == null -> startFadeAnimation(from = dropTargetView.alpha, to = 0f) dropTargetView.alpha == 0f -> { dropTargetView.translationX = dropTargetBounds.exactCenterX() dropTargetView.translationY = dropTargetBounds.exactCenterY() dropTargetView.scaleX = dropTargetBounds.width().toFloat() dropTargetView.scaleY = dropTargetBounds.height().toFloat() startFadeAnimation(from = 0f, to = 1f) } else -> startMorphAnimation(dropTargetBounds) } } private fun startFadeAnimation(from: Float, to: Float, onEnd: (() -> Unit)? = null) { animator?.cancel() val animator = ValueAnimator.ofFloat(from, to).setDuration(ANIMATION_DURATION_MS) animator.addUpdateListener { _ -> dropTargetView.alpha = animator.animatedValue as Float } if (onEnd != null) { animator.doOnEnd(onEnd) } this.animator = animator animator.start() } private fun startMorphAnimation(bounds: Rect) { animator?.cancel() val startAlpha = dropTargetView.alpha val startTx = dropTargetView.translationX val startTy = dropTargetView.translationY val startScaleX = dropTargetView.scaleX val startScaleY = dropTargetView.scaleY val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS) animator.addUpdateListener { _ -> val fraction = animator.animatedValue as Float dropTargetView.alpha = startAlpha + (1 - startAlpha) * fraction dropTargetView.translationX = startTx + (bounds.exactCenterX() - startTx) * fraction dropTargetView.translationY = startTy + (bounds.exactCenterY() - startTy) * fraction dropTargetView.scaleX = startScaleX + (bounds.width().toFloat() - startScaleX) * fraction dropTargetView.scaleY = startScaleY + (bounds.height().toFloat() - startScaleY) * fraction } this.animator = animator animator.start() } /** Stores the current drag state. */ private inner class DragState( private val dragZones: List<DragZone>, Loading @@ -72,7 +157,18 @@ class DropTargetManager( interface DragZoneChangedListener { /** An initial drag zone was set. Called when a drag starts. */ fun onInitialDragZoneSet(dragZone: DragZone) /** Called when the object was dragged to a different drag zone. */ fun onDragZoneChanged(from: DragZone, to: DragZone) } private fun Animator.doOnEnd(onEnd: () -> Unit) { addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { onEnd() } } ) } } libs/WindowManager/Shell/tests/unittest/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,7 @@ android_test { "androidx.test.rules", "androidx.test.ext.junit", "androidx.datastore_datastore", "androidx.core_core-animation-testing", "kotlinx_coroutines_test", "androidx.dynamicanimation_dynamicanimation", "dagger2", Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt +181 −29 Original line number Diff line number Diff line Loading @@ -16,23 +16,33 @@ package com.android.wm.shell.shared.bubbles import android.content.Context import android.graphics.Rect import android.view.View import android.widget.FrameLayout import androidx.core.animation.AnimatorTestRule import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat import kotlin.test.assertFails import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import kotlin.test.assertFails /** Unit tests for [DropTargetManager]. */ @SmallTest @RunWith(AndroidJUnit4::class) class DropTargetManagerTest { @get:Rule val animatorTestRule = AnimatorTestRule() private val context = getApplicationContext<Context>() private lateinit var dropTargetManager: DropTargetManager private lateinit var dragZoneChangedListener: FakeDragZoneChangedListener private val dropTarget = Rect(0, 0, 0, 0) private lateinit var container: FrameLayout // create 3 drop zones that are horizontally next to each other // ------------------------------------------------- Loading @@ -43,15 +53,20 @@ class DropTargetManagerTest { // | | | | // ------------------------------------------------- private val bubbleLeftDragZone = DragZone.Bubble.Left(bounds = Rect(0, 0, 100, 100), dropTarget = dropTarget) DragZone.Bubble.Left(bounds = Rect(0, 0, 100, 100), dropTarget = Rect(0, 0, 50, 200)) private val dismissDragZone = DragZone.Dismiss(bounds = Rect(100, 0, 200, 100)) private val bubbleRightDragZone = DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = dropTarget) DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = Rect(200, 0, 280, 150)) private val dropTargetView: View get() = container.getChildAt(0) @Before fun setUp() { container = FrameLayout(context) dragZoneChangedListener = FakeDragZoneChangedListener() dropTargetManager = DropTargetManager(isLayoutRtl = false, dragZoneChangedListener) dropTargetManager = DropTargetManager(context, container, isLayoutRtl = false, dragZoneChangedListener) } @Test Loading Loading @@ -79,17 +94,21 @@ class DropTargetManagerTest { DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) ) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( bubbleRightDragZone.bounds.centerX(), bubbleRightDragZone.bounds.centerY() ) } assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone) assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( dismissDragZone.bounds.centerX(), dismissDragZone.bounds.centerY() ) } assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone) assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone) } Loading @@ -100,10 +119,12 @@ class DropTargetManagerTest { DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) ) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( bubbleLeftDragZone.bounds.centerX(), bubbleLeftDragZone.bounds.centerY() ) } assertThat(dragZoneChangedListener.fromDragZone).isNull() assertThat(dragZoneChangedListener.toDragZone).isNull() } Loading @@ -118,7 +139,9 @@ class DropTargetManagerTest { val pointY = 200 assertThat(bubbleLeftDragZone.contains(pointX, pointY)).isFalse() assertThat(bubbleRightDragZone.contains(pointX, pointY)).isFalse() InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated(pointX, pointY) } assertThat(dragZoneChangedListener.fromDragZone).isNull() assertThat(dragZoneChangedListener.toDragZone).isNull() } Loading @@ -135,27 +158,30 @@ class DropTargetManagerTest { // drag to a point that is within both the bubble right zone and split zone val (pointX, pointY) = Pair( bubbleRightDragZone.bounds.centerX(), bubbleRightDragZone.bounds.centerY() ) Pair(bubbleRightDragZone.bounds.centerX(), bubbleRightDragZone.bounds.centerY()) assertThat(splitDragZone.contains(pointX, pointY)).isTrue() InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated(pointX, pointY) } // verify we dragged to the bubble right zone because that has higher priority than split assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone) assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( bubbleRightDragZone.bounds.centerX(), 150 // below the bubble and dismiss drag zones but within split ) } assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone) assertThat(dragZoneChangedListener.toDragZone).isEqualTo(splitDragZone) val (dismissPointX, dismissPointY) = Pair(dismissDragZone.bounds.centerX(), dismissDragZone.bounds.centerY()) assertThat(splitDragZone.contains(dismissPointX, dismissPointY)).isTrue() InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated(dismissPointX, dismissPointY) } assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(splitDragZone) assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone) } Loading @@ -166,7 +192,9 @@ class DropTargetManagerTest { DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) ) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragEnded() } dropTargetManager.onDragUpdated( bubbleRightDragZone.bounds.centerX(), bubbleRightDragZone.bounds.centerY() Loading @@ -175,6 +203,129 @@ class DropTargetManagerTest { assertThat(dragZoneChangedListener.toDragZone).isNull() } @Test fun onDragStarted_dropTargetAddedToContainer() { dropTargetManager.onDragStarted( DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(bubbleLeftDragZone, bubbleRightDragZone) ) assertThat(container.childCount).isEqualTo(1) assertThat(dropTargetView.alpha).isEqualTo(0) } @Test fun onDragEnded_dropTargetRemovedFromContainer() { dropTargetManager.onDragStarted( DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(bubbleLeftDragZone, bubbleRightDragZone) ) assertThat(container.childCount).isEqualTo(1) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragEnded() animatorTestRule.advanceTimeBy(250) } assertThat(container.childCount).isEqualTo(0) } @Test fun startNewDrag_beforeDropTargetRemoved() { dropTargetManager.onDragStarted( DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(bubbleLeftDragZone, bubbleRightDragZone) ) assertThat(container.childCount).isEqualTo(1) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragEnded() // advance the timer by 100ms so the animation doesn't complete animatorTestRule.advanceTimeBy(100) } assertThat(container.childCount).isEqualTo(1) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragStarted( DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(bubbleLeftDragZone, bubbleRightDragZone) ) } assertThat(container.childCount).isEqualTo(1) } @Test fun updateDragZone_withDropTarget_dropTargetUpdated() { dropTargetManager.onDragStarted( DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(dismissDragZone, bubbleLeftDragZone, bubbleRightDragZone) ) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( bubbleRightDragZone.bounds.centerX(), bubbleRightDragZone.bounds.centerY() ) animatorTestRule.advanceTimeBy(250) } assertThat(dropTargetView.alpha).isEqualTo(1) verifyDropTargetPosition(bubbleRightDragZone.dropTarget) } @Test fun updateDragZone_withoutDropTarget_dropTargetHidden() { dropTargetManager.onDragStarted( DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(dismissDragZone, bubbleLeftDragZone, bubbleRightDragZone) ) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( dismissDragZone.bounds.centerX(), dismissDragZone.bounds.centerY() ) animatorTestRule.advanceTimeBy(250) } assertThat(dropTargetView.alpha).isEqualTo(0) } @Test fun updateDragZone_betweenZonesWithDropTarget_dropTargetUpdated() { dropTargetManager.onDragStarted( DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(dismissDragZone, bubbleLeftDragZone, bubbleRightDragZone) ) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( bubbleRightDragZone.bounds.centerX(), bubbleRightDragZone.bounds.centerY() ) animatorTestRule.advanceTimeBy(250) } assertThat(dropTargetView.alpha).isEqualTo(1) verifyDropTargetPosition(bubbleRightDragZone.dropTarget) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( bubbleLeftDragZone.bounds.centerX(), bubbleLeftDragZone.bounds.centerY() ) animatorTestRule.advanceTimeBy(250) } assertThat(dropTargetView.alpha).isEqualTo(1) verifyDropTargetPosition(bubbleLeftDragZone.dropTarget) } private fun verifyDropTargetPosition(rect: Rect) { assertThat(dropTargetView.scaleX).isEqualTo(rect.width()) assertThat(dropTargetView.scaleY).isEqualTo(rect.height()) assertThat(dropTargetView.translationX).isEqualTo(rect.exactCenterX()) assertThat(dropTargetView.translationY).isEqualTo(rect.exactCenterY()) } private class FakeDragZoneChangedListener : DropTargetManager.DragZoneChangedListener { var initialDragZone: DragZone? = null var fromDragZone: DragZone? = null Loading @@ -183,6 +334,7 @@ class DropTargetManagerTest { override fun onInitialDragZoneSet(dragZone: DragZone) { initialDragZone = dragZone } override fun onDragZoneChanged(from: DragZone, to: DragZone) { fromDragZone = from toDragZone = to Loading Loading
libs/WindowManager/Shell/shared/res/values/dimen.xml +1 −0 Original line number Diff line number Diff line Loading @@ -38,6 +38,7 @@ <dimen name="drag_zone_v_split_from_expanded_view_height_fold_short">100dp</dimen> <!-- Bubble drop target dimensions --> <dimen name="drop_target_elevation">1dp</dimen> <dimen name="drop_target_full_screen_padding">20dp</dimen> <dimen name="drop_target_desktop_window_padding_small">100dp</dimen> <dimen name="drop_target_desktop_window_padding_large">130dp</dimen> Loading
libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt +18 −6 Original line number Diff line number Diff line Loading @@ -31,29 +31,41 @@ sealed interface DragZone { /** The bounds of this drag zone. */ val bounds: Rect /** The bounds of the drop target associated with this drag zone. */ val dropTarget: Rect? fun contains(x: Int, y: Int) = bounds.contains(x, y) /** Represents the bubble drag area on the screen. */ sealed class Bubble(override val bounds: Rect) : DragZone { data class Left(override val bounds: Rect, val dropTarget: Rect) : Bubble(bounds) data class Right(override val bounds: Rect, val dropTarget: Rect) : Bubble(bounds) sealed class Bubble(override val bounds: Rect, override val dropTarget: Rect) : DragZone { data class Left(override val bounds: Rect, override val dropTarget: Rect) : Bubble(bounds, dropTarget) data class Right(override val bounds: Rect, override val dropTarget: Rect) : Bubble(bounds, dropTarget) } /** Represents dragging to Desktop Window. */ data class DesktopWindow(override val bounds: Rect, val dropTarget: Rect) : DragZone data class DesktopWindow(override val bounds: Rect, override val dropTarget: Rect) : DragZone /** Represents dragging to Full Screen. */ data class FullScreen(override val bounds: Rect, val dropTarget: Rect) : DragZone data class FullScreen(override val bounds: Rect, override val dropTarget: Rect) : DragZone /** Represents dragging to dismiss. */ data class Dismiss(override val bounds: Rect) : DragZone data class Dismiss(override val bounds: Rect) : DragZone { override val dropTarget: Rect? = null } /** Represents dragging to enter Split or replace a Split app. */ sealed class Split(override val bounds: Rect) : DragZone { override val dropTarget: Rect? = null data class Left(override val bounds: Rect) : Split(bounds) data class Right(override val bounds: Rect) : Split(bounds) data class Top(override val bounds: Rect) : Split(bounds) data class Bottom(override val bounds: Rect) : Split(bounds) } }
libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt +97 −1 Original line number Diff line number Diff line Loading @@ -16,22 +16,54 @@ package com.android.wm.shell.shared.bubbles import android.content.Context import android.graphics.Rect import android.view.View import android.widget.FrameLayout import androidx.core.animation.Animator import androidx.core.animation.AnimatorListenerAdapter import androidx.core.animation.ValueAnimator /** * Manages animating drop targets in response to dragging bubble icons or bubble expanded views * across different drag zones. */ class DropTargetManager( context: Context, private val container: FrameLayout, private val isLayoutRtl: Boolean, private val dragZoneChangedListener: DragZoneChangedListener private val dragZoneChangedListener: DragZoneChangedListener, ) { private var state: DragState? = null private val dropTargetView = View(context) private var animator: ValueAnimator? = null private companion object { const val ANIMATION_DURATION_MS = 250L } /** Must be called when a drag gesture is starting. */ fun onDragStarted(draggedObject: DraggedObject, dragZones: List<DragZone>) { val state = DragState(dragZones, draggedObject) dragZoneChangedListener.onInitialDragZoneSet(state.initialDragZone) this.state = state animator?.cancel() setupDropTarget() } private fun setupDropTarget() { if (dropTargetView.parent != null) container.removeView(dropTargetView) container.addView(dropTargetView, 0) // TODO b/393173014: set elevation and background dropTargetView.alpha = 0f dropTargetView.scaleX = 1f dropTargetView.scaleY = 1f dropTargetView.translationX = 0f dropTargetView.translationY = 0f // the drop target is added with a width and height of 1 pixel. when it gets resized, we use // set its scale to the width and height of the bounds it should have to avoid layout passes dropTargetView.layoutParams = FrameLayout.LayoutParams(/* width= */ 1, /* height= */ 1) } /** Called when the user drags to a new location. */ Loading @@ -42,14 +74,67 @@ class DropTargetManager( state.currentDragZone = newDragZone if (oldDragZone != newDragZone) { dragZoneChangedListener.onDragZoneChanged(from = oldDragZone, to = newDragZone) updateDropTarget() } } /** Called when the drag ended. */ fun onDragEnded() { startFadeAnimation(from = dropTargetView.alpha, to = 0f) { container.removeView(dropTargetView) } state = null } private fun updateDropTarget() { val currentDragZone = state?.currentDragZone ?: return val dropTargetBounds = currentDragZone.dropTarget when { dropTargetBounds == null -> startFadeAnimation(from = dropTargetView.alpha, to = 0f) dropTargetView.alpha == 0f -> { dropTargetView.translationX = dropTargetBounds.exactCenterX() dropTargetView.translationY = dropTargetBounds.exactCenterY() dropTargetView.scaleX = dropTargetBounds.width().toFloat() dropTargetView.scaleY = dropTargetBounds.height().toFloat() startFadeAnimation(from = 0f, to = 1f) } else -> startMorphAnimation(dropTargetBounds) } } private fun startFadeAnimation(from: Float, to: Float, onEnd: (() -> Unit)? = null) { animator?.cancel() val animator = ValueAnimator.ofFloat(from, to).setDuration(ANIMATION_DURATION_MS) animator.addUpdateListener { _ -> dropTargetView.alpha = animator.animatedValue as Float } if (onEnd != null) { animator.doOnEnd(onEnd) } this.animator = animator animator.start() } private fun startMorphAnimation(bounds: Rect) { animator?.cancel() val startAlpha = dropTargetView.alpha val startTx = dropTargetView.translationX val startTy = dropTargetView.translationY val startScaleX = dropTargetView.scaleX val startScaleY = dropTargetView.scaleY val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS) animator.addUpdateListener { _ -> val fraction = animator.animatedValue as Float dropTargetView.alpha = startAlpha + (1 - startAlpha) * fraction dropTargetView.translationX = startTx + (bounds.exactCenterX() - startTx) * fraction dropTargetView.translationY = startTy + (bounds.exactCenterY() - startTy) * fraction dropTargetView.scaleX = startScaleX + (bounds.width().toFloat() - startScaleX) * fraction dropTargetView.scaleY = startScaleY + (bounds.height().toFloat() - startScaleY) * fraction } this.animator = animator animator.start() } /** Stores the current drag state. */ private inner class DragState( private val dragZones: List<DragZone>, Loading @@ -72,7 +157,18 @@ class DropTargetManager( interface DragZoneChangedListener { /** An initial drag zone was set. Called when a drag starts. */ fun onInitialDragZoneSet(dragZone: DragZone) /** Called when the object was dragged to a different drag zone. */ fun onDragZoneChanged(from: DragZone, to: DragZone) } private fun Animator.doOnEnd(onEnd: () -> Unit) { addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { onEnd() } } ) } }
libs/WindowManager/Shell/tests/unittest/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,7 @@ android_test { "androidx.test.rules", "androidx.test.ext.junit", "androidx.datastore_datastore", "androidx.core_core-animation-testing", "kotlinx_coroutines_test", "androidx.dynamicanimation_dynamicanimation", "dagger2", Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt +181 −29 Original line number Diff line number Diff line Loading @@ -16,23 +16,33 @@ package com.android.wm.shell.shared.bubbles import android.content.Context import android.graphics.Rect import android.view.View import android.widget.FrameLayout import androidx.core.animation.AnimatorTestRule import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat import kotlin.test.assertFails import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import kotlin.test.assertFails /** Unit tests for [DropTargetManager]. */ @SmallTest @RunWith(AndroidJUnit4::class) class DropTargetManagerTest { @get:Rule val animatorTestRule = AnimatorTestRule() private val context = getApplicationContext<Context>() private lateinit var dropTargetManager: DropTargetManager private lateinit var dragZoneChangedListener: FakeDragZoneChangedListener private val dropTarget = Rect(0, 0, 0, 0) private lateinit var container: FrameLayout // create 3 drop zones that are horizontally next to each other // ------------------------------------------------- Loading @@ -43,15 +53,20 @@ class DropTargetManagerTest { // | | | | // ------------------------------------------------- private val bubbleLeftDragZone = DragZone.Bubble.Left(bounds = Rect(0, 0, 100, 100), dropTarget = dropTarget) DragZone.Bubble.Left(bounds = Rect(0, 0, 100, 100), dropTarget = Rect(0, 0, 50, 200)) private val dismissDragZone = DragZone.Dismiss(bounds = Rect(100, 0, 200, 100)) private val bubbleRightDragZone = DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = dropTarget) DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = Rect(200, 0, 280, 150)) private val dropTargetView: View get() = container.getChildAt(0) @Before fun setUp() { container = FrameLayout(context) dragZoneChangedListener = FakeDragZoneChangedListener() dropTargetManager = DropTargetManager(isLayoutRtl = false, dragZoneChangedListener) dropTargetManager = DropTargetManager(context, container, isLayoutRtl = false, dragZoneChangedListener) } @Test Loading Loading @@ -79,17 +94,21 @@ class DropTargetManagerTest { DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) ) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( bubbleRightDragZone.bounds.centerX(), bubbleRightDragZone.bounds.centerY() ) } assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone) assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( dismissDragZone.bounds.centerX(), dismissDragZone.bounds.centerY() ) } assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone) assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone) } Loading @@ -100,10 +119,12 @@ class DropTargetManagerTest { DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) ) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( bubbleLeftDragZone.bounds.centerX(), bubbleLeftDragZone.bounds.centerY() ) } assertThat(dragZoneChangedListener.fromDragZone).isNull() assertThat(dragZoneChangedListener.toDragZone).isNull() } Loading @@ -118,7 +139,9 @@ class DropTargetManagerTest { val pointY = 200 assertThat(bubbleLeftDragZone.contains(pointX, pointY)).isFalse() assertThat(bubbleRightDragZone.contains(pointX, pointY)).isFalse() InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated(pointX, pointY) } assertThat(dragZoneChangedListener.fromDragZone).isNull() assertThat(dragZoneChangedListener.toDragZone).isNull() } Loading @@ -135,27 +158,30 @@ class DropTargetManagerTest { // drag to a point that is within both the bubble right zone and split zone val (pointX, pointY) = Pair( bubbleRightDragZone.bounds.centerX(), bubbleRightDragZone.bounds.centerY() ) Pair(bubbleRightDragZone.bounds.centerX(), bubbleRightDragZone.bounds.centerY()) assertThat(splitDragZone.contains(pointX, pointY)).isTrue() InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated(pointX, pointY) } // verify we dragged to the bubble right zone because that has higher priority than split assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone) assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( bubbleRightDragZone.bounds.centerX(), 150 // below the bubble and dismiss drag zones but within split ) } assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone) assertThat(dragZoneChangedListener.toDragZone).isEqualTo(splitDragZone) val (dismissPointX, dismissPointY) = Pair(dismissDragZone.bounds.centerX(), dismissDragZone.bounds.centerY()) assertThat(splitDragZone.contains(dismissPointX, dismissPointY)).isTrue() InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated(dismissPointX, dismissPointY) } assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(splitDragZone) assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone) } Loading @@ -166,7 +192,9 @@ class DropTargetManagerTest { DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) ) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragEnded() } dropTargetManager.onDragUpdated( bubbleRightDragZone.bounds.centerX(), bubbleRightDragZone.bounds.centerY() Loading @@ -175,6 +203,129 @@ class DropTargetManagerTest { assertThat(dragZoneChangedListener.toDragZone).isNull() } @Test fun onDragStarted_dropTargetAddedToContainer() { dropTargetManager.onDragStarted( DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(bubbleLeftDragZone, bubbleRightDragZone) ) assertThat(container.childCount).isEqualTo(1) assertThat(dropTargetView.alpha).isEqualTo(0) } @Test fun onDragEnded_dropTargetRemovedFromContainer() { dropTargetManager.onDragStarted( DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(bubbleLeftDragZone, bubbleRightDragZone) ) assertThat(container.childCount).isEqualTo(1) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragEnded() animatorTestRule.advanceTimeBy(250) } assertThat(container.childCount).isEqualTo(0) } @Test fun startNewDrag_beforeDropTargetRemoved() { dropTargetManager.onDragStarted( DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(bubbleLeftDragZone, bubbleRightDragZone) ) assertThat(container.childCount).isEqualTo(1) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragEnded() // advance the timer by 100ms so the animation doesn't complete animatorTestRule.advanceTimeBy(100) } assertThat(container.childCount).isEqualTo(1) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragStarted( DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(bubbleLeftDragZone, bubbleRightDragZone) ) } assertThat(container.childCount).isEqualTo(1) } @Test fun updateDragZone_withDropTarget_dropTargetUpdated() { dropTargetManager.onDragStarted( DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(dismissDragZone, bubbleLeftDragZone, bubbleRightDragZone) ) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( bubbleRightDragZone.bounds.centerX(), bubbleRightDragZone.bounds.centerY() ) animatorTestRule.advanceTimeBy(250) } assertThat(dropTargetView.alpha).isEqualTo(1) verifyDropTargetPosition(bubbleRightDragZone.dropTarget) } @Test fun updateDragZone_withoutDropTarget_dropTargetHidden() { dropTargetManager.onDragStarted( DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(dismissDragZone, bubbleLeftDragZone, bubbleRightDragZone) ) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( dismissDragZone.bounds.centerX(), dismissDragZone.bounds.centerY() ) animatorTestRule.advanceTimeBy(250) } assertThat(dropTargetView.alpha).isEqualTo(0) } @Test fun updateDragZone_betweenZonesWithDropTarget_dropTargetUpdated() { dropTargetManager.onDragStarted( DraggedObject.Bubble(BubbleBarLocation.LEFT), listOf(dismissDragZone, bubbleLeftDragZone, bubbleRightDragZone) ) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( bubbleRightDragZone.bounds.centerX(), bubbleRightDragZone.bounds.centerY() ) animatorTestRule.advanceTimeBy(250) } assertThat(dropTargetView.alpha).isEqualTo(1) verifyDropTargetPosition(bubbleRightDragZone.dropTarget) InstrumentationRegistry.getInstrumentation().runOnMainSync { dropTargetManager.onDragUpdated( bubbleLeftDragZone.bounds.centerX(), bubbleLeftDragZone.bounds.centerY() ) animatorTestRule.advanceTimeBy(250) } assertThat(dropTargetView.alpha).isEqualTo(1) verifyDropTargetPosition(bubbleLeftDragZone.dropTarget) } private fun verifyDropTargetPosition(rect: Rect) { assertThat(dropTargetView.scaleX).isEqualTo(rect.width()) assertThat(dropTargetView.scaleY).isEqualTo(rect.height()) assertThat(dropTargetView.translationX).isEqualTo(rect.exactCenterX()) assertThat(dropTargetView.translationY).isEqualTo(rect.exactCenterY()) } private class FakeDragZoneChangedListener : DropTargetManager.DragZoneChangedListener { var initialDragZone: DragZone? = null var fromDragZone: DragZone? = null Loading @@ -183,6 +334,7 @@ class DropTargetManagerTest { override fun onInitialDragZoneSet(dragZone: DragZone) { initialDragZone = dragZone } override fun onDragZoneChanged(from: DragZone, to: DragZone) { fromDragZone = from toDragZone = to Loading