Merge "Implement bubble bar flyout background animation" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
index 4939c99..49760ff 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
@@ -19,6 +19,7 @@
import android.view.Gravity
import android.view.ViewGroup
import android.widget.FrameLayout
+import androidx.core.animation.ValueAnimator
import com.android.launcher3.R
/** Creates and manages the visibility of the [BubbleBarFlyoutView]. */
@@ -33,7 +34,7 @@
fun setUpFlyout(message: BubbleBarFlyoutMessage) {
flyout?.let(container::removeView)
- val flyout = BubbleBarFlyoutView(container.context, onLeft = positioner.isOnLeft)
+ val flyout = BubbleBarFlyoutView(container.context, positioner)
flyout.translationY = positioner.targetTy
@@ -47,7 +48,11 @@
lp.marginEnd = horizontalMargin
container.addView(flyout, lp)
- flyout.setData(message)
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.addUpdateListener { _ ->
+ flyout.updateExpansionProgress(animator.animatedValue as Float)
+ }
+ flyout.showFromCollapsed(message) { animator.start() }
this.flyout = flyout
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt
index deed1f5..2b77dec 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt
@@ -16,6 +16,8 @@
package com.android.launcher3.taskbar.bubbles.flyout
+import android.graphics.PointF
+
/** Provides positioning data to the flyout view. */
interface BubbleBarFlyoutPositioner {
@@ -24,4 +26,14 @@
/** The target translation Y that the flyout view should have when displayed. */
val targetTy: Float
+
+ /**
+ * The distance between the expanded position of the flyout and the collapsed position.
+ *
+ * The distance is calculated between the bottom corner which is aligned with the bubble bar.
+ */
+ val distanceToCollapsedPosition: PointF
+
+ /** The size of the flyout when collapsed. */
+ val collapsedSize: Float
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
index 4b91f46..8884b64 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
@@ -22,6 +22,7 @@
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
+import android.graphics.PointF
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.TextView
@@ -30,9 +31,14 @@
import com.android.launcher3.popup.RoundedArrowDrawable
/** The flyout view used to notify the user of a new bubble notification. */
-class BubbleBarFlyoutView(context: Context, private val onLeft: Boolean) :
+class BubbleBarFlyoutView(context: Context, private val positioner: BubbleBarFlyoutPositioner) :
ConstraintLayout(context) {
+ private companion object {
+ // the minimum progress of the expansion animation before the triangle is made visible.
+ const val MIN_EXPANSION_PROGRESS_FOR_TRIANGLE = 0.1f
+ }
+
private val sender: TextView by
lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_name) }
@@ -82,6 +88,14 @@
private val cornerRadius: Float
private val triangle: Path = Path()
private var backgroundColor = Color.BLACK
+ /** Represents the progress of the expansion animation. 0 when collapsed. 1 when expanded. */
+ private var expansionProgress = 0f
+ /** Translation x-y values to move the flyout to its collapsed position. */
+ private var translationToCollapsedPosition = PointF(0f, 0f)
+ /** The size of the flyout when it's collapsed. */
+ private var collapsedSize = 0f
+ /** The corner radius of the flyout when it's collapsed. */
+ private var collapsedCornerRadius = 0f
/**
* The paint used to draw the background, whose color changes as the flyout transitions to the
@@ -116,7 +130,28 @@
applyConfigurationColors(resources.configuration)
}
- fun setData(flyoutMessage: BubbleBarFlyoutMessage) {
+ /** Sets the data for the flyout and starts playing the expand animation. */
+ fun showFromCollapsed(flyoutMessage: BubbleBarFlyoutMessage, expandAnimation: () -> Unit) {
+ setData(flyoutMessage)
+ val txToCollapsedPosition =
+ if (positioner.isOnLeft) {
+ positioner.distanceToCollapsedPosition.x
+ } else {
+ -positioner.distanceToCollapsedPosition.x
+ }
+ val tyToCollapsedPosition =
+ positioner.distanceToCollapsedPosition.y + triangleHeight - triangleOverlap
+ translationToCollapsedPosition = PointF(txToCollapsedPosition, tyToCollapsedPosition)
+
+ collapsedSize = positioner.collapsedSize
+ collapsedCornerRadius = collapsedSize / 2
+
+ // post the request to start the expand animation to the looper so the view can measure
+ // itself
+ post(expandAnimation)
+ }
+
+ private fun setData(flyoutMessage: BubbleBarFlyoutMessage) {
// the avatar is only displayed in group chat messages
if (flyoutMessage.senderAvatar != null && flyoutMessage.isGroupChat) {
avatar.visibility = VISIBLE
@@ -151,24 +186,56 @@
message.text = flyoutMessage.message
}
+ /** Updates the flyout view with the progress of the animation. */
+ fun updateExpansionProgress(fraction: Float) {
+ expansionProgress = fraction
+ invalidate()
+ }
+
override fun onDraw(canvas: Canvas) {
+ // interpolate the width, height, corner radius and translation based on the progress of the
+ // animation
+
+ val currentWidth = collapsedSize + (width - collapsedSize) * expansionProgress
+ val rectBottom = height - triangleHeight + triangleOverlap
+ val currentHeight = collapsedSize + (rectBottom - collapsedSize) * expansionProgress
+ val currentCornerRadius =
+ collapsedCornerRadius + (cornerRadius - collapsedCornerRadius) * expansionProgress
+ val tx = translationToCollapsedPosition.x * (1 - expansionProgress)
+ val ty = translationToCollapsedPosition.y * (1 - expansionProgress)
+
+ canvas.save()
+ canvas.translate(tx, ty)
+ // draw the background starting from the bottom left if we're positioned left, or the bottom
+ // right if we're positioned right.
canvas.drawRoundRect(
- 0f,
- 0f,
- width.toFloat(),
+ if (positioner.isOnLeft) 0f else width.toFloat() - currentWidth,
+ height.toFloat() - triangleHeight + triangleOverlap - currentHeight,
+ if (positioner.isOnLeft) currentWidth else width.toFloat(),
height.toFloat() - triangleHeight + triangleOverlap,
- cornerRadius,
- cornerRadius,
+ currentCornerRadius,
+ currentCornerRadius,
backgroundPaint,
)
- drawTriangle(canvas)
+ if (expansionProgress >= MIN_EXPANSION_PROGRESS_FOR_TRIANGLE) {
+ drawTriangle(canvas, currentCornerRadius)
+ }
+ canvas.restore()
super.onDraw(canvas)
}
- private fun drawTriangle(canvas: Canvas) {
+ private fun drawTriangle(canvas: Canvas, currentCornerRadius: Float) {
canvas.save()
- val triangleX = if (onLeft) cornerRadius else width - cornerRadius - triangleWidth
- canvas.translate(triangleX, (height - triangleHeight).toFloat())
+ val triangleX =
+ if (positioner.isOnLeft) {
+ currentCornerRadius
+ } else {
+ width - currentCornerRadius - triangleWidth
+ }
+ // instead of scaling the triangle, increasingly reveal it from the background, starting
+ // with half the size. this has the effect of the triangle scaling.
+ val triangleY = height - triangleHeight - 0.5f * triangleHeight * (1 - expansionProgress)
+ canvas.translate(triangleX, triangleY)
canvas.drawPath(triangle, backgroundPaint)
canvas.restore()
}
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt
index 537a755..467a9cb 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.graphics.Color
+import android.graphics.PointF
import android.graphics.drawable.ColorDrawable
import androidx.test.core.app.ApplicationProvider
import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
@@ -59,15 +60,17 @@
fun bubbleBarFlyoutView_noAvatar_onRight() {
screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_onRight") { activity ->
activity.actionBar?.hide()
- val flyout = BubbleBarFlyoutView(context, onLeft = false)
- flyout.setData(
+ val flyout =
+ BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = false))
+ flyout.showFromCollapsed(
BubbleBarFlyoutMessage(
senderAvatar = null,
senderName = "sender",
message = "message",
isGroupChat = false,
)
- )
+ ) {}
+ flyout.updateExpansionProgress(1f)
flyout
}
}
@@ -76,15 +79,17 @@
fun bubbleBarFlyoutView_noAvatar_onLeft() {
screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_onLeft") { activity ->
activity.actionBar?.hide()
- val flyout = BubbleBarFlyoutView(context, onLeft = true)
- flyout.setData(
+ val flyout =
+ BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+ flyout.showFromCollapsed(
BubbleBarFlyoutMessage(
senderAvatar = null,
senderName = "sender",
message = "message",
isGroupChat = false,
)
- )
+ ) {}
+ flyout.updateExpansionProgress(1f)
flyout
}
}
@@ -93,15 +98,17 @@
fun bubbleBarFlyoutView_noAvatar_longMessage() {
screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_longMessage") { activity ->
activity.actionBar?.hide()
- val flyout = BubbleBarFlyoutView(context, onLeft = true)
- flyout.setData(
+ val flyout =
+ BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+ flyout.showFromCollapsed(
BubbleBarFlyoutMessage(
senderAvatar = null,
senderName = "sender",
message = "really, really, really, really, really long message. like really.",
isGroupChat = false,
)
- )
+ ) {}
+ flyout.updateExpansionProgress(1f)
flyout
}
}
@@ -110,15 +117,17 @@
fun bubbleBarFlyoutView_avatar_onRight() {
screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_onRight") { activity ->
activity.actionBar?.hide()
- val flyout = BubbleBarFlyoutView(context, onLeft = false)
- flyout.setData(
+ val flyout =
+ BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = false))
+ flyout.showFromCollapsed(
BubbleBarFlyoutMessage(
senderAvatar = ColorDrawable(Color.RED),
senderName = "sender",
message = "message",
isGroupChat = true,
)
- )
+ ) {}
+ flyout.updateExpansionProgress(1f)
flyout
}
}
@@ -127,15 +136,17 @@
fun bubbleBarFlyoutView_avatar_onLeft() {
screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_onLeft") { activity ->
activity.actionBar?.hide()
- val flyout = BubbleBarFlyoutView(context, onLeft = true)
- flyout.setData(
+ val flyout =
+ BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+ flyout.showFromCollapsed(
BubbleBarFlyoutMessage(
senderAvatar = ColorDrawable(Color.RED),
senderName = "sender",
message = "message",
isGroupChat = true,
)
- )
+ ) {}
+ flyout.updateExpansionProgress(1f)
flyout
}
}
@@ -144,16 +155,25 @@
fun bubbleBarFlyoutView_avatar_longMessage() {
screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_longMessage") { activity ->
activity.actionBar?.hide()
- val flyout = BubbleBarFlyoutView(context, onLeft = true)
- flyout.setData(
+ val flyout =
+ BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+ flyout.showFromCollapsed(
BubbleBarFlyoutMessage(
senderAvatar = ColorDrawable(Color.RED),
senderName = "sender",
message = "really, really, really, really, really long message. like really.",
isGroupChat = true,
)
- )
+ ) {}
+ flyout.updateExpansionProgress(1f)
flyout
}
}
+
+ private class FakeBubbleBarFlyoutPositioner(override val isOnLeft: Boolean) :
+ BubbleBarFlyoutPositioner {
+ override val targetTy = 0f
+ override val distanceToCollapsedPosition = PointF(0f, 0f)
+ override val collapsedSize = 30f
+ }
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
index a58ce08..fb12ac9 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
@@ -17,6 +17,7 @@
package com.android.launcher3.taskbar.bubbles.flyout
import android.content.Context
+import android.graphics.PointF
import android.view.Gravity
import android.widget.FrameLayout
import android.widget.TextView
@@ -46,11 +47,12 @@
flyoutContainer = FrameLayout(context)
val positioner =
object : BubbleBarFlyoutPositioner {
- override val isOnLeft: Boolean
+ override val isOnLeft
get() = onLeft
- override val targetTy: Float
- get() = 50f
+ override val targetTy = 50f
+ override val distanceToCollapsedPosition = PointF(100f, 200f)
+ override val collapsedSize = 30f
}
flyoutController = BubbleBarFlyoutController(flyoutContainer, positioner)
}