» » » MotionLayout: анимации лучше, кода — меньше

 

MotionLayout: анимации лучше, кода — меньше

Автор: admin от 4-07-2019, 19:40, посмотрело: 148

MotionLayout: анимации лучше, кода — меньше
Google продолжает улучшать нашу жизнь, выпуская новые удобные библиотеки и API. Среди которых оказался и новый MotionLayout. Учитывая обилие анимаций в наших приложениях, мой коллега Cedric Holtz сразу же реализовал важнейшую анимацию нашего приложения — голосование в знакомствах — с использованием нового API, сэкономив при этом огромное количество кода. Делюсь переводом его статьи. 



Недавно закончилась конференция Google I/O 2019, на которой анонсировали обновления и самые свежие улучшения нашего любимого SDK. Лично мне особенно интересна была презентация Николаса Роарда и Джона Хофорда о будущей функциональности ConstraintLayout. А точнее, о его расширении в виде MotionLayout. 



После выпуска бета-версии мне захотелось реализовать анимацию знакомств на основе этой библиотеки.Документация[/quote]



Если вы ещё не читали серию статей Николаса Роарда, в которой объясняются ключевые идеи MotionLayout, то очень рекомендую прочитать.



Итак, с введением закончили, теперь давайте посмотрим, что мы хотим получить:



MotionLayout: анимации лучше, кода — меньше


Стек карт



Показываем сдвигаемую карту



Начнём с того, что в директорию лэйаутов добавим MotionLayout, который пока что содержит только одну верхнюю карту:



