Merge "[Media TTT] Polish the tablet ripple" into tm-qpr-dev

This commit is contained in:
Michael Mikhail
2023-01-20 20:51:52 +00:00
committed by Android (Google) Code Review
7 changed files with 238 additions and 84 deletions

View File

@@ -27,6 +27,14 @@
android:layout_height="wrap_content"
/>
<com.android.systemui.media.taptotransfer.receiver.ReceiverChipRippleView
android:id="@+id/icon_glow_ripple"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<!-- Add a bottom margin to avoid the glow of the icon ripple from being cropped by screen
bounds while animating with the icon -->
<com.android.internal.widget.CachingIconView
android:id="@+id/app_icon"
android:background="@drawable/media_ttt_chip_background_receiver"
@@ -34,6 +42,7 @@
android:layout_height="@dimen/media_ttt_icon_size_receiver"
android:layout_gravity="center|bottom"
android:alpha="0.0"
android:layout_marginBottom="@dimen/media_ttt_receiver_icon_bottom_margin"
/>
</FrameLayout>

View File

@@ -1087,6 +1087,7 @@
(112 - 40) / 2 = 36dp -->
<dimen name="media_ttt_generic_icon_padding">36dp</dimen>
<dimen name="media_ttt_receiver_vert_translation">40dp</dimen>
<dimen name="media_ttt_receiver_icon_bottom_margin">10dp</dimen>
<!-- Window magnification -->
<dimen name="magnification_border_drag_size">35dp</dimen>

View File

