Loading packages/SystemUI/res/layout/media_ttt_chip_receiver.xml +9 −0 Original line number Diff line number Diff line Loading @@ -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" Loading @@ -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> packages/SystemUI/res/values/dimens.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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> Loading packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +49 −81 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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, Loading Loading @@ -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, Loading Loading @@ -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) } } Loading @@ -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?, Loading packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt 0 → 100644 +163 −0 Original line number Diff line number Diff line /* * 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 } } packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt +9 −2 Original line number Diff line number Diff line Loading @@ -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) } Loading @@ -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() { Loading @@ -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 Loading @@ -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 } } Loading
packages/SystemUI/res/layout/media_ttt_chip_receiver.xml +9 −0 Original line number Diff line number Diff line Loading @@ -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" Loading @@ -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>
packages/SystemUI/res/values/dimens.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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> Loading
packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +49 −81 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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, Loading Loading @@ -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, Loading Loading @@ -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) } } Loading @@ -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?, Loading
packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt 0 → 100644 +163 −0 Original line number Diff line number Diff line /* * 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 } }
packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt +9 −2 Original line number Diff line number Diff line Loading @@ -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) } Loading @@ -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() { Loading @@ -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 Loading @@ -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 } }