问题描述
我的代码
活动
class SwipeHandlerActivity:AppCompatActivity(R.layout.activity_swipe_handler){重写fun onSaveInstanceState(outState:Bundle){super.onSaveInstanceState(outState)outState.putBundle("Foo",findViewById< MotionLayout>(R.id.the_motion_layout).transitionState)}重写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-autoandroid: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"><查看android:id ="@@ id/touchAnchorView"android:background ="#8309ACandroid: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"?< MotionScenexmlns:android ="http://schemas.android.com/apk/res/android"xmlns:motion ="http://schemas.android.com/apk/res-auto"><过渡运动:constraintSetEnd ="@ + id/end";运动:constraintSetStart ="@ id/开始"运动:持续时间="1000"<在刷卡motion:touchAnchorId =" @ id/imageView"motion:dragDirection ="dragUp"motion:touchAnchorSide ="top"/></过渡>< ConstraintSet android:id =" @@ id/start"></ConstraintSet>< ConstraintSet android:id =" @ + id/end"><约束android:layout_height ="250dp";运动:layout_constraintStart_toStartOf ="@@ id/textView2";运动:layout_constraintEnd_toEndOf ="@@ id/textView2";android:layout_width ="250dp";android:id ="@@ id/imageView"运动:layout_constraintBottom_toTopOf ="@@ id/textView2";android:layout_marginBottom =" 68dp"/></ConstraintSet></MotionScene>
观察到的行为
预期行为
更改布局后,运动布局将保持其初始状态
编辑(hacky解决方案)
我最终创建了这些扩展功能
funmotionLayout.restoreState(savedInstanceState:Bundle ?, key:String){viewTreeObserver.addOnGlobalLayoutListener(object:ViewTreeObserver.OnGlobalLayoutListener {覆盖fun onGlobalLayout(){doRestore(savedInstanceState,键)viewTreeObserver.removeOnGlobalLayoutListener(this)}})}私人乐趣MotionLayout.doRestore(savedInstanceState:Bundle ?, key:String)=savedInstanceState?.let {val motionBundle = savedInstanceState.getBundle(key)?:错误(未保存$ key状态")setTransition(motionBundle.getInt("claptrap.motion.startState",-1).takeIf {it!= -1}?:错误(无法检索$ key的开始状态"),motionBundle.getInt("claptrap.motion.endState",-1).takeIf {it!= -1}?:错误(无法检索$ key的结束状态"))进度= motionBundle.getFloat("claptrap.motion.progress",-1.0f).takeIf {它!= -1.0f}?:错误(无法检索$ key的进度")}fun MotionLayout.saveState(outState:Bundle,key:String){outState.putBundle(钥匙,bundleOf("claptrap.motion.startState";进入startState,"claptrap.motion.endState";到endState,"claptrap.motion.progress"进步))}
然后我这样称呼他们:
onCreate
, onCreateView
if(savedInstanceState!= null){binding.transactionsMotionLayout.restoreState(savedInstanceState,MOTION_LAYOUT_STATE_KEY)}
onSaveInstanceState
binding.transactionsMotionLayout.saveState(outState,MOTION_LAYOUT_STATE_KEY)
这导致了 Activity
s中的 MotionLayouts
和 Fragment
s中的 MotionLayouts
的预期行为.但是我对所需的代码量不满意,因此,如果有人可以提出更干净的解决方案,我将非常高兴听到:)
我不明白为什么没有这样做,但需要扩展 MotionLayout
才能正确保存视图状态./p>
您当前的解决方案(除了需要在活动和片段层中进行额外处理)在可分离片段的情况下将失败,因为它们在内部处理视图保存状态而没有实际填充 savedInstanceState
(我知道这很令人困惑).
打开类SavingMotionLayout @JvmOverloads构造函数(context:上下文,attrs:AttributeSet?= null,defStyleAttr:Int = 0):MotionLayout(context,attrs,defStyleAttr){重写fun onSaveInstanceState():可打包{返回SaveState(super.onSaveInstanceState(),startState,endState,targetPosition)}重写fun onRestoreInstanceState(state:Parcelable?){(状态为?SaveState)?. let {super.onRestoreInstanceState(it.superParcel)setTransition(it.startState,it.endState)进度= it.progress}}@ kotlinx.android.parcel.Parcelize私有类SaveState(val superParcel:可包裹吗?,val startState:Int,val endState:Int,val进展:浮动):可包裹}
您将需要在XML中使用此类,而不是 MotionLayout
,但是此类不太容易出错,并且会遵循适当的视图状态保存机制,因此您无需向活动或碎片了.
如果要禁用保存,可以使用XML中的 android:saveEnabled ="false"
或代码中的 isSaveEnabled = false
进行保存.
My code
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>
Observed behavior
Expected behavior
The motion layout stays at its start state after configuration change
Edit (hacky solution)
I ended up creating these extension functions
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
)
)
}
Then I called them like this:
onCreate
, onCreateView
if (savedInstanceState != null) {
binding.transactionsMotionLayout.restoreState(savedInstanceState, MOTION_LAYOUT_STATE_KEY)
}
onSaveInstanceState
binding.transactionsMotionLayout.saveState(outState, MOTION_LAYOUT_STATE_KEY)
This resulted in the expected behavior for both MotionLayouts
in Activity
s, and MotionLayouts
inside Fragment
s. But I'm not happy with the amount of code required, so if anyone could suggest a cleaner solution, I would be really happy to hear that :)
I don't see why you haven't done that but you need to extend MotionLayout
to properly save the view state.
Your current solution (aside from requiring extra handling in activity and fragment layer) will fail in case of detachable fragments because they handle views save state internally without actually populating savedInstanceState
(quite confusing I know).
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
}
You will need to use this class in your XML instead of MotionLayout
however its less prone to errors and will respect proper view state saving mechanisms so you do not need to add any extra code to activities or fragments anymore.
If you want to disable saving you can do it with android:saveEnabled="false"
in XML or isSaveEnabled = false
in code.
这篇关于如何在不自动播放过渡的情况下恢复MotionLayout的过渡状态?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!