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 4939c993e0..49760ff113 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 @@ package com.android.launcher3.taskbar.bubbles.flyout 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 @@ class BubbleBarFlyoutController( 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 @@ class BubbleBarFlyoutController( 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 deed1f5900..2b77dec462 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 @@ interface BubbleBarFlyoutPositioner { /** 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 4b91f461e9..8884b646b7 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.Canvas 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.R 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 @@ class BubbleBarFlyoutView(context: Context, private val onLeft: Boolean) : 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 @@ class BubbleBarFlyoutView(context: Context, private val onLeft: Boolean) : 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 @@ class BubbleBarFlyoutView(context: Context, private val onLeft: Boolean) : 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 537a755c21..467a9cb968 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 @@ package com.android.launcher3.taskbar.bubbles.flyout 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 @@ class BubbleBarFlyoutViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { 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 @@ class BubbleBarFlyoutViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { 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 @@ class BubbleBarFlyoutViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { 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 @@ class BubbleBarFlyoutViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { 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 @@ class BubbleBarFlyoutViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { 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 @@ class BubbleBarFlyoutViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { 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 a58ce08209..fb12ac9a59 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 @@ class BubbleBarFlyoutControllerTest { 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) }