[Media TTT] Apply ripple effect for new states

Adds a new ripple effect when the transfer succeeded. The ripple should
glow all over the screen and vanish.

Bug: 257301489
Test: atest MediaTttChipControllerReceiverTest
Test: Checked the UI is animating as intended.
Change-Id: I66967487ce88a41316fbc83d2621e59a8aa7c168
This commit is contained in:
Michael Mikhail
2022-12-07 21:21:08 +00:00
parent e78c205dc8
commit be47aafb12
11 changed files with 97 additions and 17 deletions

View File

@@ -169,11 +169,9 @@ class RippleShader(rippleShape: RippleShape = RippleShape.CIRCLE) :
setFloatUniform("in_progress", value)
val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
setFloatUniform(
"in_size",
/* width= */ maxSize.x * curvedProg,
/* height= */ maxSize.y * curvedProg
)
currentWidth = maxSize.x * curvedProg
currentHeight = maxSize.y * curvedProg
setFloatUniform("in_size", /* width= */ currentWidth, /* height= */ currentHeight)
setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f)
// radius should not exceed width and height values.
setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * curvedProg)
@@ -237,4 +235,10 @@ class RippleShader(rippleShape: RippleShape = RippleShape.CIRCLE) :
* False for a ring effect.
*/
var rippleFill: Boolean = false
var currentWidth: Float = 0f
private set
var currentHeight: Float = 0f
private set
}

View File

@@ -36,7 +36,7 @@ import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape
*/
open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private lateinit var rippleShader: RippleShader
protected lateinit var rippleShader: RippleShader
lateinit var rippleShape: RippleShape
private set

View File

@@ -291,6 +291,10 @@ object Flags {
// TODO(b/261734857): Tracking Bug
@JvmField val UMO_TURBULENCE_NOISE = unreleasedFlag(909, "umo_turbulence_noise")
// TODO(b/263272731): Tracking Bug
val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE =
unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true)
// 1000 - dock
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")

View File

@@ -26,4 +26,8 @@ import javax.inject.Inject
class MediaTttFlags @Inject constructor(private val featureFlags: FeatureFlags) {
/** */
fun isMediaTttEnabled(): Boolean = featureFlags.isEnabled(Flags.MEDIA_TAP_TO_TRANSFER)
/** Check whether the flag for the receiver success state is enabled. */
fun isMediaTttReceiverSuccessRippleEnabled(): Boolean =
featureFlags.isEnabled(Flags.MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE)
}

View File