[leech=https://github.com/calimbak/SwipeRightOnMotionLayout]репозитории[/leech].



[code]<ConstraintSet android:id="@+id/offScreenLike">

    <Constraint
        android:id="@id/topCard"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_marginBottom="80dp"
        android:layout_marginEnd="50dp"
        android:layout_marginTop="20dp"
        app:layout_constraintStart_toEndOf="parent"
        app:layout_constraintWidth_percent="0.7" />
    
</ConstraintSet>




Теперь, как и в предыдущем примере, нужно определить переход от состояния свайпа к конечному состоянию. Переход должен автоматически срабатывать сразу после завершения анимации свайпа. Сделать это можно с помощью autoTransition:



<Transition
    app:autoTransition="animateToEnd"
    app:constraintSetEnd="@+id/offScreenLike"
    app:constraintSetStart="@+id/like"
    app:duration="150" />




Теперь у нас есть свайпабельная карта, которую можно свайпнуть с экрана!



MotionLayout: анимации лучше, кода — меньше


Анимация нижней карты





Теперь сделаем нижнюю карту, чтобы создать иллюзию бесконечности колоды.



Добавим в лэйаут ещё одну карту, аналогичную первой:



<FrameLayout
    android:id="@+id/bottomCard"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:background="@color/colorAccent" />




Изменим XML, чтобы задать ограничения, которые применяются к этой карте на каждом этапе анимации:



<ConstraintSet android:id="@id/rest">

    <!-- ... -

    <Constraint android:id="@id/bottomCard">

        <Layout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="50dp"
            android:layout_marginEnd="50dp"
            android:layout_marginStart="50dp"
            android:layout_marginTop="50dp" />

        <Transform
            android:scaleX="0.90"
            android:scaleY="0.90" />

    </Constraint>

</ConstraintSet>

<ConstraintSet
    android:id="@+id/offScreenLike"
    app:deriveConstraintsFrom="@id/like">

    <!-- ... -

    <Constraint android:id="@id/bottomCard">

        <Transform
            android:scaleX="1"
            android:scaleY="1" />

    </Constraint>

</ConstraintSet>




Для этого мы можем воспользоваться удобным свойством ConstraintSet. 



По умолчанию, каждый новый набор берёт атрибуты из родительского MotionLayout. Но с помощью флага deriveConstraintsFrom можно задать для нашего набора другого родителя. Стоит иметь в виду, что если мы задаем ограничения с помощью тега constraint, то тем самым переопределяем все ограничения из родительского набора. Чтобы этого избежать, можно задать в тегах конкретные атрибуты, чтобы замещались лишь они.



MotionLayout: анимации лучше, кода — меньше


В нашем случае это означает, что в наборе pass мы не определяем тег Layout, а копируем из родителя. Однако мы переопределяем Transform, поэтому поэтому заменяем все атрибуты, заданные в теге Transform, нашими собственными, в данном случае — изменением масштаба.



Вот так легко можно с помощью MotionLayout добавить новый элемент и бесшовно интегрировать его с анимациями нашей сцены.



MotionLayout: анимации лучше, кода — меньше


Делаем анимацию бесконечной





После завершения анимации верхнюю карту уже нельзя смахнуть, потому что теперь она стала нижней картой. Чтобы получилась бесконечная анимация, нужно менять карты местами. 



Сначала я хотел сделать это с помощью нового перехода:



<Transition
    app:autoTransition="jumpToEnd"
    app:constraintSetEnd="@+id/rest"
    app:constraintSetStart="@+id/offScreenLike"
    app:duration="0" />




MotionLayout: анимации лучше, кода — меньше


Анимация целиком проигрывается так, как нужно. Теперь у нас есть стек карт, которые можно бесконечно свайпить!



Посвайпив немного, я кое-что заметил. Анимация перехода к концу колоды останавливается, если коснуться карты. Даже при том, что длительность анимации нулевая, всё равно происходит остановка, а это плохо. 



MotionLayout: анимации лучше, кода — меньше


Мне удалось победить только одним способом — программно изменив активный переход в MotionLayout.



Для этого мы зададим коллбэк по завершению анимации. Как только завершаются offScreenLike и offScreenPass, мы просто сбрасываем переход обратно на состояние rest и обнуляем прогресс.



motionLayout.setTransitionListener(object : TransitionAdapter() {

    override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {
        when (currentId) {
            R.id.offScreenPass,
            R.id.offScreenLike  {
                motionLayout.progress = 0f
                motionLayout.setTransition(R.id.rest, R.id.like)
            }
        }
    }
    
})




Не имеет значения, какой переход мы задали, pass или like, при свайпе мы переключаемся на нужный.



MotionLayout: анимации лучше, кода — меньше


Выглядит так же, но анимация не останавливается! Идём дальше!



Привязка (биндинг) данных



Создадим тестовые данные для отображения на картах. Пока что ограничимся изменением фонового цвета у каждой карты.



Мы создаем ViewModel со свайп-методом, который всего лишь подставляет новые данные. Биндим её в Activity таким образом:



val viewModel = ViewModelProviders
    .of(this)
    .get(SwipeRightViewModel::class.java)

viewModel
    .modelStream
    .observe(this, Observer {
        bindCard(it)
    })

motionLayout.setTransitionListener(object : TransitionAdapter() {

    override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {
        when (currentId) {
            R.id.offScreenPass,
            R.id.offScreenLike  {
                motionLayout.progress = 0f
                motionLayout.setTransition(R.id.rest, R.id.like)
                viewModel.swipe()
            }
        }
    }

})




Осталось сообщить ViewModel о завершении анимации свайпа, и она обновит данные, которые отображаются в текущий момент.



MotionLayout: анимации лучше, кода — меньше


Всплывающие иконки





Добавим две вьюхи, которые при свайпе появляются с одной из сторон экрана (ниже показана только одна, вторая делается зеркально).



<ImageView
    android:id="@+id/likeIndicator"
    android:layout_width="0dp"
    android:layout_height="0dp" />




Теперь для карт нужно задать состояния анимации с этим вьюхами.



<ConstraintSet android:id="@id/rest">

    <!-- ... -

    <Constraint android:id="@+id/like">

        <Layout
            android:layout_width="40dp"
            android:layout_height="40dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Transform
            android:scaleX="0.5"
            android:scaleY="0.5" />

        <PropertySet android:alpha="0" />

    </Constraint>
    
</ConstraintSet>

<ConstraintSet
    android:id="@+id/like"
    app:deriveConstraintsFrom="@id/rest">

    <!-- ... -

    <Constraint android:id="@+id/like">

        <Layout
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:layout_constraintBottom_toBottomOf="@id/topCard"
            app:layout_constraintEnd_toEndOf="@id/topCard"
            app:layout_constraintStart_toStartOf="@id/topCard"
            app:layout_constraintTop_toTopOf="@id/topCard" />

        <Transform
            android:scaleX="1"
            android:scaleY="1" />

        <PropertySet android:alpha="1" />

    </Constraint>

</ConstraintSet>




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



Это всё, что нам нужно сделать. Теперь можно очень легко добавлять компоненты в цепочки анимаций.



MotionLayout: анимации лучше, кода — меньше


Запускаем анимацию программно





Можем сделать на картах две кнопки, чтобы пользователь мог не только свайпить, но управлять с помощью кнопок.



Каждая кнопка запускает ту же анимацию, что и свайп.



Как обычно, подписываемся на клики кнопок и запустим анимацию прямо на объекте MotionLayout:



likeButton.setonclickListener {
    motionLayout.transitionToState(R.id.like)
}
passButton.setonclickListener {
    motionLayout.transitionToState(R.id.pass)
}




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



MotionLayout: анимации лучше, кода — меньше


Ещё один замечательный пример того, как MotionLayout обрабатывает для нас изменения состояний. Давайте слегка замедлим анимацию:



MotionLayout: анимации лучше, кода — меньше


Посмотрите на переход, который выполняет MotionLayout, когда pass сменяет like. Магия!



Свайпим карту по кривой



Допустим, нам нравится, если карта будет двигаться не по прямой, а по кривой (честно говоря, мне просто хотелось попробовать так сделать).



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



Добавим это в сцену движения:



<Transition
    app:constraintSetEnd="@+id/like"
    app:constraintSetStart="@+id/rest"
    app:duration="300">

    <!-- ... -

    <KeyFrameSet>

        <KeyPosition
            app:drawPath="path"
            app:framePosition="50"
            app:keyPositionType="pathRelative"
            app:motionTarget="@id/topCard"
            app:percentX="0.5"
            app:percentY="-0.1" />
        
    </KeyFrameSet>

</Transition>




MotionLayout: анимации лучше, кода — меньше


Теперь карта движется по небанальной изогнутой траектории. Волшебно!



Заключение



Когда сравниваешь объём кода, получившийся у меня при создании этих анимаций, с нашей текущей реализацией похожей анимации в продакшне, результат ошеломляет. 



MotionLayout незаметно обрабатывает отмену переходов (например, при касании), создание цепочек анимаций, изменения свойства при переходах и многое другое. Этот инструмент в корне всё меняет, значительно упрощая UI-логику. 



Есть еще некоторые вещи, над которыми стоит поработать (в основном, отключение анимаций и двунаправленный скроллинг в RecyclerView), но уверен, что это решаемо.



Помните, что библиотека ещё находится в статусе беты, но она уже открывает для нас много захватывающих возможностей. С нетерпением ждем релиза MotionLayout, который, я уверен, еще не раз пригодится нам в будущем. Полностью работающее приложение из этой статьи вы можете посмотреть в репозитории.



P.S.: и раз уж мне как переводчику предоставили слово — в нашей Android-команде есть место разработчика. Спасибо за внимание. 

Источник: Хабр / Интересные публикации

Категория: Game Development, Android

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *