XmlAnimatorParser.android.kt

/*
 * Copyright 2021 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 androidx.compose.animation.graphics.vector.compat

import android.content.res.Resources
import android.content.res.TypedArray
import android.util.AttributeSet
import android.util.TypedValue
import android.view.animation.PathInterpolator
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.graphics.res.AccelerateDecelerateEasing
import androidx.compose.animation.graphics.res.AccelerateEasing
import androidx.compose.animation.graphics.res.AnticipateEasing
import androidx.compose.animation.graphics.res.AnticipateOvershootEasing
import androidx.compose.animation.graphics.res.BounceEasing
import androidx.compose.animation.graphics.res.CycleEasing
import androidx.compose.animation.graphics.res.DecelerateEasing
import androidx.compose.animation.graphics.res.OvershootEasing
import androidx.compose.animation.graphics.res.loadInterpolatorResource
import androidx.compose.animation.graphics.res.toEasing
import androidx.compose.animation.graphics.vector.Animator
import androidx.compose.animation.graphics.vector.AnimatorSet
import androidx.compose.animation.graphics.vector.Keyframe
import androidx.compose.animation.graphics.vector.ObjectAnimator
import androidx.compose.animation.graphics.vector.Ordering
import androidx.compose.animation.graphics.vector.PropertyValuesHolder
import androidx.compose.animation.graphics.vector.PropertyValuesHolder1D
import androidx.compose.animation.graphics.vector.PropertyValuesHolder2D
import androidx.compose.animation.graphics.vector.PropertyValuesHolderColor
import androidx.compose.animation.graphics.vector.PropertyValuesHolderFloat
import androidx.compose.animation.graphics.vector.PropertyValuesHolderInt
import androidx.compose.animation.graphics.vector.PropertyValuesHolderPath
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.PathNode
import androidx.compose.ui.graphics.vector.addPathNodes
import androidx.core.graphics.PathParser
import org.xmlpull.v1.XmlPullParser

internal const val TagSet = "set"
internal const val TagObjectAnimator = "objectAnimator"
private const val TagPropertyValuesHolder = "propertyValuesHolder"
private const val TagKeyframe = "keyframe"

private const val ValueTypeFloat = 0
private const val ValueTypeInt = 1
private const val ValueTypePath = 2
private const val ValueTypeColor = 3
private const val ValueTypeUndefined = 4

private const val RepeatModeReverse = 2

private enum class ValueType {
    Float,
    Int,
    Color,
    Path
}

private val FallbackValueType = ValueType.Float

private fun TypedArray.getInterpolator(
    res: Resources,
    theme: Resources.Theme?,
    index: Int,
    defaultValue: Easing
): Easing {
    val id = getResourceId(index, 0)
    return if (id == 0) {
        defaultValue
    } else {
        loadInterpolatorResource(theme, res, id)
    }
}

private fun parseKeyframe(
    res: Resources,
    theme: Resources.Theme?,
    attrs: AttributeSet,
    holderValueType: ValueType?,
    defaultInterpolator: Easing
): Pair<Keyframe<Any>, ValueType> {
    return attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_KEYFRAME) { a ->
        val inferredValueType =
            // The type is specified in <propertyValuesHolder>.
            holderValueType
                ?: inferValueType( // Identify the type from our attribute values.
                    a.getInt(
                        AndroidVectorResources.STYLEABLE_KEYFRAME_VALUE_TYPE,
                        ValueTypeUndefined
                    ),
                    a.peekValue(AndroidVectorResources.STYLEABLE_KEYFRAME_VALUE).type
                )
                // We didn't have any clue until the end.
                ?: FallbackValueType
        a.getKeyframe(
            a.getFloat(AndroidVectorResources.STYLEABLE_KEYFRAME_FRACTION, 0f),
            a.getInterpolator(
                res,
                theme,
                AndroidVectorResources.STYLEABLE_KEYFRAME_INTERPOLATOR,
                defaultInterpolator
            ),
            inferredValueType,
            AndroidVectorResources.STYLEABLE_KEYFRAME_VALUE
        ) to inferredValueType // Report back the type to <propertyValuesHolder>.
    }
}

/**
 * Extracts a [Keyframe] value from this [TypedArray]. This [TypedArray] can come from either
 * `<propertyValuesHolder>` or `<keyframe>`
 */
