summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libs/WindowManager/Shell/shared/res/values/dimen.xml2
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt54
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt63
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt18
4 files changed, 99 insertions, 38 deletions
diff --git a/libs/WindowManager/Shell/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml
index 23c9caf2046c..f6a176fb3be8 100644
--- a/libs/WindowManager/Shell/shared/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml
@@ -39,6 +39,8 @@
<!-- Bubble drop target dimensions -->
<dimen name="drop_target_elevation">1dp</dimen>
+ <dimen name="drop_target_radius">28dp</dimen>
+ <dimen name="drop_target_stroke">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>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt
index 2dc183f3f707..5c2590849dd2 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt
@@ -18,29 +18,34 @@ package com.android.wm.shell.shared.bubbles
import android.content.Context
import android.graphics.Rect
-import android.view.View
+import android.graphics.RectF
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import androidx.core.animation.Animator
import androidx.core.animation.AnimatorListenerAdapter
import androidx.core.animation.ValueAnimator
+import com.android.wm.shell.shared.R
/**
* Manages animating drop targets in response to dragging bubble icons or bubble expanded views
* across different drag zones.
*/
class DropTargetManager(
- context: Context,
+ private val context: Context,
private val container: FrameLayout,
private val isLayoutRtl: Boolean,
private val dragZoneChangedListener: DragZoneChangedListener,
) {
private var state: DragState? = null
- private val dropTargetView = View(context)
+ private val dropTargetView = DropTargetView(context)
private var animator: ValueAnimator? = null
+ private var morphRect: RectF = RectF(0f, 0f, 0f, 0f)
private companion object {
- const val ANIMATION_DURATION_MS = 250L
+ const val MORPH_ANIM_DURATION = 250L
+ const val DROP_TARGET_ALPHA_IN_DURATION = 150L
+ const val DROP_TARGET_ALPHA_OUT_DURATION = 100L
}
/** Must be called when a drag gesture is starting. */
@@ -55,15 +60,10 @@ class DropTargetManager(
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)
+ dropTargetView.elevation = context.resources.getDimension(R.dimen.drop_target_elevation)
+ // Match parent and the target is drawn within the view
+ dropTargetView.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
}
/** Called when the user drags to a new location. */
@@ -92,10 +92,7 @@ class DropTargetManager(
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()
+ dropTargetView.update(RectF(dropTargetBounds))
startFadeAnimation(from = 0f, to = 1f)
}
else -> startMorphAnimation(dropTargetBounds)
@@ -104,7 +101,9 @@ class DropTargetManager(
private fun startFadeAnimation(from: Float, to: Float, onEnd: (() -> Unit)? = null) {
animator?.cancel()
- val animator = ValueAnimator.ofFloat(from, to).setDuration(ANIMATION_DURATION_MS)
+ val duration =
+ if (from < to) DROP_TARGET_ALPHA_IN_DURATION else DROP_TARGET_ALPHA_OUT_DURATION
+ val animator = ValueAnimator.ofFloat(from, to).setDuration(duration)
animator.addUpdateListener { _ -> dropTargetView.alpha = animator.animatedValue as Float }
if (onEnd != null) {
animator.doOnEnd(onEnd)
@@ -113,23 +112,20 @@ class DropTargetManager(
animator.start()
}
- private fun startMorphAnimation(bounds: Rect) {
+ private fun startMorphAnimation(endBounds: 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)
+ val startRect = dropTargetView.getRect()
+ val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(MORPH_ANIM_DURATION)
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
+
+ morphRect.left = (startRect.left + (endBounds.left - startRect.left) * fraction)
+ morphRect.top = (startRect.top + (endBounds.top - startRect.top) * fraction)
+ morphRect.right = (startRect.right + (endBounds.right - startRect.right) * fraction)
+ morphRect.bottom = (startRect.bottom + (endBounds.bottom - startRect.bottom) * fraction)
+ dropTargetView.update(morphRect)
}
this.animator = animator
animator.start()
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt
new file mode 100644
index 000000000000..2bb6cf4ec3aa
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 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.wm.shell.shared.bubbles
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.RectF
+import android.view.View
+import com.android.wm.shell.shared.R
+
+/**
+ * Shows a drop target within this view.
+ */
+class DropTargetView(context: Context) : View(context) {
+
+ private val rectPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer)
+ style = Paint.Style.FILL
+ alpha = (0.35f * 255).toInt()
+ }
+
+ private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer)
+ style = Paint.Style.STROKE
+ strokeWidth = context.resources.getDimensionPixelSize(R.dimen.drop_target_stroke).toFloat()
+ }
+
+ private val cornerRadius = context.resources.getDimensionPixelSize(
+ R.dimen.drop_target_radius).toFloat()
+
+ private val rect = RectF(0f, 0f, 0f, 0f)
+
+ override fun onDraw(canvas: Canvas) {
+ canvas.save()
+ canvas.drawRoundRect(rect, cornerRadius, cornerRadius, rectPaint)
+ canvas.drawRoundRect(rect, cornerRadius, cornerRadius, strokePaint)
+ canvas.restore()
+ }
+
+ fun update(positionRect: RectF) {
+ rect.set(positionRect)
+ invalidate()
+ }
+
+ fun getRect(): RectF {
+ return RectF(rect)
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt
index 180a6915b45f..3f7f21ef2074 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt
@@ -18,7 +18,6 @@ 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
@@ -58,8 +57,8 @@ class DropTargetManagerTest {
private val bubbleRightDragZone =
DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = Rect(200, 0, 280, 150))
- private val dropTargetView: View
- get() = container.getChildAt(0)
+ private val dropTargetView: DropTargetView
+ get() = container.getChildAt(0) as DropTargetView
@Before
fun setUp() {
@@ -238,8 +237,9 @@ class DropTargetManagerTest {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
dropTargetManager.onDragEnded()
- // advance the timer by 100ms so the animation doesn't complete
- animatorTestRule.advanceTimeBy(100)
+ // advance the timer by 50ms so the animation doesn't complete
+ // needs to be < DropTargetManager.DROP_TARGET_ALPHA_OUT_DURATION
+ animatorTestRule.advanceTimeBy(50)
}
assertThat(container.childCount).isEqualTo(1)
@@ -320,10 +320,10 @@ class DropTargetManagerTest {
}
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())
+ assertThat(dropTargetView.getRect().left).isEqualTo(rect.left)
+ assertThat(dropTargetView.getRect().top).isEqualTo(rect.top)
+ assertThat(dropTargetView.getRect().right).isEqualTo(rect.right)
+ assertThat(dropTargetView.getRect().bottom).isEqualTo(rect.bottom)
}
private class FakeDragZoneChangedListener : DropTargetManager.DragZoneChangedListener {