Как восстановить переходное состояние MotionLayout без автоматического воспроизведения перехода?

#android #android-animation #android-motionlayout

#Android #android-анимация #android-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>
 

Наблюдаемое поведение

введите описание изображения здесь

Ожидаемое поведение

Макет движения остается в начальном состоянии после изменения конфигурации

Редактировать (хакерское решение)

В итоге я создал эти функции расширения

   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 s, так и MotionLayouts внутри Fragment s. Но я недоволен количеством требуемого кода, поэтому, если кто-нибудь может предложить более чистое решение, я был бы очень рад это услышать 🙂

Ответ №1:

Я не понимаю, почему вы этого не сделали, но вам нужно расширить MotionLayout , чтобы правильно сохранить состояние просмотра.

Ваше текущее решение (помимо необходимости дополнительной обработки в activity и fragment layer) потерпит неудачу в случае съемных фрагментов, потому что они обрабатывают представления, сохраняют состояние внутри, фактически не заполняя 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 того, чтобы, однако, он менее подвержен ошибкам и будет соблюдать надлежащие механизмы сохранения состояния просмотра, поэтому вам больше не нужно добавлять дополнительный код к действиям или фрагментам.

Если вы хотите отключить сохранение, вы можете сделать это с android:saveEnabled="false" помощью XML или isSaveEnabled = false кода.

Комментарии:

1. Сработало как шарм, спасибо!

2. Не забудьте определить идентификатор представления для макета движения, если у макета движения нет идентификатора, сохранение и восстановление не будут работать.

Ответ №2:

У MotionLayout есть методы:

 Bundle getTransitionState()
 

и

 void setTransitionState(Bundle bundle) 
 

Возвращенный и установленный пакет содержит
Прогресс, скорость, начальное состояние и конечное состояние.

Для простых MotionLayouts это можно использовать. Возможно иметь более сложные MotionLayouts, где этой информации недостаточно.

Комментарии:

1. Ну, как вы можете видеть в моем коде активности, я делаю именно то, что вы сказали, и, как вы можете видеть в gif, это не работает для простейших случаев.