private fun TypedArray.getKeyframe(
    fraction: Float,
    interpolator: Easing,
    valueType: ValueType,
    valueIndex: Int
): Keyframe<Any> {
    return when (valueType) {
        ValueType.Float -> Keyframe(
            fraction,
            getFloat(valueIndex, 0f),
            interpolator
        )
        ValueType.Int -> Keyframe(
            fraction,
            getInt(valueIndex, 0),
            interpolator
        )
        ValueType.Color -> Keyframe(
            fraction,
            Color(getColor(valueIndex, 0)),
            interpolator
        )
        ValueType.Path -> Keyframe(
            fraction,
            addPathNodes(getString(valueIndex)),
            interpolator
        )
    }
}

private fun XmlPullParser.parsePropertyValuesHolder(
    res: Resources,
    theme: Resources.Theme?,
    attrs: AttributeSet,
    interpolator: Easing
): PropertyValuesHolder<*> {
    return attrs.attrs(
        res,
        theme,
        AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER
    ) { a ->
        a.getPropertyValuesHolder1D(
            a.getString(
                AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER_PROPERTY_NAME
            )!!,
            AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_TYPE,
            AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_FROM,
            AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_TO,
            interpolator
        ) { valueType, keyframes ->
            var vt: ValueType? = null
            forEachChildOf(TagPropertyValuesHolder) {
                if (eventType == XmlPullParser.START_TAG && name == TagKeyframe) {
                    val (keyframe, keyframeValueType) =
                        parseKeyframe(res, theme, attrs, valueType, interpolator)
                    if (vt == null) vt = keyframeValueType
                    keyframes.add(keyframe)
                }
            }
            // This determines the final ValueType of the PropertyValuesHolder.
            vt ?: valueType ?: FallbackValueType
        }
    }
}

/**
 * Infers a [ValueType] from various information from XML.
 *
 * @param valueType The `valueType` attribute specified in the XML.
 * @param typedValueTypes [TypedValue.type] values taken from multiple [TypedValue]s.
 * @return A [ValueType] identified by the information so far, or `null` if it is uncertain.
 */
private fun inferValueType(valueType: Int, vararg typedValueTypes: Int): ValueType? {
    return when (valueType) {
        ValueTypeFloat -> ValueType.Float
        ValueTypeInt -> ValueType.Int
        ValueTypeColor -> ValueType.Color
        ValueTypePath -> ValueType.Path
        else ->
            if (
                typedValueTypes
                    .all {
                        it in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT
                    }
            ) {
                ValueType.Color
            } else {
                null
            }
    }
}

/**
 * Extracts attribute values related to [PropertyValuesHolder]. This [TypedArray] can be taken from
 * either `<objectAnimator>` or `<propertyValuesHolder>`.
 *
 * @param parseKeyframes The caller should parse `<keyframe>`s inside of this
 * `<propertyValuesHolder>` and store them in the `keyframes` [MutableList]. The lambda receives
 * a [ValueType] if it has been identified so far. The lambda has to return [ValueType] in case it
 * is first identified while parsing keyframes.
 */
private fun TypedArray.getPropertyValuesHolder1D(
    propertyName: String,
    valueTypeIndex: Int,
    valueFromIndex: Int,
    valueToIndex: Int,
    interpolator: Easing,
    parseKeyframes: (
        valueType: ValueType?,
        keyframes: MutableList<Keyframe<Any>>
    ) -> ValueType = { vt, _ -> vt ?: FallbackValueType }
): PropertyValuesHolder1D<*> {
    val valueType = getInt(
        valueTypeIndex,
        ValueTypeUndefined
    )

    val valueFrom = peekValue(valueFromIndex)
    val hasFrom = valueFrom != null
    val typeFrom = valueFrom?.type ?: ValueTypeUndefined

    val valueTo = peekValue(valueToIndex)
    val hasTo = valueTo != null
    val typeTo = valueTo?.type ?: ValueTypeUndefined

    var inferredValueType =
        inferValueType(
            valueType,
            typeFrom,
            typeTo
        )
    val keyframes = mutableListOf<Keyframe<Any>>()
    if (inferredValueType == null && (hasFrom || hasTo)) {
        inferredValueType =
            ValueType.Float
    }
    if (hasFrom) {
        keyframes.add(getKeyframe(0f, interpolator, inferredValueType!!, valueFromIndex))
    }
    if (hasTo) {
        keyframes.add(getKeyframe(1f, interpolator, inferredValueType!!, valueToIndex))
    }
    inferredValueType = parseKeyframes(inferredValueType, keyframes)
    keyframes.sortBy { it.fraction }
    @Suppress("UNCHECKED_CAST")
    return when (inferredValueType) {
        ValueType.Float -> PropertyValuesHolderFloat(
            propertyName,
            keyframes as List<Keyframe<Float>>
        )
        ValueType.Int -> PropertyValuesHolderInt(
            propertyName,
            keyframes as List<Keyframe<Int>>
        )
        ValueType.Color -> PropertyValuesHolderColor(
            propertyName,
            keyframes as List<Keyframe<Color>>
        )
        ValueType.Path -> PropertyValuesHolderPath(
            propertyName,
            keyframes as List<Keyframe<List<PathNode>>>
        )
    }
}

