Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6252b67a authored by Michael Mikhail's avatar Michael Mikhail Committed by Automerger Merge Worker
Browse files

Merge "[Media TTT] Polish the tablet ripple" into tm-qpr-dev am: 6115f2ec

parents db103368 6115f2ec
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -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>
+1 −0
Original line number Diff line number Diff line
@@ -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>
+49 −81
Original line number Diff line number Diff line
@@ -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?,
+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
    }
}
+9 −2
Original line number Diff line number Diff line
@@ -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
    }
}
Loading