@@ -111,6 +111,9 @@ open class MediaTttChipControllerReceiver @Inject constructor(
}
}
private var maxRippleWidth: Float = 0f
private var maxRippleHeight: Float = 0f
private fun updateMediaTapToTransferReceiverDisplay(
@StatusBarManager.MediaTransferReceiverState displayState: Int,
routeInfo: MediaRoute2Info,
@@ -212,7 +215,7 @@ open class MediaTttChipControllerReceiver @Inject constructor(
expandRipple(view.requireViewById(R.id.ripple))
}
override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
val appIconView = view.getAppIconView()
appIconView.animate()
.translationYBy(getTranslationAmount().toFloat())
@@ -222,7 +225,14 @@ open class MediaTttChipControllerReceiver @Inject constructor(
.alpha(0f)
.setDuration(ICON_ALPHA_ANIM_DURATION)
.start()
(view.requireViewById(R.id.ripple) as ReceiverChipRippleView).collapseRipple(onAnimationEnd)
val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
if (removalReason == ChipStateReceiver.TRANSFER_TO_RECEIVER_SUCCEEDED.name &&
mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()) {
expandRippleToFull(rippleView, onAnimationEnd)
} else {
rippleView.collapseRipple(onAnimationEnd)
}
}
override fun getTouchableRegion(view: View, outRect: Rect) {
@@ -267,12 +277,19 @@ open class MediaTttChipControllerReceiver @Inject constructor(
})
}
private fun layoutRipple(rippleView: ReceiverChipRippleView) {
private fun layoutRipple(rippleView: ReceiverChipRippleView, isFullScreen: Boolean = false) {
val windowBounds = windowManager.currentWindowMetrics.bounds
val height = windowBounds.height().toFloat()
val width = windowBounds.width().toFloat()
rippleView.setMaxSize(width / 2f, height / 2f)
if (isFullScreen) {
maxRippleHeight = height * 2f
maxRippleWidth = width * 2f
} else {
maxRippleHeight = height / 2f
maxRippleWidth = width / 2f
}
rippleView.setMaxSize(maxRippleWidth, maxRippleHeight)
// Center the ripple on the bottom of the screen in the middle.
rippleView.setCenter(width * 0.5f, height)
val color = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
@@ -282,6 +299,11 @@ open class MediaTttChipControllerReceiver @Inject constructor(
private fun View.getAppIconView(): CachingIconView {
return this.requireViewById(R.id.app_icon)
}
private fun expandRippleToFull(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable?) {
layoutRipple(rippleView, true)
rippleView.expandToFull(maxRippleHeight, onAnimationEnd)
}
}
val ICON_TRANSLATION_ANIM_DURATION = 30.frames

View File

@@ -22,6 +22,7 @@ import android.content.Context
import android.util.AttributeSet
import com.android.systemui.surfaceeffects.ripple.RippleShader
import com.android.systemui.surfaceeffects.ripple.RippleView
import kotlin.math.pow
/**
* An expanding ripple effect for the media tap-to-transfer receiver chip.
@@ -59,4 +60,44 @@ class ReceiverChipRippleView(context: Context?, attrs: AttributeSet?) : RippleVi
})
animator.reverse()
}
// Expands the ripple to cover full screen.
fun expandToFull(newHeight: Float, onAnimationEnd: Runnable? = null) {
if (!isStarted) {
return
}
// Reset all listeners to animator.
animator.removeAllListeners()
animator.removeAllUpdateListeners()
// Only show the outline as ripple expands and disappears when animation ends.
setRippleFill(false)
val startingPercentage = calculateStartingPercentage(newHeight)
animator.addUpdateListener { updateListener ->
val now = updateListener.currentPlayTime
val progress = updateListener.animatedValue as Float
rippleShader.progress = startingPercentage + (progress * (1 - startingPercentage))
rippleShader.distortionStrength = 1 - rippleShader.progress
rippleShader.pixelDensity = 1 - rippleShader.progress
rippleShader.time = now.toFloat()
invalidate()
}
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
animation?.let { visibility = GONE }
onAnimationEnd?.run()
isStarted = false
}
})
animator.start()
}
// Calculates the actual starting percentage according to ripple shader progress set method.
// Check calculations in [RippleShader.progress]
fun calculateStartingPercentage(newHeight: Float): Float {
val ratio = rippleShader.currentHeight / newHeight
val remainingPercentage = (1 - ratio).toDouble().pow(1 / 3.toDouble()).toFloat()
return 1 - remainingPercentage
}
}

View File

@@ -321,7 +321,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
return
}
removeViewFromWindow(displayInfo)
removeViewFromWindow(displayInfo, removalReason)
// Prune anything that's already timed out before determining if we should re-display a
// different chipbar.
@@ -348,14 +348,14 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
removeViewFromWindow(displayInfo)
}
private fun removeViewFromWindow(displayInfo: DisplayInfo) {
private fun removeViewFromWindow(displayInfo: DisplayInfo, removalReason: String? = null) {
val view = displayInfo.view
if (view == null) {
logger.logViewRemovalIgnored(displayInfo.info.id, "View is null")
return
}
displayInfo.view = null // Need other places??
animateViewOut(view) {
animateViewOut(view, removalReason) {
windowManager.removeView(view)
displayInfo.wakeLock?.release(displayInfo.info.wakeReason)
}
@@ -405,7 +405,11 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
*
* @param onAnimationEnd an action that *must* be run once the animation finishes successfully.
*/
internal open fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
internal open fun animateViewOut(
view: ViewGroup,
removalReason: String? = null,
onAnimationEnd: Runnable
) {
onAnimationEnd.run()
}

View File

@@ -208,7 +208,7 @@ open class ChipbarCoordinator @Inject constructor(
)
}
override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
val innerView = view.getInnerView()
innerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE
ViewHierarchyAnimator.animateRemoval(

View File

@@ -63,7 +63,7 @@ class FakeMediaTttChipControllerReceiver(
wakeLockBuilder,
systemClock,
) {
override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
// Just bypass the animation in tests
onAnimationEnd.run()
}

View File

@@ -95,6 +95,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true)
whenever(mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()).thenReturn(true)
fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!!
whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable)

View File

@@ -61,7 +61,7 @@ class FakeChipbarCoordinator(
wakeLockBuilder,
systemClock,
) {
override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
// Just bypass the animation in tests
onAnimationEnd.run()
}