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

Commit ecf8e840 authored by omarmt's avatar omarmt
Browse files

Add support for rounded notification in groups

- Added `NotificationTargetsHelper` helps to find viewBefore/viewAfter for notifications inside of a group
- Added `Roundable` interface could be applied to Views and ViewWrappers

Interface that allows to request/retrieve top and bottom roundness (a value between 0f and 1f).

To request a roundness value, an `SourceType` must be specified.
In case more origins require different roundness, for the same property, the maximum value will always be chosen.

It also returns the current radius for all corners (`updatedRadii`).

Test: atest NotificationTargetsHelperTest
Test: Manual tests (phone and large screens)
Bug: 184682172
Change-Id: I39b96f79a00fa358b80e62dbb090f01f60b8d718
parent b0d01823
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -63,7 +63,8 @@ object Flags {
    @JvmField val NOTIFICATION_DISMISSAL_FADE = UnreleasedFlag(113, teamfood = true)
    val STABILITY_INDEX_FIX = UnreleasedFlag(114, teamfood = true)
    val SEMI_STABLE_SORT = UnreleasedFlag(115, teamfood = true)
    // next id: 116
    @JvmField val NOTIFICATION_GROUP_CORNER = UnreleasedFlag(116, true)
    // next id: 117

    // 200 - keyguard/lockscreen
    // ** Flag retired **
+20 −11
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import com.android.systemui.animation.Interpolators;
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -110,8 +111,8 @@ public class NotificationShelf extends ActivatableNotificationView implements
        setClipChildren(false);
        setClipToPadding(false);
        mShelfIcons.setIsStaticLayout(false);
        setBottomRoundness(1.0f, false /* animate */);
        setTopRoundness(1f, false /* animate */);
        requestBottomRoundness(1.0f, /* animate = */ false, SourceType.DefaultValue);
        requestTopRoundness(1f, false, SourceType.DefaultValue);

        // Setting this to first in section to get the clipping to the top roundness correct. This
        // value determines the way we are clipping to the top roundness of the overall shade
@@ -413,7 +414,7 @@ public class NotificationShelf extends ActivatableNotificationView implements
                    if (iconState != null && iconState.clampedAppearAmount == 1.0f) {
                        // only if the first icon is fully in the shelf we want to clip to it!
                        backgroundTop = (int) (child.getTranslationY() - getTranslationY());
                        firstElementRoundness = expandableRow.getCurrentTopRoundness();
                        firstElementRoundness = expandableRow.getTopRoundness();
                    }
                }

@@ -507,28 +508,36 @@ public class NotificationShelf extends ActivatableNotificationView implements
            // Round bottom corners within animation bounds
            final float changeFraction = MathUtils.saturate(
                    (viewEnd - cornerAnimationTop) / cornerAnimationDistance);
            anv.setBottomRoundness(anv.isLastInSection() ? 1f : changeFraction,
                    false /* animate */);
            anv.requestBottomRoundness(
                    anv.isLastInSection() ? 1f : changeFraction,
                    /* animate = */ false,
                    SourceType.OnScroll);

        } else if (viewEnd < cornerAnimationTop) {
            // Fast scroll skips frames and leaves corners with unfinished rounding.
            // Reset top and bottom corners outside of animation bounds.
            anv.setBottomRoundness(anv.isLastInSection() ? 1f : smallCornerRadius,
                    false /* animate */);
            anv.requestBottomRoundness(
                    anv.isLastInSection() ? 1f : smallCornerRadius,
                    /* animate = */ false,
                    SourceType.OnScroll);
        }

        if (viewStart >= cornerAnimationTop) {
            // Round top corners within animation bounds
            final float changeFraction = MathUtils.saturate(
                    (viewStart - cornerAnimationTop) / cornerAnimationDistance);
            anv.setTopRoundness(anv.isFirstInSection() ? 1f : changeFraction,
                    false /* animate */);
            anv.requestTopRoundness(
                    anv.isFirstInSection() ? 1f : changeFraction,
                    false,
                    SourceType.OnScroll);

        } else if (viewStart < cornerAnimationTop) {
            // Fast scroll skips frames and leaves corners with unfinished rounding.
            // Reset top and bottom corners outside of animation bounds.
            anv.setTopRoundness(anv.isFirstInSection() ? 1f : smallCornerRadius,
                    false /* animate */);
            anv.requestTopRoundness(
                    anv.isFirstInSection() ? 1f : smallCornerRadius,
                    false,
                    SourceType.OnScroll);
        }
    }

