如何在不自动播放过渡的情况下恢复 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
)
)
}
然后我这样称呼他们:
onCreate
、onCreateView
if (savedInstanceState != null) {
binding.transactionsMotionLayout.restoreState(savedInstanceState, MOTION_LAYOUT_STATE_KEY)
}
onSaveInstanceState
binding.transactionsMotionLayout.saveState(outState, MOTION_LAYOUT_STATE_KEY)
这导致 MotionLayouts
在 Activity
中和 MotionLayouts
在 Fragment
中的预期行为。但是我对所需的代码量不满意,所以如果有人能提出更简洁的解决方案,我会很高兴听到:)
我不明白你为什么不这样做,但你需要扩展 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。
我的代码
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
)
)
}
然后我这样称呼他们:
onCreate
、onCreateView
if (savedInstanceState != null) {
binding.transactionsMotionLayout.restoreState(savedInstanceState, MOTION_LAYOUT_STATE_KEY)
}
onSaveInstanceState
binding.transactionsMotionLayout.saveState(outState, MOTION_LAYOUT_STATE_KEY)
这导致 MotionLayouts
在 Activity
中和 MotionLayouts
在 Fragment
中的预期行为。但是我对所需的代码量不满意,所以如果有人能提出更简洁的解决方案,我会很高兴听到:)
我不明白你为什么不这样做,但你需要扩展 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。