Merge "Animate changes between action icons." into 24D1-dev am: 6610b19405
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/27196797
Change-Id: I95faa93346c7e7c3541ad65090aa6cbacf9263a8
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/TransitioningIconDrawable.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/TransitioningIconDrawable.kt
new file mode 100644
index 0000000..0bc280c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/TransitioningIconDrawable.kt
@@ -0,0 +1,134 @@
+/*
+ * 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
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
index 3c5a0ec..750bd53 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
@@ -21,6 +21,7 @@
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 @@
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) {