+6 −6
Original line number Diff line number Diff line
@@ -70,8 +70,8 @@ class NotificationLaunchAnimatorController(
        val height = max(0, notification.actualHeight - notification.clipBottomAmount)
        val location = notification.locationOnScreen

        val clipStartLocation = notificationListContainer.getTopClippingStartLocation()
        val roundedTopClipping = Math.max(clipStartLocation - location[1], 0)
        val clipStartLocation = notificationListContainer.topClippingStartLocation
        val roundedTopClipping = (clipStartLocation - location[1]).coerceAtLeast(0)
        val windowTop = location[1] + roundedTopClipping
        val topCornerRadius = if (roundedTopClipping > 0) {
            // Because the rounded Rect clipping is complex, we start the top rounding at
@@ -80,7 +80,7 @@ class NotificationLaunchAnimatorController(
            // if we'd like to have this perfect, but this is close enough.
            0f
        } else {
            notification.currentBackgroundRadiusTop
            notification.topCornerRadius
        }
        val params = LaunchAnimationParameters(
            top = windowTop,
@@ -88,7 +88,7 @@ class NotificationLaunchAnimatorController(
            left = location[0],
            right = location[0] + notification.width,
            topCornerRadius = topCornerRadius,
            bottomCornerRadius = notification.currentBackgroundRadiusBottom
            bottomCornerRadius = notification.bottomCornerRadius
        )

        params.startTranslationZ = notification.translationZ
@@ -97,8 +97,8 @@ class NotificationLaunchAnimatorController(
        params.startClipTopAmount = notification.clipTopAmount
        if (notification.isChildInGroup) {
            params.startNotificationTop += notification.notificationParent.translationY
            val parentRoundedClip = Math.max(
                clipStartLocation - notification.notificationParent.locationOnScreen[1], 0)
            val locationOnScreen = notification.notificationParent.locationOnScreen[1]
            val parentRoundedClip = (clipStartLocation - locationOnScreen).coerceAtLeast(0)
            params.parentStartRoundedTopClipping = parentRoundedClip

            val parentClip = notification.notificationParent.clipTopAmount
+284 −0
Original line number Diff line number Diff line
package com.android.systemui.statusbar.notification

import android.util.FloatProperty
import android.view.View
import androidx.annotation.FloatRange
import com.android.systemui.R
import com.android.systemui.statusbar.notification.stack.AnimationProperties
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import kotlin.math.abs

/**
 * Interface that allows to request/retrieve top and bottom roundness (a value between 0f and 1f).
 *
 * To request a roundness value, an [SourceType] must be specified. In case more origins require
 * different roundness, for the same property, the maximum value will always be chosen.
 *
 * It also returns the current radius for all corners ([updatedRadii]).
 */
interface Roundable {
    /** Properties required for a Roundable */
    val roundableState: RoundableState

    /** Current top roundness */
    @get:FloatRange(from = 0.0, to = 1.0)
    @JvmDefault
    val topRoundness: Float
        get() = roundableState.topRoundness

    /** Current bottom roundness */
    @get:FloatRange(from = 0.0, to = 1.0)
    @JvmDefault
    val bottomRoundness: Float
        get() = roundableState.bottomRoundness

    /** Max radius in pixel */
    @JvmDefault
    val maxRadius: Float
        get() = roundableState.maxRadius

    /** Current top corner in pixel, based on [topRoundness] and [maxRadius] */
    @JvmDefault
    val topCornerRadius: Float
        get() = topRoundness * maxRadius

    /** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */
    @JvmDefault
    val bottomCornerRadius: Float
        get() = bottomRoundness * maxRadius

    /** Get and update the current radii */
    @JvmDefault
    val updatedRadii: FloatArray
        get() =
            roundableState.radiiBuffer.also { radii ->
                updateRadii(
                    topCornerRadius = topCornerRadius,
                    bottomCornerRadius = bottomCornerRadius,
                    radii = radii,
                )
            }

    /**
     * Request the top roundness [value] for a specific [sourceType].
     *
     * The top roundness of a [Roundable] can be defined by different [sourceType]. In case more
     * origins require different roundness, for the same property, the maximum value will always be
     * chosen.
     *
     * @param value a value between 0f and 1f.
     * @param animate true if it should animate to that value.
     * @param sourceType the source from which the request for roundness comes.
     * @return Whether the roundness was changed.
     */
    @JvmDefault
    fun requestTopRoundness(
        @FloatRange(from = 0.0, to = 1.0) value: Float,
        animate: Boolean,
        sourceType: SourceType,
    ): Boolean {
        val roundnessMap = roundableState.topRoundnessMap
        val lastValue = roundnessMap.values.maxOrNull() ?: 0f
        if (value == 0f) {
            // we should only take the largest value, and since the smallest value is 0f, we can
            // remove this value from the list. In the worst case, the list is empty and the
            // default value is 0f.
            roundnessMap.remove(sourceType)
        } else {
            roundnessMap[sourceType] = value
        }
        val newValue = roundnessMap.values.maxOrNull() ?: 0f

        if (lastValue != newValue) {
            val wasAnimating = roundableState.isTopAnimating()

            // Fail safe:
            // when we've been animating previously and we're now getting an update in the
            // other direction, make sure to animate it too, otherwise, the localized updating
            // may make the start larger than 1.0.
            val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f

            roundableState.setTopRoundness(value = newValue, animated = shouldAnimate || animate)
            return true
        }
        return false
    }

    /**
     * Request the bottom roundness [value] for a specific [sourceType].
     *
     * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more
     * origins require different roundness, for the same property, the maximum value will always be
     * chosen.
     *
     * @param value value between 0f and 1f.
     * @param animate true if it should animate to that value.
     * @param sourceType the source from which the request for roundness comes.
     * @return Whether the roundness was changed.
     */
    @JvmDefault
    fun requestBottomRoundness(
        @FloatRange(from = 0.0, to = 1.0) value: Float,
        animate: Boolean,
        sourceType: SourceType,
    ): Boolean {
        val roundnessMap = roundableState.bottomRoundnessMap
        val lastValue = roundnessMap.values.maxOrNull() ?: 0f
        if (value == 0f) {
            // we should only take the largest value, and since the smallest value is 0f, we can
            // remove this value from the list. In the worst case, the list is empty and the
            // default value is 0f.
            roundnessMap.remove(sourceType)
        } else {
            roundnessMap[sourceType] = value
        }
        val newValue = roundnessMap.values.maxOrNull() ?: 0f

        if (lastValue != newValue) {
            val wasAnimating = roundableState.isBottomAnimating()

            // Fail safe:
            // when we've been animating previously and we're now getting an update in the
            // other direction, make sure to animate it too, otherwise, the localized updating
            // may make the start larger than 1.0.
            val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f

            roundableState.setBottomRoundness(value = newValue, animated = shouldAnimate || animate)
            return true
        }
        return false
    }

    /** Apply the roundness changes, usually means invalidate the [RoundableState.targetView]. */
    @JvmDefault
    fun applyRoundness() {
        roundableState.targetView.invalidate()
    }

    /** @return true if top or bottom roundness is not zero. */
    @JvmDefault
    fun hasRoundedCorner(): Boolean {
        return topRoundness != 0f || bottomRoundness != 0f
    }

    /**
     * Update an Array of 8 values, 4 pairs of [X,Y] radii. As expected by param radii of
     * [android.graphics.Path.addRoundRect].
     *
     * This method reuses the previous [radii] for performance reasons.
     */
    @JvmDefault
    fun updateRadii(
        topCornerRadius: Float,
        bottomCornerRadius: Float,
        radii: FloatArray,
    ) {
        if (radii.size != 8) error("Unexpected radiiBuffer size ${radii.size}")

        if (radii[0] != topCornerRadius || radii[4] != bottomCornerRadius) {
            (0..3).forEach { radii[it] = topCornerRadius }
            (4..7).forEach { radii[it] = bottomCornerRadius }
        }
    }
}

/**
 * State object for a `Roundable` class.
 * @param targetView Will handle the [AnimatableProperty]
 * @param roundable Target of the radius animation
 * @param maxRadius Max corner radius in pixels
 */
class RoundableState(
    internal val targetView: View,
    roundable: Roundable,
    internal val maxRadius: Float,
) {
    /** Animatable for top roundness */
    private val topAnimatable = topAnimatable(roundable)

    /** Animatable for bottom roundness */
    private val bottomAnimatable = bottomAnimatable(roundable)

    /** Current top roundness. Use [setTopRoundness] to update this value */
    @set:FloatRange(from = 0.0, to = 1.0)
    internal var topRoundness = 0f
        private set

    /** Current bottom roundness. Use [setBottomRoundness] to update this value */
    @set:FloatRange(from = 0.0, to = 1.0)
    internal var bottomRoundness = 0f
        private set

    /** Last requested top roundness associated by [SourceType] */
    internal val topRoundnessMap = mutableMapOf<SourceType, Float>()

    /** Last requested bottom roundness associated by [SourceType] */
    internal val bottomRoundnessMap = mutableMapOf<SourceType, Float>()

    /** Last cached radii */
    internal val radiiBuffer = FloatArray(8)

    /** Is top roundness animation in progress? */
    internal fun isTopAnimating() = PropertyAnimator.isAnimating(targetView, topAnimatable)

    /** Is bottom roundness animation in progress? */
    internal fun isBottomAnimating() = PropertyAnimator.isAnimating(targetView, bottomAnimatable)

    /** Set the current top roundness */
    internal fun setTopRoundness(
        value: Float,
        animated: Boolean = targetView.isShown,
    ) {
        PropertyAnimator.setProperty(targetView, topAnimatable, value, DURATION, animated)
    }

    /** Set the current bottom roundness */
    internal fun setBottomRoundness(
        value: Float,
        animated: Boolean = targetView.isShown,
    ) {
        PropertyAnimator.setProperty(targetView, bottomAnimatable, value, DURATION, animated)
    }

    companion object {
        private val DURATION: AnimationProperties =
            AnimationProperties()
                .setDuration(StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS.toLong())

        private fun topAnimatable(roundable: Roundable): AnimatableProperty =
            AnimatableProperty.from(
                object : FloatProperty<View>("topRoundness") {
                    override fun get(view: View): Float = roundable.topRoundness

                    override fun setValue(view: View, value: Float) {
                        roundable.roundableState.topRoundness = value
                        roundable.applyRoundness()
                    }
                },
                R.id.top_roundess_animator_tag,
                R.id.top_roundess_animator_end_tag,
                R.id.top_roundess_animator_start_tag,
            )

        private fun bottomAnimatable(roundable: Roundable): AnimatableProperty =
            AnimatableProperty.from(
                object : FloatProperty<View>("bottomRoundness") {
                    override fun get(view: View): Float = roundable.bottomRoundness

                    override fun setValue(view: View, value: Float) {
                        roundable.roundableState.bottomRoundness = value
                        roundable.applyRoundness()
                    }
                },
                R.id.bottom_roundess_animator_tag,
                R.id.bottom_roundess_animator_end_tag,
                R.id.bottom_roundess_animator_start_tag,
            )
    }
}

enum class SourceType {
    DefaultValue,
    OnDismissAnimation,
    OnScroll,
}
+6 −7
Original line number Diff line number Diff line
@@ -613,22 +613,21 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
    protected void resetAllContentAlphas() {}

    @Override
    protected void applyRoundness() {
    public void applyRoundness() {
        super.applyRoundness();
        applyBackgroundRoundness(getCurrentBackgroundRadiusTop(),
                getCurrentBackgroundRadiusBottom());
        applyBackgroundRoundness(getTopCornerRadius(), getBottomCornerRadius());
    }

    @Override
    public float getCurrentBackgroundRadiusTop() {
    public float getTopCornerRadius() {
        float fraction = getInterpolatedAppearAnimationFraction();
        return MathUtils.lerp(0, super.getCurrentBackgroundRadiusTop(), fraction);
        return MathUtils.lerp(0, super.getTopCornerRadius(), fraction);
    }

    @Override
    public float getCurrentBackgroundRadiusBottom() {
    public float getBottomCornerRadius() {
        float fraction = getInterpolatedAppearAnimationFraction();
        return MathUtils.lerp(0, super.getCurrentBackgroundRadiusBottom(), fraction);
        return MathUtils.lerp(0, super.getBottomCornerRadius(), fraction);
    }

    private void applyBackgroundRoundness(float topRadius, float bottomRadius) {
Loading