diff options
| author | 2023-01-05 17:43:36 +0000 | |
|---|---|---|
| committer | 2023-01-20 17:42:16 +0000 | |
| commit | 08cf9c3955d1d86d6b84e8b9e858ef0cacf4a7f5 (patch) | |
| tree | 518e3d98b05104ea85f3619871743d0985dd7e89 | |
| parent | d7c56b2435ac41db70b73d6849114d164e148aa5 (diff) | |
[Media TTT] Polish the tablet ripple
Change the ripple shape to be circular and make the icon translate to
the top of the circle. Adds an icon ripple behind the receiver icon to
match the new specs. And also change the way icon animate out when the
transfer is succeeded.
Bug: 254265058
Test: Checked the UI changes ( demo in bug link )
Change-Id: Ic039a05089c89016ff7503b572830ef8707acced
7 files changed, 237 insertions, 83 deletions
diff --git a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml index 21d12c278453..4483db8aeb6f 100644 --- a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml +++ b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml @@ -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> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 890d96444b04..a68e7591d4fb 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1089,6 +1089,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> diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index 889147b598b5..6884370c505c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -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) } - /** 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) - } - }) + /** 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() } - 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) + /** Returns the amount that the chip will be translated by in its intro animation. */ + 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?, diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt new file mode 100644 index 000000000000..50138024e268 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt @@ -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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt index 87b2528f93d3..f8785fcf5de0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt @@ -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 + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt index 9c4e849df738..b3e621e32d08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt @@ -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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt index cefc7424324b..5e40898030cf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt @@ -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() |