Loading packages/SystemUI/src/com/android/systemui/flags/Flags.kt +2 −1 Original line number Diff line number Diff line Loading @@ -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 ** Loading packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +20 −11 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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(); } } Loading Loading @@ -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); } } Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt +6 −6 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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, Loading @@ -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 Loading @@ -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 Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt 0 → 100644 +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, } packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +6 −7 Original line number Diff line number Diff line Loading @@ -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 Loading
packages/SystemUI/src/com/android/systemui/flags/Flags.kt +2 −1 Original line number Diff line number Diff line Loading @@ -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 ** Loading
packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +20 −11 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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(); } } Loading Loading @@ -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); } } Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt +6 −6 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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, Loading @@ -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 Loading @@ -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 Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt 0 → 100644 +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, }
packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +6 −7 Original line number Diff line number Diff line Loading @@ -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