@@ -31,7 +31,6 @@ import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import com.android.internal.widget.CachingIconView
import com.android.settingslib.Utils
import com.android.systemui.R
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.ui.binder.TintedIconViewBinder
@@ -78,6 +77,7 @@ open class MediaTttChipControllerReceiver @Inject constructor(
private val viewUtil: ViewUtil,
wakeLockBuilder: WakeLock.Builder,
systemClock: SystemClock,
private val rippleController: MediaTttReceiverRippleController,
) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger<ChipReceiverInfo>>(
context,
logger,
@@ -114,9 +114,6 @@ open class MediaTttChipControllerReceiver @Inject constructor(
}
}
private var maxRippleWidth: Float = 0f
private var maxRippleHeight: Float = 0f
private fun updateMediaTapToTransferReceiverDisplay(
@StatusBarManager.MediaTransferReceiverState displayState: Int,
routeInfo: MediaRoute2Info,
@@ -206,36 +203,40 @@ open class MediaTttChipControllerReceiver @Inject constructor(
override fun animateViewIn(view: ViewGroup) {
val appIconView = view.getAppIconView()
appIconView.animate()
.translationYBy(-1 * getTranslationAmount().toFloat())
.setDuration(ICON_TRANSLATION_ANIM_DURATION)
.start()
appIconView.animate()
.alpha(1f)
.setDuration(ICON_ALPHA_ANIM_DURATION)
.start()
val iconRippleView: ReceiverChipRippleView = view.requireViewById(R.id.icon_glow_ripple)
val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
animateViewTranslationAndFade(appIconView, -1 * getTranslationAmount(), 1f)
animateViewTranslationAndFade(iconRippleView, -1 * getTranslationAmount(), 1f)
// Using withEndAction{} doesn't apply a11y focus when screen is unlocked.
appIconView.postOnAnimation { view.requestAccessibilityFocus() }
expandRipple(view.requireViewById(R.id.ripple))
rippleController.expandToInProgressState(rippleView, iconRippleView)
}
override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
val appIconView = view.getAppIconView()
appIconView.animate()
.translationYBy(getTranslationAmount().toFloat())
.setDuration(ICON_TRANSLATION_ANIM_DURATION)
.start()
appIconView.animate()
.alpha(0f)
.setDuration(ICON_ALPHA_ANIM_DURATION)
.start()
val iconRippleView: ReceiverChipRippleView = view.requireViewById(R.id.icon_glow_ripple)
val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
if (removalReason == ChipStateReceiver.TRANSFER_TO_RECEIVER_SUCCEEDED.name &&
mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()) {
expandRippleToFull(rippleView, onAnimationEnd)
rippleController.expandToSuccessState(rippleView, onAnimationEnd)
animateViewTranslationAndFade(
iconRippleView,
-1 * getTranslationAmount(),
0f,
translationDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
alphaDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
)
animateViewTranslationAndFade(
appIconView,
-1 * getTranslationAmount(),
0f,
translationDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
alphaDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
)
} else {
rippleView.collapseRipple(onAnimationEnd)
rippleController.collapseRipple(rippleView, onAnimationEnd)
animateViewTranslationAndFade(iconRippleView, getTranslationAmount(), 0f)
animateViewTranslationAndFade(appIconView, getTranslationAmount(), 0f)
}
}
@@ -245,74 +246,41 @@ open class MediaTttChipControllerReceiver @Inject constructor(
viewUtil.setRectToViewWindowLocation(view.getAppIconView(), outRect)
}
/** Animation of view translation and fading. */
private fun animateViewTranslationAndFade(
view: View,
translationYBy: Float,
alphaEndValue: Float,
translationDuration: Long = ICON_TRANSLATION_ANIM_DURATION,
alphaDuration: Long = ICON_ALPHA_ANIM_DURATION,
) {
view.animate()
.translationYBy(translationYBy)
.setDuration(translationDuration)
.start()
view.animate()
.alpha(alphaEndValue)
.setDuration(alphaDuration)
.start()
}
/** Returns the amount that the chip will be translated by in its intro animation. */
private fun getTranslationAmount(): Int {
return context.resources.getDimensionPixelSize(R.dimen.media_ttt_receiver_vert_translation)
}
private fun expandRipple(rippleView: ReceiverChipRippleView) {
if (rippleView.rippleInProgress()) {
// Skip if ripple is still playing
return
}
// In case the device orientation changes, we need to reset the layout.
rippleView.addOnLayoutChangeListener (
View.OnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
if (v == null) return@OnLayoutChangeListener
val layoutChangedRippleView = v as ReceiverChipRippleView
layoutRipple(layoutChangedRippleView)
layoutChangedRippleView.invalidate()
}
)
rippleView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(view: View?) {}
override fun onViewAttachedToWindow(view: View?) {
if (view == null) {
return
}
val attachedRippleView = view as ReceiverChipRippleView
layoutRipple(attachedRippleView)
attachedRippleView.expandRipple()
attachedRippleView.removeOnAttachStateChangeListener(this)
}
})
}
private fun layoutRipple(rippleView: ReceiverChipRippleView, isFullScreen: Boolean = false) {
val windowBounds = windowManager.currentWindowMetrics.bounds
val height = windowBounds.height().toFloat()
val width = windowBounds.width().toFloat()
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)
rippleView.setColor(color, 70)
private fun getTranslationAmount(): Float {
return rippleController.getRippleSize() * 0.5f -
rippleController.getReceiverIconSize()
}
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)
companion object {
private const val ICON_TRANSLATION_ANIM_DURATION = 500L
private const val ICON_TRANSLATION_SUCCEEDED_DURATION = 167L
private val ICON_ALPHA_ANIM_DURATION = 5.frames
}
}
val ICON_TRANSLATION_ANIM_DURATION = 30.frames
val ICON_ALPHA_ANIM_DURATION = 5.frames
data class ChipReceiverInfo(
val routeInfo: MediaRoute2Info,
val appIconDrawableOverride: Drawable?,

View File

@@ -0,0 +1,163 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.taptotransfer.receiver
import android.content.Context
import android.content.res.ColorStateList
import android.view.View
import android.view.WindowManager
import com.android.settingslib.Utils
import com.android.systemui.R
import javax.inject.Inject
/**
* A controller responsible for the animation of the ripples shown in media tap-to-transfer on the
* receiving device.
*/
class MediaTttReceiverRippleController
@Inject
constructor(
private val context: Context,
private val windowManager: WindowManager,
) {
private var maxRippleWidth: Float = 0f
private var maxRippleHeight: Float = 0f
/** Expands the icon and main ripple to in-progress state */
fun expandToInProgressState(
mainRippleView: ReceiverChipRippleView,
iconRippleView: ReceiverChipRippleView,
) {
expandRipple(mainRippleView, isIconRipple = false)
expandRipple(iconRippleView, isIconRipple = true)
}
private fun expandRipple(rippleView: ReceiverChipRippleView, isIconRipple: Boolean) {
if (rippleView.rippleInProgress()) {
// Skip if ripple is still playing
return
}
// In case the device orientation changes, we need to reset the layout.
rippleView.addOnLayoutChangeListener(
View.OnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
if (v == null) return@OnLayoutChangeListener
val layoutChangedRippleView = v as ReceiverChipRippleView
if (isIconRipple) {
layoutIconRipple(layoutChangedRippleView)
} else {
layoutRipple(layoutChangedRippleView)
}
layoutChangedRippleView.invalidate()
}
)
rippleView.addOnAttachStateChangeListener(
object : View.OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(view: View?) {}
override fun onViewAttachedToWindow(view: View?) {
if (view == null) {
return
}
val attachedRippleView = view as ReceiverChipRippleView
if (isIconRipple) {
layoutIconRipple(attachedRippleView)
} else {
layoutRipple(attachedRippleView)
}
attachedRippleView.expandRipple()
attachedRippleView.removeOnAttachStateChangeListener(this)
}
}
)
}
/** Expands the ripple to cover the screen. */
fun expandToSuccessState(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable?) {
layoutRipple(rippleView, isFullScreen = true)
rippleView.expandToFull(maxRippleHeight, onAnimationEnd)
}
/** Collapses the ripple. */
fun collapseRipple(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable? = null) {
rippleView.collapseRipple(onAnimationEnd)
}
private fun layoutRipple(rippleView: ReceiverChipRippleView, isFullScreen: Boolean = false) {
val windowBounds = windowManager.currentWindowMetrics.bounds
val height = windowBounds.height().toFloat()
val width = windowBounds.width().toFloat()
if (isFullScreen) {
maxRippleHeight = height * 2f
maxRippleWidth = width * 2f
} else {
maxRippleHeight = getRippleSize()
maxRippleWidth = getRippleSize()
}
rippleView.setMaxSize(maxRippleWidth, maxRippleHeight)
// Center the ripple on the bottom of the screen in the middle.
rippleView.setCenter(width * 0.5f, height)
rippleView.setColor(getRippleColor(), RIPPLE_OPACITY)
}
private fun layoutIconRipple(iconRippleView: ReceiverChipRippleView) {
val windowBounds = windowManager.currentWindowMetrics.bounds
val height = windowBounds.height().toFloat()
val width = windowBounds.width().toFloat()
val radius = getReceiverIconSize().toFloat()
iconRippleView.setMaxSize(radius * 0.8f, radius * 0.8f)
iconRippleView.setCenter(
width * 0.5f,
height - getReceiverIconSize() * 0.5f - getReceiverIconBottomMargin()
)
iconRippleView.setColor(getRippleColor(), RIPPLE_OPACITY)
}
private fun getRippleColor(): Int {
var colorStateList =
ColorStateList.valueOf(
Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
)
return colorStateList.withLStar(TONE_PERCENT).defaultColor
}
/** Returns the size of the ripple. */
internal fun getRippleSize(): Float {
return getReceiverIconSize() * 4f
}
/** Returns the size of the icon of the receiver. */
internal fun getReceiverIconSize(): Int {
return context.resources.getDimensionPixelSize(R.dimen.media_ttt_icon_size_receiver)
}
/** Return the bottom margin of the icon of the receiver. */
internal fun getReceiverIconBottomMargin(): Int {
// Adding a margin to make sure ripple behind the icon is not cut by the screen bounds.
return context.resources.getDimensionPixelSize(
R.dimen.media_ttt_receiver_icon_bottom_margin
)
}
companion object {
const val RIPPLE_OPACITY = 70
const val TONE_PERCENT = 95f
}
}

