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

12

我的代码

活动(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)
    }
}

布局

<?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>

场景

<?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>

观察到的行为

enter image description here

期望的行为

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

编辑(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
      )
    )
  }

然后我这样调用它们:

onCreate, onCreateView

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

onSaveInstanceState

:在Android开发中,onSaveInstanceState是一个用于保存Activity或Fragment状态的方法。当操作系统因内存不足或者旋转屏幕等原因导致Activity或者Fragment被销毁时,可以使用onSaveInstanceState保存当前状态,以便在Activity或者Fragment被重新创建时恢复之前的状态。
    binding.transactionsMotionLayout.saveState(outState, MOTION_LAYOUT_STATE_KEY)

这导致了在Activity中的MotionLayouts和在Fragment中的MotionLayouts都表现出了预期的行为。但是我不满意所需的代码量,如果有人能提供更简洁的解决方案,我将非常高兴听取意见 :)

2个回答

22

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

你当前的解决方案(除了需要在活动和片段层面进行额外的处理)将在可分离片段的情况下失败,因为它们内部处理视图保存状态而不实际填充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中使用这个类,而不是MotionLayout,但它更少出错并且将尊重适当的视图状态保存机制,因此您不再需要在活动或片段中添加任何额外的代码。

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


运行得很好,谢谢! - Astrit Veliu
1
请记得为MotionLayout定义视图ID,如果MotionLayout没有ID,则保存和恢复功能将无法正常工作。 - robert

0

MotionLayout有以下方法:

Bundle getTransitionState()

并且

void setTransitionState(Bundle bundle) 

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

对于简单的MotionLayout,可以使用此功能。也可能有更复杂的MotionLayout,其中该信息是不足的。


正如您在我的Activity代码中所看到的那样,我正在按照您所说的做,但是正如您在gif中所看到的那样,它对于最简单的情况也无法正常工作。 - A. Patrik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接