Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f7fc19be authored by Liran Binyamin's avatar Liran Binyamin
Browse files

Implement bubble bar flyout background animation

Update the bubble bar flyout drawing behavior to allow to animate
from a collapsed position into an expanded position.

This change only handles the rounded rect and the triangle. The
text is still left unchanged.

When wired up this looks like this:
left: http://recall/-/bJtug1HhvXkkeA4MQvIaiP/gIsckRmFKj8CceafiJnPTa
right: http://recall/-/bJtug1HhvXkkeA4MQvIaiP/dpn51yXFCCkT6ViUegf351

Flag: com.android.wm.shell.enable_bubble_bar
Bug: 277815200
Test: atest BubbleBarFlyoutViewScreenshotTest
Test: atest BubbleBarFlyoutControllerTest
Change-Id: I85ae3bf908c04e5473655c9e536495f56d80f466
parent 22754e86
Loading
Loading
Loading
Loading
+7 −2
Original line number Diff line number Diff line
@@ -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
    }

+12 −0
Original line number Diff line number Diff line
@@ -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
}
+78 −11
Original line number Diff line number Diff line
@@ -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()
    }
+38 −18
Original line number Diff line number Diff line
@@ -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
    }
}
+5 −3
Original line number Diff line number Diff line
@@ -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)
    }