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>
The motion layout stays at its start state after configuration change
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.