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

Commit 6610b194 authored by Matt Casey's avatar Matt Casey Committed by Android (Google) Code Review
Browse files

Merge "Animate changes between action icons." into 24D1-dev

parents b81a7d39 9f8c5c6b
Loading
Loading
Loading
Loading
+134 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.screenshot.ui

import android.animation.ValueAnimator
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.drawable.Drawable
import androidx.core.animation.doOnEnd
import java.util.Objects

/**  */
class TransitioningIconDrawable : Drawable() {
    // The drawable for the current icon of this view. During icon transitions, this is the one
    // being animated out.
    private var drawable: Drawable? = null

    // The incoming new icon. Only populated during transition animations (when drawable is also
    // non-null).
    private var enteringDrawable: Drawable? = null
    private var colorFilter: ColorFilter? = null
    private var tint: ColorStateList? = null
    private var alpha = 255

    private var transitionAnimator =
        ValueAnimator.ofFloat(0f, 1f).also { it.doOnEnd { onTransitionComplete() } }

    /**
     * Set the drawable to be displayed, potentially animating the transition from one icon to the
     * next.
     */
    fun setIcon(incomingDrawable: Drawable?) {
        if (Objects.equals(drawable, incomingDrawable) && !transitionAnimator.isRunning) {
            return
        }

        incomingDrawable?.colorFilter = colorFilter
        incomingDrawable?.setTintList(tint)

        if (drawable == null) {
            // No existing icon drawn, just show the new one without a transition
            drawable = incomingDrawable
            invalidateSelf()
            return
        }

        if (enteringDrawable != null) {
            // There's already an entrance animation happening, just update the entering icon, not
            // maintaining a queue or anything.
            enteringDrawable = incomingDrawable
            return
        }

        // There was already an icon, need to animate between icons.
        enteringDrawable = incomingDrawable
        transitionAnimator.setCurrentFraction(0f)
        transitionAnimator.start()
        invalidateSelf()
    }

    override fun draw(canvas: Canvas) {
        // Scale the old one down, scale the new one up.
        drawable?.let {
            val scale =
                if (transitionAnimator.isRunning) {
                    1f - transitionAnimator.animatedFraction
                } else {
                    1f
                }
            drawScaledDrawable(it, canvas, scale)
        }
        enteringDrawable?.let {
            val scale = transitionAnimator.animatedFraction
            drawScaledDrawable(it, canvas, scale)
        }

        if (transitionAnimator.isRunning) {
            invalidateSelf()
        }
    }

    private fun drawScaledDrawable(drawable: Drawable, canvas: Canvas, scale: Float) {
        drawable.bounds = getBounds()
        canvas.save()
        canvas.scale(
            scale,
            scale,
            (drawable.intrinsicWidth / 2).toFloat(),
            (drawable.intrinsicHeight / 2).toFloat()
        )
        drawable.draw(canvas)
        canvas.restore()
    }

    private fun onTransitionComplete() {
        drawable = enteringDrawable
        enteringDrawable = null
        invalidateSelf()
    }

    override fun setTintList(tint: ColorStateList?) {
        super.setTintList(tint)
        drawable?.setTintList(tint)
        enteringDrawable?.setTintList(tint)
        this.tint = tint
    }

    override fun setAlpha(alpha: Int) {
        this.alpha = alpha
    }

    override fun setColorFilter(colorFilter: ColorFilter?) {
        this.colorFilter = colorFilter
        drawable?.colorFilter = colorFilter
        enteringDrawable?.colorFilter = colorFilter
    }

    override fun getOpacity(): Int = alpha
}
+8 −1
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.android.systemui.res.R
import com.android.systemui.screenshot.ui.TransitioningIconDrawable
import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel

object ActionButtonViewBinder {
@@ -28,7 +29,13 @@ object ActionButtonViewBinder {
    fun bind(view: View, viewModel: ActionButtonViewModel) {
        val iconView = view.requireViewById<ImageView>(R.id.overlay_action_chip_icon)
        val textView = view.requireViewById<TextView>(R.id.overlay_action_chip_text)
        iconView.setImageDrawable(viewModel.appearance.icon)
        if (iconView.drawable == null) {
            iconView.setImageDrawable(TransitioningIconDrawable())
        }
        val drawable = iconView.drawable as? TransitioningIconDrawable
        // Note we never re-bind a view to a different ActionButtonViewModel, different view
        // models would remove/create separate views.
        drawable?.setIcon(viewModel.appearance.icon)
        textView.text = viewModel.appearance.label
        setMargins(iconView, textView, viewModel.appearance.label?.isNotEmpty() ?: false)
        if (viewModel.onClicked != null) {