private fun convertRepeatMode(repeatMode: Int) = when (repeatMode) {
    RepeatModeReverse -> RepeatMode.Reverse
    else -> RepeatMode.Restart
}

internal fun XmlPullParser.parseObjectAnimator(
    res: Resources,
    theme: Resources.Theme?,
    attrs: AttributeSet
): ObjectAnimator {
    return attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_ANIMATOR) { a ->
        attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR) { oa ->
            val interpolator = a.getInterpolator(
                res,
                theme,
                AndroidVectorResources.STYLEABLE_ANIMATOR_INTERPOLATOR,
                AccelerateDecelerateEasing
            )
            val holders = mutableListOf<PropertyValuesHolder<*>>()
            // 2D; This <objectAnimator> has `propertyXName`, `propertyYName`, and `pathData`.
            oa.getString(
                AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR_PATH_DATA
            )?.let { pathData ->
                holders.add(
                    PropertyValuesHolder2D(
                        oa.getString(
                            AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_X_NAME
                        )!!,
                        oa.getString(
                            AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_Y_NAME
                        )!!,
                        addPathNodes(pathData),
                        interpolator
                    )
                )
            }
            // 1D; This <objectAnimator> has `propertyName`, `valueFrom`, and `valueTo`.
            oa.getString(
                AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_NAME
            )?.let { propertyName ->
                holders.add(
                    a.getPropertyValuesHolder1D(
                        propertyName,
                        AndroidVectorResources.STYLEABLE_ANIMATOR_VALUE_TYPE,
                        AndroidVectorResources.STYLEABLE_ANIMATOR_VALUE_FROM,
                        AndroidVectorResources.STYLEABLE_ANIMATOR_VALUE_TO,
                        interpolator
                    )
                )
            }
            // This <objectAnimator> has <propertyValuesHolder> inside.
            forEachChildOf(TagObjectAnimator) {
                if (eventType == XmlPullParser.START_TAG && name == TagPropertyValuesHolder) {
                    holders.add(parsePropertyValuesHolder(res, theme, attrs, interpolator))
                }
            }

            ObjectAnimator(
                duration = a.getInt(
                    AndroidVectorResources.STYLEABLE_ANIMATOR_DURATION,
                    300
                ),
                startDelay = a.getInt(
                    AndroidVectorResources.STYLEABLE_ANIMATOR_START_OFFSET,
                    0
                ),
                repeatCount = a.getInt(
                    AndroidVectorResources.STYLEABLE_ANIMATOR_REPEAT_COUNT,
                    0
                ),
                repeatMode = convertRepeatMode(
                    a.getInt(AndroidVectorResources.STYLEABLE_ANIMATOR_REPEAT_MODE, 0)
                ),
                holders = holders
            )
        }
    }
}

