如何在不自动播放过渡的情况下恢复 MotionLayout 的过渡状态?

How to restore transition state of MotionLayout without auto-playing the transition?

我的代码

Activity

class SwipeHandlerActivity : AppCompatActivity(R.layout.activity_swipe_handler){
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putBundle("Foo", findViewById<MotionLayout>(R.id.the_motion_layout).transitionState)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        savedInstanceState?.getBundle("Foo")?.let(findViewById<MotionLayout>(R.id.the_motion_layout)::setTransitionState)
    }
}

Layout

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/activity_swipe_handler_scene"
    android:id="@+id/the_motion_layout"
    app:motionDebug="SHOW_ALL">

    <View
        android:id="@+id/touchAnchorView"
        android:background="#8309AC"
        android:layout_width="64dp"
        android:layout_height="64dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>

Scene

<?xml version="1.0" encoding="utf-8"?>
<MotionScene 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="1000">

        <OnSwipe
            motion:touchAnchorId="@id/imageView"
            motion:dragDirection="dragUp"
            motion:touchAnchorSide="top" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:layout_height="250dp"
            motion:layout_constraintStart_toStartOf="@+id/textView2"
            motion:layout_constraintEnd_toEndOf="@+id/textView2"
            android:layout_width="250dp"
            android:id="@+id/imageView"
            motion:layout_constraintBottom_toTopOf="@+id/textView2"
            android:layout_marginBottom="68dp" />
    </ConstraintSet>
</MotionScene>

观察到的行为

预期行为

配置更改后运动布局保持在其开始状态

编辑(hacky 解决方案)

我最终创建了这些扩展函数

  fun MotionLayout.restoreState(savedInstanceState: Bundle?, key: String) {
    viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
      override fun onGlobalLayout() {
        doRestore(savedInstanceState, key)
        viewTreeObserver.removeOnGlobalLayoutListener(this)
      }
    })
  }

  private fun MotionLayout.doRestore(savedInstanceState: Bundle?, key: String) =
    savedInstanceState?.let {
      val motionBundle = savedInstanceState.getBundle(key) ?: error("$key state was not saved")
      setTransition(
        motionBundle.getInt("claptrap.motion.startState", -1)
          .takeIf { it != -1 }
          ?: error("Could not retrieve start state for $key"),
        motionBundle.getInt("claptrap.motion.endState", -1)
          .takeIf { it != -1 }
          ?: error("Could not retrieve end state for $key")
      )
      progress = motionBundle.getFloat("claptrap.motion.progress", -1.0f)
        .takeIf { it != -1.0f }
        ?: error("Could not retrieve progress for $key")
    }

  fun MotionLayout.saveState(outState: Bundle, key: String) {
    outState.putBundle(
      key,
      bundleOf(
        "claptrap.motion.startState" to startState,
        "claptrap.motion.endState" to endState,
        "claptrap.motion.progress" to progress
      )
    )
  }

然后我这样称呼他们:

onCreateonCreateView

    if (savedInstanceState != null) {
      binding.transactionsMotionLayout.restoreState(savedInstanceState, MOTION_LAYOUT_STATE_KEY)
    }

onSaveInstanceState

    binding.transactionsMotionLayout.saveState(outState, MOTION_LAYOUT_STATE_KEY)

这导致 MotionLayoutsActivity 中和 MotionLayoutsFragment 中的预期行为。但是我对所需的代码量不满意,所以如果有人能提出更简洁的解决方案,我会很高兴听到:)

我不明白你为什么不这样做,但你需要扩展 MotionLayout 以正确保存视图状态。

您当前的解决方案(除了需要在 activity 和片段层中进行额外处理)在可分离片段的情况下将失败,因为它们在内部处理视图保存状态而不实际填充 savedInstanceState(我很困惑知道)。

open class SavingMotionLayout @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr) {

    override fun onSaveInstanceState(): Parcelable {
        return SaveState(super.onSaveInstanceState(), startState, endState, targetPosition)
    }

    override fun onRestoreInstanceState(state: Parcelable?) {
        (state as? SaveState)?.let {
            super.onRestoreInstanceState(it.superParcel)
            setTransition(it.startState, it.endState)
            progress = it.progress
        }
    }

    @kotlinx.android.parcel.Parcelize
    private class SaveState(
            val superParcel: Parcelable?,
            val startState: Int,
            val endState: Int,
            val progress: Float
    ) : Parcelable
}

您将需要在 XML 中使用此 class 而不是 MotionLayout 但是它不太容易出错并且会遵守适当的视图状态保存机制,因此您不需要不再向活动或片段添加任何额外代码。

如果您想禁用保存,您可以在 XML 中使用 android:saveEnabled="false" 或在代码中使用 isSaveEnabled = false

MotionLayout 有一个方法:

Bundle getTransitionState()

void setTransitionState(Bundle bundle) 

返回并设置的包包含 进度、速度、开始状态和结束状态。

对于简单的 MotionLayouts 可以使用它。 在信息不足的情况下,可能会有更复杂的 MotionLayout。