MotionScene.kt

/*
 * Copyright (C) 2022 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.constraintlayout.compose

import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.core.parser.CLParser
import androidx.constraintlayout.core.parser.CLParsingException
import androidx.constraintlayout.core.state.ConstraintSetParser
import androidx.constraintlayout.core.state.CoreMotionScene
import androidx.constraintlayout.core.state.CorePixelDp
import org.intellij.lang.annotations.Language

/**
 * Information for MotionLayout to animate between multiple [ConstraintSet]s.
 */
@Immutable
@ExperimentalMotionApi
interface MotionScene : CoreMotionScene {
    fun getConstraintSetInstance(name: String): ConstraintSet?

    fun getTransitionInstance(name: String): Transition?
}

/**
 * Parses the given JSON5 into a [MotionScene].
 *
 * See the official [Github Wiki](https://github.com/androidx/constraintlayout/wiki/Compose-MotionLayout-JSON-Syntax) to learn the syntax.
 */
@SuppressLint("ComposableNaming")
@ExperimentalMotionApi
@Composable
fun MotionScene(@Language("json5") content: String): MotionScene {
    val density = LocalDensity.current
    return remember(content) {
        JSONMotionScene(content, CorePixelDp { with(density) { 1.dp.toPx() } })
    }
}

@ExperimentalMotionApi
internal class JSONMotionScene(
    @Language("json5") content: String,
    private val dpToPx: CorePixelDp
) : EditableJSONLayout(content), MotionScene {

    private val constraintSetsContent = HashMap<String, String>()
    private val transitionsContent = HashMap<String, String>()
    private var forcedProgress: Float = Float.NaN

    init {
        // call parent init here so that hashmaps are created
        initialization()
    }

    // region Accessors
    override fun setConstraintSetContent(name: String, content: String) {
        constraintSetsContent[name] = content
    }

    override fun setTransitionContent(name: String, content: String) {
        transitionsContent[name] = content
    }

    override fun getConstraintSet(name: String): String? {
        return constraintSetsContent[name]
    }

    override fun getConstraintSet(index: Int): String? {
        return constraintSetsContent.values.elementAtOrNull(index)
    }

    override fun getTransition(name: String): String? {
        return transitionsContent[name]
    }

    override fun getForcedProgress(): Float {
        return forcedProgress
    }

    override fun resetForcedProgress() {
        forcedProgress = Float.NaN
    }

    override fun getConstraintSetInstance(name: String): ConstraintSet? {
        return getConstraintSet(name)?.let { ConstraintSet(jsonContent = it) }
    }

    override fun getTransitionInstance(name: String): Transition? {
        val parsed = getTransition(name)?.let {
            try {
                CLParser.parse(it)
            } catch (e: CLParsingException) {
                Log.e("CML", "Error parsing JSON $e")
                null
            }
        } ?: return null
        return TransitionImpl(parsed, dpToPx)
    }

    // endregion

    // region On Update Methods
    override fun onNewContent(content: String) {
        super.onNewContent(content)
        try {
            ConstraintSetParser.parseMotionSceneJSON(this, content)
        } catch (e: Exception) {
            // nothing (content might be invalid, sent by live edit)
        }
    }

    override fun onNewProgress(progress: Float) {
        forcedProgress = progress
        signalUpdate()
    }
    // endregion
}