Как восстановить переходное состояние 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 в Activitys, так и для MotionLayouts внутри Fragments. Но меня не устраивает объем необходимого кода, поэтому, если кто-нибудь может предложить более чистое решение, я был бы очень рад это услышать :)


person A. Patrik    schedule 28.11.2020    source источник


Ответы (2)


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

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

person Pawel    schedule 15.03.2021

MotionLayout имеет методы:

Bundle getTransitionState()

а также

void setTransitionState(Bundle bundle) 

Возвращенный и установленный комплект содержит Progress, Velocity, startState и endState.

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

person hoford    schedule 16.03.2021
comment
Как вы можете видеть в моем коде действий, я делаю именно то, что вы сказали, и, как вы можете видеть на гифке, это не работает в простейших случаях. - person A. Patrik; 17.03.2021