internal fun XmlPullParser.parseAnimatorSet(
    res: Resources,
    theme: Resources.Theme?,
    attrs: AttributeSet
): AnimatorSet {
    return attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_ANIMATOR_SET) { a ->
        val ordering = a.getInt(AndroidVectorResources.STYLEABLE_ANIMATOR_SET_ORDERING, 0)
        val animators = mutableListOf<Animator>()
        forEachChildOf(TagSet) {
            if (eventType == XmlPullParser.START_TAG) {
                when (name) {
                    TagSet -> animators.add(parseAnimatorSet(res, theme, attrs))
                    TagObjectAnimator -> animators.add(parseObjectAnimator(res, theme, attrs))
                }
            }
        }
        AnimatorSet(
            animators,
            if (ordering != 0) Ordering.Sequentially else Ordering.Together
        )
    }
}

internal fun XmlPullParser.parseInterpolator(
    res: Resources,
    theme: Resources.Theme?,
    attrs: AttributeSet
): Easing {
    return when (name) {
        "linearInterpolator" -> LinearEasing
        "accelerateInterpolator" ->
            attrs.attrs(
                res, theme, AndroidVectorResources.STYLEABLE_ACCELERATE_INTERPOLATOR
            ) { a ->
                val factor = a.getFloat(
                    AndroidVectorResources.STYLEABLE_ACCELERATE_INTERPOLATOR_FACTOR, 1.0f
                )
                if (factor == 1.0f) AccelerateEasing else AccelerateEasing(factor)
            }
        "decelerateInterpolator" ->
            attrs.attrs(
                res, theme, AndroidVectorResources.STYLEABLE_DECELERATE_INTERPOLATOR
            ) { a ->
                val factor = a.getFloat(
                    AndroidVectorResources.STYLEABLE_DECELERATE_INTERPOLATOR_FACTOR, 1.0f
                )
                if (factor == 1.0f) DecelerateEasing else DecelerateEasing(factor)
            }
        "accelerateDecelerateInterpolator" -> AccelerateDecelerateEasing
        "cycleInterpolator" ->
            attrs.attrs(
                res, theme, AndroidVectorResources.STYLEABLE_CYCLE_INTERPOLATOR
            ) { a ->
                CycleEasing(
                    a.getFloat(
                        AndroidVectorResources.STYLEABLE_CYCLE_INTERPOLATOR_CYCLES, 1.0f
                    )
                )
            }
        "anticipateInterpolator" ->
            attrs.attrs(
                res,
                theme,
                AndroidVectorResources.STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR
            ) { a ->
                AnticipateEasing(
                    a.getFloat(
                        AndroidVectorResources.STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR_TENSION,
                        2.0f
                    )
                )
            }
        "overshootInterpolator" ->
            attrs.attrs(
                res, theme, AndroidVectorResources.STYLEABLE_OVERSHOOT_INTERPOLATOR
            ) { a ->
                OvershootEasing(
                    a.getFloat(
                        AndroidVectorResources.STYLEABLE_OVERSHOOT_INTERPOLATOR_TENSION,
                        2.0f
                    )
                )
            }
        "anticipateOvershootInterpolator" ->
            attrs.attrs(
                res,
                theme,
                AndroidVectorResources.STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR
            ) { a ->
                AnticipateOvershootEasing(
                    a.getFloat(
                        AndroidVectorResources.STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR_TENSION,
                        2.0f
                    ),
                    a.getFloat(
                        AndroidVectorResources
                            .STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR_EXTRA_TENSION,
                        1.5f
                    )
                )
            }
        "bounceInterpolator" -> BounceEasing
        "pathInterpolator" ->
            attrs.attrs(
                res, theme, AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR
            ) { a ->
                val pathData =
                    a.getString(AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_PATH_DATA)
                if (pathData != null) {
                    PathInterpolator(PathParser.createPathFromPathData(pathData)).toEasing()
                } else if (
                    !a.hasValue(AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_2) ||
                    !a.hasValue(AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_2)
                ) {
                    PathInterpolator(
                        a.getFloat(
                            AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_1,
                            0f
                        ),
                        a.getFloat(
                            AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_1,
                            0f
                        )
                    ).toEasing()
                } else {
                    CubicBezierEasing(
                        a.getFloat(
                            AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_1,
                            0f
                        ),
                        a.getFloat(
                            AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_1,
                            0f
                        ),
                        a.getFloat(
                            AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_2,
                            1f
                        ),
                        a.getFloat(
                            AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_2,
                            1f
                        )
                    )
                }
            }
        else -> throw RuntimeException("Unknown interpolator: $name")
    }
}