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.ui.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.ui.graphics.Color
import androidx.compose.ui.graphics.vector.Animator
import androidx.compose.ui.graphics.vector.AnimatorSet
import androidx.compose.ui.graphics.vector.Keyframe
import androidx.compose.ui.graphics.vector.ObjectAnimator
import androidx.compose.ui.graphics.vector.Ordering
import androidx.compose.ui.graphics.vector.PathNode
import androidx.compose.ui.graphics.vector.PropertyValuesHolder
import androidx.compose.ui.graphics.vector.PropertyValuesHolder1D
import androidx.compose.ui.graphics.vector.PropertyValuesHolder2D
import androidx.compose.ui.graphics.vector.PropertyValuesHolderColor
import androidx.compose.ui.graphics.vector.PropertyValuesHolderFloat
import androidx.compose.ui.graphics.vector.PropertyValuesHolderInt
import androidx.compose.ui.graphics.vector.PropertyValuesHolderPath
import androidx.compose.ui.graphics.vector.addPathNodes
import androidx.compose.ui.res.AccelerateDecelerateEasing
import androidx.compose.ui.res.AccelerateEasing
import androidx.compose.ui.res.AnticipateEasing
import androidx.compose.ui.res.AnticipateOvershootEasing
import androidx.compose.ui.res.BounceEasing
import androidx.compose.ui.res.CycleEasing
import androidx.compose.ui.res.DecelerateEasing
import androidx.compose.ui.res.OvershootEasing
import androidx.compose.ui.res.loadInterpolatorResource
import androidx.compose.ui.res.toEasing
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")
    }
}