1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.docklib.view.animation
18 
19 import android.content.res.ColorStateList
20 import android.graphics.Color
21 import android.graphics.PorterDuff
22 import android.graphics.PorterDuffColorFilter
23 import android.os.Build
24 import android.util.Log
25 import android.util.Property
26 import android.view.View
27 import android.widget.ImageView
28 import androidx.annotation.ColorInt
29 import androidx.annotation.VisibleForTesting
30 import androidx.core.animation.Animator
31 import androidx.core.animation.ArgbEvaluator
32 import androidx.core.animation.ObjectAnimator
33 import androidx.core.animation.PathInterpolator
34 import androidx.core.animation.PropertyValuesHolder
35 import androidx.core.graphics.alpha
36 import androidx.core.graphics.toColorLong
37 import com.google.android.material.imageview.ShapeableImageView
38 
39 /**
40  * Helper class to build an [Animator] that can animate a view to be excited or reset. This is
41  * generally used to excite the dock item when something is hovered over it.
42  */
43 class ExcitementAnimationHelper {
44     companion object {
45         private const val TAG = "ExcitementAnimator"
46         private val DEBUG = Build.isDebuggable()
47         private const val PVH_STROKE_WIDTH = "strokeWidth"
48         private const val PVH_STROKE_COLOR = "strokeColor"
49         private const val PVH_PADDING = "padding"
50         private const val PVH_COLOR_FILTER = "colorFilter"
51 
52         /**
53          * Creates an [Animator] using the final values to be animated to. The initial values to
54          * start the animation from are taken from the View.
55          *
56          * Some assumptions:
57          * 1. strokeWidth, strokeColor and contentPadding will only be animated if the given [view]
58          *   is of type [ShapeableImageView]
59          * 2. colorFilter will only be animated if the given [view] is of type [ImageView]
60          * 3. The colorFilter set on the [view] should be [PorterDuffColorFilter] to be able to get
61          *   the initial value from.
62          *
63          *   @param view [View] that should be animated and sourced the initial animation values
64          *   from.
65          *   @param animationDuration length of the animation
66          *   @param toStrokeWidth final strokeWidth value
67          *   @param toStrokeColor final strokeColor value
68          *   @param toContentPadding final content padding value, applied to all sides
69          *   @param toColorFilterAlpha final alpha value of the colorFilter
70          *   @param successCallback called when the animation has completed successfully
71          *   @param failureCallback called when the animation is unsuccessful/cancelled
72          */
getExcitementAnimatornull73         fun getExcitementAnimator(
74             view: View,
75             animationDuration: Long,
76             toStrokeWidth: Float,
77             @ColorInt toStrokeColor: Int,
78             toContentPadding: Int,
79             toColorFilterAlpha: Float,
80             successCallback: Runnable,
81             failureCallback: Runnable
82         ): Animator {
83             // todo(b/312718542): hidden api(PorterDuffColorFilter.getAlpha) usage
84             return getExcitementAnimator(
85                 view,
86                 animationDuration,
87                 fromStrokeWidth = getStrokeWidth(view, defaultWidth = 0f),
88                 toStrokeWidth = toStrokeWidth,
89                 fromStrokeColor = getStrokeColor(view, defaultColor = Color.rgb(0f, 0f, 0f)),
90                 toStrokeColor = toStrokeColor,
91                 fromPadding = getAverageContentPadding(view, defaultContentPadding = 0),
92                 toContentPadding = toContentPadding,
93                 fromColorFilterAlpha = getColorFilterAlpha(view, defaultColorFilterAlpha = 0f),
94                 toColorFilterAlpha = toColorFilterAlpha,
95                 successCallback,
96                 failureCallback
97             )
98         }
99 
getExcitementAnimatornull100         private fun getExcitementAnimator(
101             view: View,
102             animationDuration: Long,
103             fromStrokeWidth: Float,
104             toStrokeWidth: Float,
105             @ColorInt fromStrokeColor: Int,
106             @ColorInt toStrokeColor: Int,
107             fromPadding: Int,
108             toContentPadding: Int,
109             fromColorFilterAlpha: Float,
110             toColorFilterAlpha: Float,
111             successCallback: Runnable,
112             failureCallback: Runnable
113         ): Animator {
114             if (DEBUG) {
115                 Log.d(
116                     TAG,
117                     "getExcitementAnimator{" +
118                             "view: $view, " +
119                             "animationDuration: $animationDuration, " +
120                             "fromStrokeWidth: $fromStrokeWidth," +
121                             "toStrokeWidth: $toStrokeWidth," +
122                             "fromStrokeColor: $fromStrokeColor," +
123                             "toStrokeColor: $toStrokeColor," +
124                             "fromPadding: $fromPadding," +
125                             "toContentPadding: $toContentPadding," +
126                             "fromColorFilterAlpha: $fromColorFilterAlpha," +
127                             "toColorFilterAlpha: $toColorFilterAlpha," +
128                             "}"
129                 )
130             }
131             var pvhStrokeWidth: PropertyValuesHolder? = null
132             val pvhPadding: PropertyValuesHolder?
133             var pvhColorFilter: PropertyValuesHolder? = null
134             var pvhStrokeColor: PropertyValuesHolder? = null
135 
136             if (view is ShapeableImageView) {
137                 pvhStrokeWidth = PropertyValuesHolder.ofFloat(
138                     PVH_STROKE_WIDTH,
139                     fromStrokeWidth,
140                     toStrokeWidth
141                 )
142 
143                 pvhStrokeColor =
144                     PropertyValuesHolder.ofObject(
145                         object : Property<View, Int>(Int::class.java, PVH_STROKE_COLOR) {
146                             override fun get(view: View?): Int {
147                                 return getStrokeColor(view, defaultColor = fromStrokeColor)
148                             }
149 
150                             override fun set(view: View?, value: Int?) {
151                                 if (view is ShapeableImageView && value != null) {
152                                     view.strokeColor = ColorStateList.valueOf(value)
153                                 }
154                             }
155                         },
156                         ArgbEvaluator.getInstance(),
157                         fromStrokeColor,
158                         toStrokeColor
159                     )
160             }
161 
162             pvhPadding = PropertyValuesHolder.ofInt(
163                 object : Property<View, Int>(Int::class.java, PVH_PADDING) {
164                     override fun get(view: View?): Int {
165                         return if (view != null) {
166                             getAverageContentPadding(view, defaultContentPadding = 0)
167                         } else {
168                             0
169                         }
170                     }
171 
172                     override fun set(view: View?, value: Int?) {
173                         if (view != null && value != null) setContentPadding(view, value)
174                     }
175                 },
176                 fromPadding,
177                 toContentPadding
178             )
179 
180             if (view is ImageView) {
181                 pvhColorFilter =
182                     PropertyValuesHolder.ofFloat(
183                         object : Property<View, Float>(
184                             Float::class.java,
185                             PVH_COLOR_FILTER
186                         ) {
187                             override fun get(view: View?): Float {
188                                 return getColorFilterAlpha(view, defaultColorFilterAlpha = 0f)
189                             }
190 
191                             override fun set(view: View?, value: Float?) {
192                                 if (view is ImageView && value != null) {
193                                     view.colorFilter = PorterDuffColorFilter(
194                                         Color.argb(value, 0f, 0f, 0f),
195                                         PorterDuff.Mode.DARKEN
196                                     )
197                                 }
198                             }
199                         },
200                         fromColorFilterAlpha,
201                         toColorFilterAlpha
202                     )
203             }
204 
205             val animator = ObjectAnimator.ofPropertyValuesHolder(
206                 view,
207                 pvhStrokeWidth,
208                 pvhPadding,
209                 pvhColorFilter,
210                 pvhStrokeColor
211             )
212             animator.setDuration(animationDuration)
213             animator.interpolator = PathInterpolator(0f, 0f, 0f, 1f)
214             animator.addListener(getAnimatorListener(successCallback, failureCallback))
215             return animator
216         }
217 
218         @VisibleForTesting
getAnimatorListenernull219         fun getAnimatorListener(
220             successCallback: Runnable,
221             failureCallback: Runnable
222         ): Animator.AnimatorListener {
223             return object : Animator.AnimatorListener {
224                 private var isCancelled = false
225                 override fun onAnimationStart(animator: Animator) {
226                     isCancelled = false
227                 }
228 
229                 override fun onAnimationEnd(animator: Animator) {
230                     if (!isCancelled) successCallback.run()
231                 }
232 
233                 override fun onAnimationCancel(animator: Animator) {
234                     isCancelled = true
235                     failureCallback.run()
236                 }
237 
238                 override fun onAnimationRepeat(animator: Animator) {
239                     // no-op
240                 }
241             }
242         }
243 
setContentPaddingnull244         private fun setContentPadding(view: View, contentPadding: Int) {
245             (view as? ShapeableImageView)?.setContentPadding(
246                 contentPadding,
247                 contentPadding,
248                 contentPadding,
249                 contentPadding
250             )
251         }
252 
getAverageContentPaddingnull253         private fun getAverageContentPadding(view: View, defaultContentPadding: Int): Int {
254             (view as? ShapeableImageView)?.let {
255                 return (it.contentPaddingStart + it.contentPaddingEnd +
256                         it.contentPaddingTop + it.contentPaddingBottom) / 4
257             }
258             return defaultContentPadding
259         }
260 
getColorFilterAlphanull261         private fun getColorFilterAlpha(view: View?, defaultColorFilterAlpha: Float): Float {
262             return ((view as? ImageView)
263                 ?.colorFilter as? PorterDuffColorFilter)
264                 ?.color?.toColorLong()?.alpha ?: defaultColorFilterAlpha
265         }
266 
getStrokeWidthnull267         private fun getStrokeWidth(view: View?, defaultWidth: Float): Float {
268             return (view as? ShapeableImageView)?.strokeWidth ?: defaultWidth
269         }
270 
getStrokeColornull271         private fun getStrokeColor(view: View?, @ColorInt defaultColor: Int): Int {
272             return (view as? ShapeableImageView)
273                 ?.strokeColor?.getColorForState(null, defaultColor) ?: defaultColor
274         }
275     }
276 }
277