View File

@@ -33,14 +33,14 @@ class ReceiverChipRippleView(context: Context?, attrs: AttributeSet?) : RippleVi
private var isStarted: Boolean
init {
setupShader(RippleShader.RippleShape.ELLIPSE)
setupShader(RippleShader.RippleShape.CIRCLE)
setRippleFill(true)
setSparkleStrength(0f)
duration = 3000L
isStarted = false
}
fun expandRipple(onAnimationEnd: Runnable? = null) {
duration = DEFAULT_DURATION
isStarted = true
super.startRipple(onAnimationEnd)
}
@@ -50,6 +50,7 @@ class ReceiverChipRippleView(context: Context?, attrs: AttributeSet?) : RippleVi
if (!isStarted) {
return // Ignore if ripple is not started yet.
}
duration = DEFAULT_DURATION
// Reset all listeners to animator.
animator.removeAllListeners()
animator.addListener(object : AnimatorListenerAdapter() {
@@ -74,6 +75,7 @@ class ReceiverChipRippleView(context: Context?, attrs: AttributeSet?) : RippleVi
setRippleFill(false)
val startingPercentage = calculateStartingPercentage(newHeight)
animator.duration = EXPAND_TO_FULL_DURATION
animator.addUpdateListener { updateListener ->
val now = updateListener.currentPlayTime
val progress = updateListener.animatedValue as Float
@@ -100,4 +102,9 @@ class ReceiverChipRippleView(context: Context?, attrs: AttributeSet?) : RippleVi
val remainingPercentage = (1 - ratio).toDouble().pow(1 / 3.toDouble()).toFloat()
return 1 - remainingPercentage
}
companion object {
const val DEFAULT_DURATION = 333L
const val EXPAND_TO_FULL_DURATION = 1000L
}
}

View File

@@ -48,6 +48,7 @@ class FakeMediaTttChipControllerReceiver(
viewUtil: ViewUtil,
wakeLockBuilder: WakeLock.Builder,
systemClock: SystemClock,
rippleController: MediaTttReceiverRippleController,
) :
MediaTttChipControllerReceiver(
commandQueue,
@@ -65,6 +66,7 @@ class FakeMediaTttChipControllerReceiver(
viewUtil,
wakeLockBuilder,
systemClock,
rippleController,
) {
override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
// Just bypass the animation in tests

View File

@@ -85,6 +85,8 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
private lateinit var windowManager: WindowManager
@Mock
private lateinit var commandQueue: CommandQueue
@Mock
private lateinit var rippleController: MediaTttReceiverRippleController
private lateinit var commandQueueCallback: CommandQueue.Callbacks
private lateinit var fakeAppIconDrawable: Drawable
private lateinit var uiEventLoggerFake: UiEventLoggerFake
@@ -134,6 +136,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
viewUtil,
fakeWakeLockBuilder,
fakeClock,
rippleController,
)
controllerReceiver.start()
@@ -163,6 +166,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
viewUtil,
fakeWakeLockBuilder,
fakeClock,
rippleController,
)
controllerReceiver.start()