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

Commit 46b33d3f authored by Omar Miatello's avatar Omar Miatello Committed by Android (Google) Code Review
Browse files

Merge "Add support for rounded notification in groups" into tm-qpr-dev

parents 6117424f ecf8e840
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