1 /*
<lambda>null2  * Copyright 2021 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 androidx.compose.animation.graphics.vector.compat
18 
19 import android.content.res.Resources
20 import android.content.res.TypedArray
21 import android.util.AttributeSet
22 import android.util.TypedValue
23 import android.view.animation.PathInterpolator
24 import androidx.compose.animation.core.CubicBezierEasing
25 import androidx.compose.animation.core.Easing
26 import androidx.compose.animation.core.LinearEasing
27 import androidx.compose.animation.core.RepeatMode
28 import androidx.compose.animation.graphics.res.AccelerateDecelerateEasing
29 import androidx.compose.animation.graphics.res.AccelerateEasing
30 import androidx.compose.animation.graphics.res.AnticipateEasing
31 import androidx.compose.animation.graphics.res.AnticipateOvershootEasing
32 import androidx.compose.animation.graphics.res.BounceEasing
33 import androidx.compose.animation.graphics.res.CycleEasing
34 import androidx.compose.animation.graphics.res.DecelerateEasing
35 import androidx.compose.animation.graphics.res.OvershootEasing
36 import androidx.compose.animation.graphics.res.loadInterpolatorResource
37 import androidx.compose.animation.graphics.res.toEasing
38 import androidx.compose.animation.graphics.vector.Animator
39 import androidx.compose.animation.graphics.vector.AnimatorSet
40 import androidx.compose.animation.graphics.vector.Keyframe
41 import androidx.compose.animation.graphics.vector.ObjectAnimator
42 import androidx.compose.animation.graphics.vector.Ordering
43 import androidx.compose.animation.graphics.vector.PropertyValuesHolder
44 import androidx.compose.animation.graphics.vector.PropertyValuesHolder1D
45 import androidx.compose.animation.graphics.vector.PropertyValuesHolder2D
46 import androidx.compose.animation.graphics.vector.PropertyValuesHolderColor
47 import androidx.compose.animation.graphics.vector.PropertyValuesHolderFloat
48 import androidx.compose.animation.graphics.vector.PropertyValuesHolderInt
49 import androidx.compose.animation.graphics.vector.PropertyValuesHolderPath
50 import androidx.compose.ui.graphics.Color
51 import androidx.compose.ui.graphics.vector.PathNode
52 import androidx.compose.ui.graphics.vector.addPathNodes
53 import androidx.core.graphics.PathParser
54 import org.xmlpull.v1.XmlPullParser
55 
56 internal const val TagSet = "set"
57 internal const val TagObjectAnimator = "objectAnimator"
58 private const val TagPropertyValuesHolder = "propertyValuesHolder"
59 private const val TagKeyframe = "keyframe"
60 
61 private const val ValueTypeFloat = 0
62 private const val ValueTypeInt = 1
63 private const val ValueTypePath = 2
64 private const val ValueTypeColor = 3
65 private const val ValueTypeUndefined = 4
66 
67 private const val RepeatModeReverse = 2
68 
69 private enum class ValueType {
70     Float,
71     Int,
72     Color,
73     Path
74 }
75 
76 private val FallbackValueType = ValueType.Float
77 
TypedArraynull78 private fun TypedArray.getInterpolator(
79     res: Resources,
80     theme: Resources.Theme?,
81     index: Int,
82     defaultValue: Easing
83 ): Easing {
84     val id = getResourceId(index, 0)
85     return if (id == 0) {
86         defaultValue
87     } else {
88         loadInterpolatorResource(theme, res, id)
89     }
90 }
91 
parseKeyframenull92 private fun parseKeyframe(
93     res: Resources,
94     theme: Resources.Theme?,
95     attrs: AttributeSet,
96     holderValueType: ValueType?,
97     defaultInterpolator: Easing
98 ): Pair<Keyframe<Any>, ValueType> {
99     return attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_KEYFRAME) { a ->
100         val inferredValueType =
101             // The type is specified in <propertyValuesHolder>.
102             holderValueType
103                 ?: inferValueType( // Identify the type from our attribute values.
104                     a.getInt(
105                         AndroidVectorResources.STYLEABLE_KEYFRAME_VALUE_TYPE,
106                         ValueTypeUndefined
107                     ),
108                     a.peekValue(AndroidVectorResources.STYLEABLE_KEYFRAME_VALUE).type
109                 )
110                 // We didn't have any clue until the end.
111                 ?: FallbackValueType
112         a.getKeyframe(
113             a.getFloat(AndroidVectorResources.STYLEABLE_KEYFRAME_FRACTION, 0f),
114             a.getInterpolator(
115                 res,
116                 theme,
117                 AndroidVectorResources.STYLEABLE_KEYFRAME_INTERPOLATOR,
118                 defaultInterpolator
119             ),
120             inferredValueType,
121             AndroidVectorResources.STYLEABLE_KEYFRAME_VALUE
122         ) to inferredValueType // Report back the type to <propertyValuesHolder>.
123     }
124 }
125 
126 /**
127  * Extracts a [Keyframe] value from this [TypedArray]. This [TypedArray] can come from either
128  * `<propertyValuesHolder>` or `<keyframe>`
129  */
TypedArraynull130 private fun TypedArray.getKeyframe(
131     fraction: Float,
132     interpolator: Easing,
133     valueType: ValueType,
134     valueIndex: Int
135 ): Keyframe<Any> {
136     return when (valueType) {
137         ValueType.Float -> Keyframe(fraction, getFloat(valueIndex, 0f), interpolator)
138         ValueType.Int -> Keyframe(fraction, getInt(valueIndex, 0), interpolator)
139         ValueType.Color -> Keyframe(fraction, Color(getColor(valueIndex, 0)), interpolator)
140         ValueType.Path -> Keyframe(fraction, addPathNodes(getString(valueIndex)), interpolator)
141     }
142 }
143 
parsePropertyValuesHoldernull144 private fun XmlPullParser.parsePropertyValuesHolder(
145     res: Resources,
146     theme: Resources.Theme?,
147     attrs: AttributeSet,
148     interpolator: Easing
149 ): PropertyValuesHolder<*> {
150     return attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER) { a ->
151         a.getPropertyValuesHolder1D(
152             a.getString(AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER_PROPERTY_NAME)!!,
153             AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_TYPE,
154             AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_FROM,
155             AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_TO,
156             interpolator
157         ) { valueType, keyframes ->
158             var vt: ValueType? = null
159             forEachChildOf(TagPropertyValuesHolder) {
160                 if (eventType == XmlPullParser.START_TAG && name == TagKeyframe) {
161                     val (keyframe, keyframeValueType) =
162                         parseKeyframe(res, theme, attrs, valueType, interpolator)
163                     if (vt == null) vt = keyframeValueType
164                     keyframes.add(keyframe)
165                 }
166             }
167             // This determines the final ValueType of the PropertyValuesHolder.
168             vt ?: valueType ?: FallbackValueType
169         }
170     }
171 }
172 
173 /**
174  * Infers a [ValueType] from various information from XML.
175  *
176  * @param valueType The `valueType` attribute specified in the XML.
177  * @param typedValueTypes [TypedValue.type] values taken from multiple [TypedValue]s.
178  * @return A [ValueType] identified by the information so far, or `null` if it is uncertain.
179  */
inferValueTypenull180 private fun inferValueType(valueType: Int, vararg typedValueTypes: Int): ValueType? {
181     return when (valueType) {
182         ValueTypeFloat -> ValueType.Float
183         ValueTypeInt -> ValueType.Int
184         ValueTypeColor -> ValueType.Color
185         ValueTypePath -> ValueType.Path
186         else ->
187             if (
188                 typedValueTypes.all {
189                     it in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT
190                 }
191             ) {
192                 ValueType.Color
193             } else {
194                 null
195             }
196     }
197 }
198 
199 /**
200  * Extracts attribute values related to [PropertyValuesHolder]. This [TypedArray] can be taken from
201  * either `<objectAnimator>` or `<propertyValuesHolder>`.
202  *
203  * @param parseKeyframes The caller should parse `<keyframe>`s inside of this
204  *   `<propertyValuesHolder>` and store them in the `keyframes` [MutableList]. The lambda receives a
205  *   [ValueType] if it has been identified so far. The lambda has to return [ValueType] in case it
206  *   is first identified while parsing keyframes.
207  */
getPropertyValuesHolder1Dnull208 private fun TypedArray.getPropertyValuesHolder1D(
209     propertyName: String,
210     valueTypeIndex: Int,
211     valueFromIndex: Int,
212     valueToIndex: Int,
213     interpolator: Easing,
214     parseKeyframes: (valueType: ValueType?, keyframes: MutableList<Keyframe<Any>>) -> ValueType =
215         { vt, _ ->
216             vt ?: FallbackValueType
217         }
218 ): PropertyValuesHolder1D<*> {
219     val valueType = getInt(valueTypeIndex, ValueTypeUndefined)
220 
221     val valueFrom = peekValue(valueFromIndex)
222     val hasFrom = valueFrom != null
223     val typeFrom = valueFrom?.type ?: ValueTypeUndefined
224 
225     val valueTo = peekValue(valueToIndex)
226     val hasTo = valueTo != null
227     val typeTo = valueTo?.type ?: ValueTypeUndefined
228 
229     var inferredValueType = inferValueType(valueType, typeFrom, typeTo)
230     val keyframes = mutableListOf<Keyframe<Any>>()
231     if (inferredValueType == null && (hasFrom || hasTo)) {
232         inferredValueType = ValueType.Float
233     }
234     if (hasFrom) {
235         keyframes.add(getKeyframe(0f, interpolator, inferredValueType!!, valueFromIndex))
236     }
237     if (hasTo) {
238         keyframes.add(getKeyframe(1f, interpolator, inferredValueType!!, valueToIndex))
239     }
240     inferredValueType = parseKeyframes(inferredValueType, keyframes)
<lambda>null241     keyframes.sortBy { it.fraction }
242     @Suppress("UNCHECKED_CAST")
243     return when (inferredValueType) {
244         ValueType.Float ->
245             PropertyValuesHolderFloat(propertyName, keyframes as List<Keyframe<Float>>)
246         ValueType.Int -> PropertyValuesHolderInt(propertyName, keyframes as List<Keyframe<Int>>)
247         ValueType.Color ->
248             PropertyValuesHolderColor(propertyName, keyframes as List<Keyframe<Color>>)
249         ValueType.Path ->
250             PropertyValuesHolderPath(propertyName, keyframes as List<Keyframe<List<PathNode>>>)
251     }
252 }
253 
convertRepeatModenull254 private fun convertRepeatMode(repeatMode: Int) =
255     when (repeatMode) {
256         RepeatModeReverse -> RepeatMode.Reverse
257         else -> RepeatMode.Restart
258     }
259 
parseObjectAnimatornull260 internal fun XmlPullParser.parseObjectAnimator(
261     res: Resources,
262     theme: Resources.Theme?,
263     attrs: AttributeSet
264 ): ObjectAnimator {
265     return attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_ANIMATOR) { a ->
266         attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR) { oa ->
267             val interpolator =
268                 a.getInterpolator(
269                     res,
270                     theme,
271                     AndroidVectorResources.STYLEABLE_ANIMATOR_INTERPOLATOR,
272                     AccelerateDecelerateEasing
273                 )
274             val holders = mutableListOf<PropertyValuesHolder<*>>()
275             val pathData =
276                 oa.getString(AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR_PATH_DATA)
277             if (pathData != null) {
278                 // 2D; This <objectAnimator> has `pathData`. It should also have `propertyXName`
279                 // and `propertyYName`.
280                 holders.add(
281                     PropertyValuesHolder2D(
282                         oa.getString(
283                             AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_X_NAME
284                         )!!,
285                         oa.getString(
286                             AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_Y_NAME
287                         )!!,
288                         addPathNodes(pathData),
289                         interpolator
290                     )
291                 )
292             } else {
293                 // 1D; This <objectAnimator> has `propertyName`, `valueFrom`, and `valueTo`.
294                 oa.getString(AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_NAME)
295                     ?.let { propertyName ->
296                         holders.add(
297                             a.getPropertyValuesHolder1D(
298                                 propertyName,
299                                 AndroidVectorResources.STYLEABLE_ANIMATOR_VALUE_TYPE,
300                                 AndroidVectorResources.STYLEABLE_ANIMATOR_VALUE_FROM,
301                                 AndroidVectorResources.STYLEABLE_ANIMATOR_VALUE_TO,
302                                 interpolator
303                             )
304                         )
305                     }
306                 // This <objectAnimator> has <propertyValuesHolder> inside.
307                 forEachChildOf(TagObjectAnimator) {
308                     if (eventType == XmlPullParser.START_TAG && name == TagPropertyValuesHolder) {
309                         holders.add(parsePropertyValuesHolder(res, theme, attrs, interpolator))
310                     }
311                 }
312             }
313 
314             ObjectAnimator(
315                 duration = a.getInt(AndroidVectorResources.STYLEABLE_ANIMATOR_DURATION, 300),
316                 startDelay = a.getInt(AndroidVectorResources.STYLEABLE_ANIMATOR_START_OFFSET, 0),
317                 repeatCount = a.getInt(AndroidVectorResources.STYLEABLE_ANIMATOR_REPEAT_COUNT, 0),
318                 repeatMode =
319                     convertRepeatMode(
320                         a.getInt(AndroidVectorResources.STYLEABLE_ANIMATOR_REPEAT_MODE, 0)
321                     ),
322                 holders = holders
323             )
324         }
325     }
326 }
327 
parseAnimatorSetnull328 internal fun XmlPullParser.parseAnimatorSet(
329     res: Resources,
330     theme: Resources.Theme?,
331     attrs: AttributeSet
332 ): AnimatorSet {
333     return attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_ANIMATOR_SET) { a ->
334         val ordering = a.getInt(AndroidVectorResources.STYLEABLE_ANIMATOR_SET_ORDERING, 0)
335         val animators = mutableListOf<Animator>()
336         forEachChildOf(TagSet) {
337             if (eventType == XmlPullParser.START_TAG) {
338                 when (name) {
339                     TagSet -> animators.add(parseAnimatorSet(res, theme, attrs))
340                     TagObjectAnimator -> animators.add(parseObjectAnimator(res, theme, attrs))
341                 }
342             }
343         }
344         AnimatorSet(animators, if (ordering != 0) Ordering.Sequentially else Ordering.Together)
345     }
346 }
347 
parseInterpolatornull348 internal fun XmlPullParser.parseInterpolator(
349     res: Resources,
350     theme: Resources.Theme?,
351     attrs: AttributeSet
352 ): Easing {
353     return when (name) {
354         "linearInterpolator" -> LinearEasing
355         "accelerateInterpolator" ->
356             attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_ACCELERATE_INTERPOLATOR) { a ->
357                 val factor =
358                     a.getFloat(
359                         AndroidVectorResources.STYLEABLE_ACCELERATE_INTERPOLATOR_FACTOR,
360                         1.0f
361                     )
362                 if (factor == 1.0f) AccelerateEasing else AccelerateEasing(factor)
363             }
364         "decelerateInterpolator" ->
365             attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_DECELERATE_INTERPOLATOR) { a ->
366                 val factor =
367                     a.getFloat(
368                         AndroidVectorResources.STYLEABLE_DECELERATE_INTERPOLATOR_FACTOR,
369                         1.0f
370                     )
371                 if (factor == 1.0f) DecelerateEasing else DecelerateEasing(factor)
372             }
373         "accelerateDecelerateInterpolator" -> AccelerateDecelerateEasing
374         "cycleInterpolator" ->
375             attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_CYCLE_INTERPOLATOR) { a ->
376                 CycleEasing(
377                     a.getFloat(AndroidVectorResources.STYLEABLE_CYCLE_INTERPOLATOR_CYCLES, 1.0f)
378                 )
379             }
380         "anticipateInterpolator" ->
381             attrs.attrs(
382                 res,
383                 theme,
384                 AndroidVectorResources.STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR
385             ) { a ->
386                 AnticipateEasing(
387                     a.getFloat(
388                         AndroidVectorResources.STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR_TENSION,
389                         2.0f
390                     )
391                 )
392             }
393         "overshootInterpolator" ->
394             attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_OVERSHOOT_INTERPOLATOR) { a ->
395                 OvershootEasing(
396                     a.getFloat(
397                         AndroidVectorResources.STYLEABLE_OVERSHOOT_INTERPOLATOR_TENSION,
398                         2.0f
399                     )
400                 )
401             }
402         "anticipateOvershootInterpolator" ->
403             attrs.attrs(
404                 res,
405                 theme,
406                 AndroidVectorResources.STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR
407             ) { a ->
408                 AnticipateOvershootEasing(
409                     a.getFloat(
410                         AndroidVectorResources.STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR_TENSION,
411                         2.0f
412                     ),
413                     a.getFloat(
414                         AndroidVectorResources
415                             .STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR_EXTRA_TENSION,
416                         1.5f
417                     )
418                 )
419             }
420         "bounceInterpolator" -> BounceEasing
421         "pathInterpolator" ->
422             attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR) { a ->
423                 val pathData =
424                     a.getString(AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_PATH_DATA)
425                 if (pathData != null) {
426                     PathInterpolator(PathParser.createPathFromPathData(pathData)).toEasing()
427                 } else if (
428                     !a.hasValue(AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_2) ||
429                         !a.hasValue(AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_2)
430                 ) {
431                     PathInterpolator(
432                             a.getFloat(
433                                 AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_1,
434                                 0f
435                             ),
436                             a.getFloat(
437                                 AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_1,
438                                 0f
439                             )
440                         )
441                         .toEasing()
442                 } else {
443                     CubicBezierEasing(
444                         a.getFloat(
445                             AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_1,
446                             0f
447                         ),
448                         a.getFloat(
449                             AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_1,
450                             0f
451                         ),
452                         a.getFloat(
453                             AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_2,
454                             1f
455                         ),
456                         a.getFloat(
457                             AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_2,
458                             1f
459                         )
460                     )
461                 }
462             }
463         else -> throw RuntimeException("Unknown interpolator: $name")
464     }
465 }
466