Merge "[Media TTT] Polish the tablet ripple" into tm-qpr-dev am: 6115f2ecc9
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20919762 Change-Id: I502bd701e678a76d52c53fcdf2fec512617be18b Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user