Android Animations

автор:

Распутина Татьяна

Что такое анимация?

Воспроизведение кадров во времени с некоторой трансформацией и сглаживанием (рендеринг)

  • Решают проблему недостаточной интерактивности UI
  • Позволяют фокусировать внимание
  • Обладают силой убеждения
  • Улучшают понимание навигации
  • Подсвечивают реакции на пользовательский ввод и нажатие на элементы
  • ...

Какое время отрисовки кадра?

  • 10-12 fps - четкое движение, глазу видно отдельные кадры
  • 24 fps - плавное движение, размытие движений - отдельные кадры сливаются
  • 30 fps - анимации похожие на "живые", могут выглядеть не убедительно
  • 60 fps - идеальная анимация для глаза, высококачественное плавное движение
  • 1000 ms / 60 frames = 16.666 ms/frame
  • Если приложение отрисовывает кадр дольше 16ms, то появляется dropped frame - Jank
  • Hitching, Lag, Stutter, Jank: глаз отлавливает дискретность в анимациях: если один кадр тормозит, пользователь сразу заметит

Overview

  • View Animation
  • Drawable Animation
  • ValueAnimator
  • ObjectAnimator
  • ViewPropertyAnimator
  • Layout Transition
  • Transitions Framework
  • Dynamic Animation

View Animation

  • Анимации через Animation были в Android до появления Animator
  • Простый анимации для View: alpha, rotate, scale, translate
  • Представление View меняется на этапе отрисовки
  • Важно: параметры View не меняются в процессе анимаций

Code ScaleAnimation:


                    view.startAnimation(
                        ScaleAnimation(0f, 1f, 0f, 1f, 0f, view.height.toFloat())
                            .apply {
                                duration = 300
                                interpolator = AccelerateInterpolator()
                                fillAfter = true
                            }
                    )

                    view.startAnimation(
                        AnimationUtils.loadAnimation(context, R.anim.pulse)
                    )
                

XML pulse animation:


                    <scale xmlns:android="http://schemas.android.com/apk/res/android"
                        android:duration="@integer/animation_time_1500"
                        android:fromXScale="1"
                        android:fromYScale="1"
                        android:pivotX="50%"
                        android:pivotY="50%"
                        android:repeatCount="infinite"
                        android:repeatMode="reverse"
                        android:toXScale="0.9"
                        android:toYScale="0.9"/>
                

XML shake animation:


                    <set xmlns:android="http://schemas.android.com/apk/res/android"
                         android:interpolator="@android:anim/linear_interpolator">

                        <translate
                                android:duration="@integer/animation_time_50"
                                android:fromXDelta="-5"
                                android:repeatCount="2"
                                android:repeatMode="reverse"
                                android:toXDelta="5"/>
                    </set>
                

Drawable Animation

  • Покадровая анимация: новый Drawable загружается на каждый кадр
  • Важно: большое потребление ресурсов, возможность получить OutOfMemoryException

                    <animation-list android:id="@+id/icon" android:oneshot="false">
                        <item android:drawable="@drawable/icon0" android:duration="@integer/animation_fast" />
                        <item android:drawable="@drawable/icon1" android:duration="@integer/animation_fast" />
                        <item android:drawable="@drawable/icon2" android:duration="@integer/animation_fast" />
                        <item android:drawable="@drawable/icon3" android:duration="@integer/animation_fast" />
                        <item android:drawable="@drawable/icon4" android:duration="@integer/animation_fast" />
                    </animation-list>
                

ValueAnimator

  • Позволяет анимировать любое свойство через Integer, Float или Object
  • Свойство меняется программно внутри UpdateListener
  • Не привязан к жизненному циклу View
  • Есть собственная иерархия вызовов в процессе формирования кадров
  • Можно одновременно анимировать несколько View элементов

                    val animationDuration = resources.getInteger(R.integer.animation_time_100).toLong()
                    val animator = ValueAnimator.ofInt(start, end)
                        .apply {
                            duration = animationDuration
                            repeatCount = 1
                            repeatMode = ValueAnimator.REVERSE
                            addUpdateListener { animation ->
                                view.setPadding(
                                    view.paddingLeft,
                                    animation.animatedValue as Int,
                                    view.paddingRight,
                                    view.paddingBottom
                                )
                            }
                            addListener(object : Animator.AnimatorListener {
                                override fun onAnimationStart(animator: Animator?) { }
                                override fun onAnimationEnd(animator: Animator?) { }
                                override fun onAnimationCancel(animator: Animator?) { }
                                override fun onAnimationRepeat(animator: Animator?) { }
                            })
                        }
                        .also { it.start() }
                

ValueAnimator

  • Время переводится в интервал [0.0, 1.0]
  • TimeInterpolator - функция, которая определяет с какой скоростью происходит анимация
  • TypeEvaluator - как должен изменяться объект в процессе анимации относительно времени
  • Можно писать свои реализации для TimeInterpolator и TypeEvaluator
  • TypeEvaluator используется для изменения сложных объектов или для изменения примитивов (RectEvaluator, ArgbEvaluator)

TimeInterpolator

  • LinearInterpolator - функция с постоянной скоростью или линейная анимация
  • AccelerateInterpolator - функция с увеличивающейся скоростью (для пропадающих элементов)
  • DecelerateInterpolator - функция с уменьшающейся скоростью (для появляющихся элементов)
  • AccelerateDecelerateInterpolator

Material Interpolators - более плавные

  • FastOutLinearInInterpolator
  • LinearOutSlowInInterpolator
  • FastOutSlowInInterpolator

ObjectAnimator

  • Расширение ValueAnimator, имеет упрощенное API
  • Можно указать конкретные свойства объекта, которые ObjectAnimator меняет автоматически
  • Значение Property может быть задано с помощью наследника Property или явно строкой
  • Не может анимировать два объекта одновременно: нужны разные экземпляры ObjectAnimator
  • Важно: задание Property через строку вызывает методы Reflection!

Property:

  • View.ALPHA
  • View.TRANSLATION_X (_Y / _Z)
  • View.X / View.Y / View.Z
  • View.ROTATION
  • View.SCALE
  • Можно создать свои Property!

ObjectAnimator

XML:


                    <set xmlns:android="http://schemas.android.com/apk/res/android"
                         android:ordering="sequentially">
                        <objectAnimator
                            android:propertyName="alpha"
                            android:duration="500"
                            android:valueFrom="0f"
                            android:valueTo="1f"/>
                        <objectAnimator
                            android:propertyName="rotation"
                            android:duration="500"
                            android:valueType="floatType"
                            android:valueFrom="0f"
                            android:valueTo="90f" />
                    </set>
                

                    val animator = AnimatorInflater
                        .loadAnimator(context, R.animator.animator_set)
                        .let { it as AnimatorSet }
                        .apply { setTarget(view) }
                        .also { it.start() }
                

Code:


                    val pulseAnimator = ObjectAnimator
                        .ofPropertyValuesHolder(
                            view,
                            PropertyValuesHolder.ofFloat(View.SCALE_X, 1f),
                            PropertyValuesHolder.ofFloat(View.SCALE_Y, 1f)
                        )
                        .apply {
                            duration = resources.getInteger(R.integer.animation_time_500).toLong()
                            repeatCount = ValueAnimator.INFINITE
                            repeatMode = ValueAnimator.REVERSE
                            setAutoCancel(true)
                        }
                        .also { it.start() }
                

                    val start = view.translationY
                    val end = start + update
                    val animator = ObjectAnimator.ofFloat(view, "translationY", start, end)
                        .apply { duration = resources.getInteger(R.integer.animation_time_200).toLong() }
                        .also { it.start() }
                

AnimatorSet

  • Позволяет воспроизводить несколько анимаций: одновременно или последовательно
  • play(animator1) - передается главный аниматор
  • with(animator2) - для воспроизведения одновременно с аниматором в play(animator1)
  • before(animator2) - для воспроизведения до
  • after(animator2) - для воспроизведения после
  • playTogether(animator1, animator2) - для воспроизведения одновременно
  • playSequentially(animator1, animator2) - для воспроизведения последовательно

Code:


                    val animator1 = AnimatorInflater.loadAnimator(context, R.animator.animator_1)
                    val animator2 = AnimatorInflater.loadAnimator(context, R.animator.animator_2)
                    val animator3 = AnimatorInflater.loadAnimator(context, R.animator.animator_3)
                    val animator4 = AnimatorInflater.loadAnimator(context, R.animator.animator_4)

                    val animator = AnimatorSet()
                        .apply {
                            play(animator1)
                                .before(animator2)
                                .with(animator3)
                                .after(animator4)
                        }
                        .also { it.start() }
                

ViewPropertyAnimator

  • Работает на основе ValueAnimator, удобный API для простых свойств
  • Может быть быстрее ObjectAnimator (если аниминуется несколько Property)
  • Набор Property ограничен
  • Важно: не можем сделать анимации кастомных атрибутов

                    view.animate().alpha(0f).alphaBy(1f).start()
                

                    view.animate().x(500f).y(500f)
                    // animate absolute position
                    // was x=100, y=100
                    // start1: x=500, y=500
                    // start2: x=500, y=500
                

                    view.animate().xBy(500f).yBy(500f)
                    // animate absolute position by value
                    // was x=100, y=100
                    // start1: x=100+500, y=100+500
                    // start2: x=100+500+500, y=100+500+500
                

                    view.animate().translationX(500f).translationY(500f)
                    // animate left-top position by value
                    // was x=100, y=100
                    // start1: x=100+500, y=100+500
                    // start2: x=100+500, y=100+500
                

                    view.animate().translationXBy(500f).translationYBy(500f)
                    // animate left-top position by value
                    // was x=100, y=100
                    // start1: x=100+500, y=100+500
                    // start2: x=100+500+500, y=100+500+500
                

Как завершить анимации?

  • view.clearAnimation() - для View Animation
  • view.animate().cancel() - для ViewPropertyAnimator
  • animator.cancel() - ValueAnimator / ObjectAnimator
  • Важно: при уходе с экрана может быть утечка Context - нужно останавливать аниматоры

Layout Transition

  • Работает на основе ValueAnimator
  • Флаг в XML: android:animateLayoutChanges="true"
  • Запускает анимацию на изменение положения, видимости или размера View внутри ViewGroup
  • Решает проблему временного прерывания анимации из-за потери фреймов
  • Анимирует прямых детей ViewGroup
  • При анимации сложной иерархии может привести к laggy behavior из-за флага animateParentHierarchy
  • При анимации нескольких изменений будет применяться только для последнего изменения
  • Appearing (addView / setVisibility)
  • Disappearing (removeView / setVisibility)
  • Change appearing - изменение родителей при Appearing
  • Change disappearing - изменение родителей при Disappearing
  • Changing - анимирует изменение размеров

Transitions Framework

  • Вводится понятие Transition и Scene
  • Transition - инкапсулирует класс анимаций
  • Scene - состояние до и после анимации
  • Анимирует всю иерархию View
  • Решает проблему временного прерывания анимации из-за потери фреймов
  • Появляется вместе с Material Design (Android 4.4+)
  • Можно делать свои Transition
  • Можно использовать для сложных иерархий View

Android 4.4+:

  • ChangeBounds
  • Fade

Android 5.+:

  • ChangeTransform
  • Explode
  • Slide
  • ChangeImageTransform
  • ChangeClipBounds

Android 6.+:

  • ChangeScroll

Transitions Framework: XML


                    <transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
                        android:transitionOrdering="sequential">
                        <fade android:fadingMode="fade_out">
                            <targets>
                                <target android:targetId="@id/view" />
                            </targets>
                        </fade>
                        <changeBounds/>
                        <fade android:fadingMode="fade_in"/>
                    </transitionSet>
                

                    TransitionManager.beginDelayedTransition(
                        viewGroup,
                        TransitionInflater.from(viewGroup.context).inflateTransition(R.transition.transition_set)
                    )
                    val newSize = resources.getDimensionPixelSize(R.dimen.size_large)
                    view.layoutParams = view.layoutParams
                        .apply {
                            width = newSize
                            height = newSize
                        }
                

Transitions Framework: Code


                    val transitions = TransitionSet()
                        .apply {
                            addTransition(Slide())
                            addTransition(ChangeBounds())
                            addTarget(R.id.view)
                            addListener(object : Transition.TransitionListener {
                                override fun onTransitionStart(transition: Transition?) {}
                                override fun onTransitionEnd(transition: Transition?) {}
                                override fun onTransitionResume(transition: Transition?) {}
                                override fun onTransitionPause(transition: Transition?) {}
                                override fun onTransitionCancel(transition: Transition?) {}
                            })
                            ordering = TransitionSet.ORDERING_TOGETHER // ORDERING_SEQUENTIAL
                            duration = resources.getInteger(R.integer.animation_time_500).toLong()
                            interpolator = AccelerateInterpolator()
                        }

                    TransitionManager.beginDelayedTransition(viewGroup, transitions)
                

Transitions Framework: Scene

res/layout/view_root.xml


                    <FrameLayout
                            android:id="@+id/viewGroup"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent">

                        <include layout="@layout/view_scene1"/>

                    </FrameLayout>
                

Code:


                    val scene2: Scene = Scene
                        .getSceneForLayout(viewGroup, R.layout.view_scene2, this)

                    viewGroup.setOnClickListener {
                        val transitionSet = TransitionSet()
                            .apply {
                                addTransition(Fade())
                                addTransition(ChangeBounds())
                                addTransition(ChangeImageTransform())
                                ordering = TransitionSet.ORDERING_TOGETHER
                                duration = 1000L
                                interpolator = AccelerateInterpolator()
                            }
                        TransitionManager.go(scene2, transitionSet)
                    }
                

res/layout/view_scene1.xml


                    <FrameLayout
                        android:id="@+id/viewGroupScene1"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent">

                        <ImageView
                            android:id="@+id/targetView"
                            android:layout_width="@dimen/target_width_scene1"
                            android:layout_height="@dimen/target_height_scene1"
                            android:src="@drawable/ic_image"
                            android:scaleType="centerCrop"
                            android:layout_gravity="top|center_horizontal"/>

                    </FrameLayout>
                

res/layout/view_scene2.xml


                    <FrameLayout
                        android:id="@+id/viewGroupScene2"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent">

                        <ImageView
                            android:id="@+id/targetView"
                            android:layout_width="@dimen/target_width_scene2"
                            android:layout_height="@dimen/target_height_scene2"
                            android:src="@drawable/ic_image"
                            android:scaleType="fitXY"
                            android:layout_gravity="bottom|center_horizontal"/>

                    </FrameLayout>
                

Dynamic Animation

  • Базируется на законах физики
  • FlingAnimation - пользователь своими действиями инициирует анимацию
  • SpringAnimation - анимация возврата к начальному значению
  • Позволяет изменить конечное значение во время анимации
  • Нет: duration
  • Нет: interpolator
  • Есть: физика (конечная позиция и начальная скорость)
  • FlingAnimation доп. параметр: трение (friction)
  • SpringAnimation доп. параметр: жёсткость (stiffness) & затухание (damping ratio)

                    val flingAnimation = FlingAnimation(view, DynamicAnimation.X)
                        .apply {
                            setStartVelocity(500f)
                            friction = 0.5f
                        }
                        .also { it.start() }
                

                    val springAnimation = SpringAnimation(view, DynamicAnimation.SCALE_X)
                        .apply {
                            spring = SpringForce()
                                .apply {
                                    finalPosition = view.x
                                    dampingRatio = SpringForce.DAMPING_RATIO_HIGH_BOUNCY
                                    stiffness = SpringForce.STIFFNESS_LOW
                                }
                            setStartVelocity(1000f)
                        }
                        .also { it.start() }
                

RecyclerView - ItemAnimator

  • Анимация элементов внутри RecyclerView
  • canReuseUpdatedViewHolder(vh: ViewHolder) - определяет будет ли анимация вызываться после изменения данных элемента
  • recordPreLayoutInformation(...): ItemHolderInfo - вызывается RecyclerView до начала отрисовки
  • ItemAnimator сохраняет информацию о View до перемещения, обновления или удаления
  • ItemHolderInfo передается в метод animateChange(oldVH: ViewHolder, newVH: ViewHolder, preInfo: ItemHolderInfo, postInfo: ItemHolderInfo): Boolean
  • RecyclerView вызывает animateChange(...) при notifyItemChanged(position: Int)

AnimatedVectorDrawable

  • Расширение PropertyAnimation
  • Ресурс анимации - это стандартный objectAnimator
  • Количество командв исходном и конечном пути должно быть одинаковым

                    <animated-vector
                        android:drawable="@drawable/vector_drawable">

                        <target
                            android:name="start"
                            android:animation="@anim/start_animation" />

                       <target
                            android:name="end"
                            android:animation="@anim/end_animation"/>

                    </animated-vector>
                

Activity / Fragment Transition

Activity Shared Element Transition

  1. Добавить атрибут android:transitionName к View
  2. Описать анимацию перехода View через XML: transitionSet
  3. Передать ActivityOptions для второй Activity при запуске
  4. Объявить анимации в темах Activity

In res/values/theme.xml:


                    <item name="android:windowContentTransitions">true</item>

                    <item name="android:windowEnterTransition">@transition/transition_fade</item>
                    <item name="android:windowExitTransition">@transition/transition_fade</item>

                    <item name="android:windowSharedElementEnterTransition">@transition/transition_fade</item>
                    <item name="android:windowSharedElementExitTransition">@transition/transition_fade</item>
                

In res/layout/activity_layout.xml


                    <ImageView
                        android:id="@+id/transitionView"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:transitionName="transitionViewName"
                        ... />
                

In ActivityStart.kt launch transition:


                    val activityOptionsCompat: ActivityOptionsCompat = ActivityOptionsCompat
                        .makeSceneTransitionAnimation(this, transitionView, "transitionViewName")
                    val intent = Intent(this, ActivityEnd::class.java)
                    startActivity(intent, activityOptionsCompat.toBundle())
                

Fragment Shared Element Transition

  1. Добавить атрибут android:transitionName к View
  2. Описать анимацию перехода View через XML: transitionSet
  3. Описать для второго фрагмента анимации через сеттеры
  4. Добавить SharedElement к FragmentTransaction

In res/layout/activity_layout.xml


                    <ImageView
                        android:id="@+id/transitionView"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:transitionName="transitionViewName"
                        ... />
                

In StartFragment.kt:


                    val endFragment = EndFragment.newInstance()

                    // possibly need to check: Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
                    endFragment.setSharedElementEnterTransition(ChangeImageTransform())
                    endFragment.setSharedElementReturnTransition(ChangeImageTransform())
                    endFragment.setEnterTransition(Slide())
                    this.setExitTransition(Slide())

                    activity.supportFragmentManager
                        .beginTransaction()
                        .addSharedElement(transitionView, "transitionViewName")
                        .replace(R.id.container, endFragment)
                        .addToBackStack(null)
                        .commit()
                

DrawerLayout - Google I/O 2013


                    <androidx.drawerlayout.widget.DrawerLayout
                        xmlns:android="http://schemas.android.com/apk/res/android"
                        xmlns:tools="http://schemas.android.com/tools"
                        android:id="@+id/drawerLayout"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent">

                        <FrameLayout
                            android:id="@+id/container"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"/>

                        <fragment
                            android:id="@+id/drawerMenu"
                            android:name="com.intership.example.DrawerMenuFragment"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:layout_gravity="start"
                            tools:layout="@layout/fragment_drawer_menu"/>

                    </androidx.drawerlayout.widget.DrawerLayout>
                

DrawerLayout


                    drawerLayout.openDrawer(drawerMenu)
                    drawerLayout.closeDrawer(drawerMenu)
                    drawerLayout.openDrawer(GravityCompat.START)
                    drawerLayout.closeDrawer(GravityCompat.START)

                    // проверить текущее состояние Drawer
                    drawerLayout.isDrawerOpen(drawerMenu)
                    drawerLayout.isDrawerOpen(GravityCompat.START)
                    // разблокировать для пользователя
                    drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
                    // заблокировать в закрытом состоянии
                    drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
                    // заблокировать в открытом состоянии
                    drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN)
                    // заблокировать в состоянии по умолчанию
                    drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED)
                

                    drawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener {

                        override fun onDrawerStateChanged(newState: Int) {
                            // DrawerLayout.STATE_IDLE
                            // DrawerLayout.STATE_DRAGGING
                            // DrawerLayout.STATE_SETTLING
                        }
                        override fun onDrawerSlide(drawerView: View, slideOffset: Float) {}
                        override fun onDrawerClosed(drawerView: View) {}
                        override fun onDrawerOpened(drawerView: View) {}
                    })
                

CoordinatorLayout - Google I/O 2015


                    <androidx.coordinatorlayout.widget.CoordinatorLayout
                        xmlns:android="http://schemas.android.com/apk/res/android"
                        xmlns:app="http://schemas.android.com/apk/res-auto"
                        android:id="@+id/coordinatorLayout"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent">

                        <com.google.android.material.appbar.AppBarLayout
                            android:id="@+id/appBarLayout"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:fitsSystemWindows="true"
                            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

                            <com.google.android.material.appbar.CollapsingToolbarLayout
                                android:id="@+id/collapsingToolbarLayout"
                                android:layout_width="match_parent"
                                android:layout_height="match_parent"
                                android:fitsSystemWindows="true"
                                app:contentScrim="?attr/colorPrimary"
                                app:layout_scrollFlags="scroll|exitUntilCollapsed">

                                <androidx.constraintlayout.widget.ConstraintLayout
                                    android:id="@+id/toolbarLayout"
                                    android:layout_width="match_parent"
                                    android:layout_height="match_parent"
                                    android:fitsSystemWindows="true"
                                    app:layout_collapseMode="parallax">

                                    <ImageView
                                        android:id="@+id/imageView"
                                        android:layout_width="0dp"
                                        android:layout_height="wrap_content"
                                        android:fitsSystemWindows="true"
                                        app:layout_constraintDimensionRatio="1:1"
                                        app:layout_constraintStart_toStartOf="parent"
                                        app:layout_constraintEnd_toEndOf="parent"
                                        app:layout_constraintTop_toTopOf="parent"/>
                                </androidx.constraintlayout.widget.ConstraintLayout>

                                <androidx.appcompat.widget.Toolbar
                                    android:id="@+id/toolbar"
                                    android:layout_width="match_parent"
                                    android:layout_height="?attr/actionBarSize"
                                    app:layout_collapseMode="pin"
                                    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

                            </com.google.android.material.appbar.CollapsingToolbarLayout>
                        </com.google.android.material.appbar.AppBarLayout>

                        <FrameLayout
                            android:id="@+id/container"
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

                        <com.google.android.material.floatingactionbutton.FloatingActionButton
                            android:id="@+id/favorite_fab"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            app:fabSize="normal"
                            app:layout_anchor="@id/appBarLayout"
                            app:layout_anchorGravity="bottom|end"
                            app:layout_behavior="@string/appbar_scrolling_view_behavior"
                            android:src="@drawable/ic_fab_image"/>

                    </androidx.coordinatorlayout.widget.CoordinatorLayout>
                

MotionLayout - Google I/O 2018

A MotionLayout is a ConstraintLayout which allows you to animate layouts between various states. (© Docs)

  • Animated Vector Drawable
  • Property Animation Framework
  • LayoutTransition animations: TransitionManager
  • CoordinatorLayout

Обратная совместимость: Android API >= 14 (IceCreamSandwich 4.0, 4.0.1, 4.0.2)

Полностью декларативный: можно описать сцены любой сложности в XML

MotionLayout: overview

MotionLayout: expandable top card example


                    <androidx.constraintlayout.motion.widget.MotionLayout
                        xmlns:android="http://schemas.android.com/apk/res/android"
                        xmlns:app="http://schemas.android.com/apk/res-auto"
                        xmlns:tools="http://schemas.android.com/tools"
                        android:id="@+id/motionLayout"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        app:layoutDescription="@xml/scene_main"
                        app:applyMotionScene="true"
                        app:progress="0.0"
                        app:currentState="@id/start"
                        app:motionDebug="SHOW_ALL"
                        tools:showPaths="true">

                        <View
                            android:id="@+id/cardBackgroundView"
                            android:layout_width="0dp"
                            android:layout_height="0dp"
                            android:background="@drawable/shape_white_large_cornered_bottom"
                            app:layout_constraintStart_toStartOf="parent"
                            app:layout_constraintEnd_toEndOf="parent"
                            app:layout_constraintBottom_toBottomOf="@id/cardLayoutGuideline"
                            app:layout_constraintTop_toTopOf="parent"/>

                        <androidx.constraintlayout.widget.Guideline
                            android:id="@+id/cardLayoutGuideline"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:orientation="horizontal"/>
                        <View android:id="@+id/view" ... />
                    </androidx.constraintlayout.motion.widget.MotionLayout>
                

                    <MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
                         xmlns:motion="http://schemas.android.com/apk/res-auto">

                        <Transition
                            motion:constraintSetStart="@id/start"
                            motion:constraintSetEnd="@id/end"
                            motion:motionInterpolator="linear"
                            motion:duration="1000">
                            <OnSwipe
                                motion:touchAnchorId="@id/cardBackgroundView"
                                motion:touchAnchorSide="bottom"
                                motion:dragDirection="dragUp"
                                motion:touchRegionId="@id/cardBackgroundView"/>
                        </Transition>
                        <ConstraintSet android:id="@+id/start">
                            <Constraint
                                    android:id="@id/cardLayoutGuideline"
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:orientation="horizontal"
                                    motion:layout_constraintGuide_percent="0.8"/>
                        </ConstraintSet>
                        <ConstraintSet android:id="@+id/end">
                            <Constraint
                                    android:id="@id/cardLayoutGuideline"
                                    android:layout_width="wrap_content"
                                    android:layout_height="wrap_content"
                                    android:orientation="horizontal"
                                    motion:layout_constraintGuide_percent="0.05"/>
                        </ConstraintSet>
                    </MotionScene>
                

MotionLayout: Interpolation attributes

  • alpha
  • visibility
  • elevation
  • rotation, rotationX, rotationY
  • translationX, translationY, translationZ
  • scaleX, scaleY

Важно: нужно определить CustomAttribute в start и end <ConstraintSet>

<CustomAttribute> supported types:

  • motion:customColorValue
  • motion:customIntegerValue
  • motion:customFloatValue
  • motion:customStringValue
  • motion:customDimension
  • motion:customBoolean

                    <Constraint
                        android:id="@id/cardBackgroundView" ...>
                        <CustomAttribute
                            motion:attributeName="backgroundColor"
                            motion:customColorValue="#0099ff"/>
                    </Constraint>
                

MotionLayout: ImageFilterView


                    <android.support.constraint.utils.ImageFilterView
                        android:id="@+id/imageFilterView"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:background="@color/accent"
                        android:src="@drawable/ic_start"
                        app:altSrc="@drawable/ic_end" />
                

                    <ConstraintSet android:id="@+id/start">
                        <Constraint
                            android:id="@id/imageFilterView" ...>
                            <CustomAttribute
                                motion:attributeName="crossfade"
                                motion:customColorValue="0"/>
                            <CustomAttribute
                                motion:attributeName="saturation"
                                <-- 0 = grayscale; 1 = original, 2 = hyper saturated -->
                                motion:customFloatValue="1" />
                            <CustomAttribute
                                motion:attributeName="contrast"
                                <-- 0 = gray; 1 = original, 2 = hyper contrast -->
                                motion:customFloatValue="1" />
                            <CustomAttribute
                                motion:attributeName="warmth"
                                <-- 0.5 = cold (blue); 1 = original, 2 = warm (red) -->
                                motion:customFloatValue="1" />
                        </Constraint>
                    </ConstraintSet>
                    <ConstraintSet android:id="@+id/end">
                        <Constraint
                            android:id="@id/imageFilterView" ...>
                            <-- exactly same list of <CustomAttribute> -->
                        </Constraint>
                    </ConstraintSet>
                

MotionLayout: Keyframe


                    <Transition ...>
                        <KeyFrameSet>

                            <KeyPosition
                                motion:keyPositionType="parentRelative"
                                motion:percentY="0.25"
                                motion:framePosition="50"
                                motion:target="@id/imageView"/>

                            <KeyAttribute
                                motion:rotation="90"
                                motion:framePosition="100"
                                motion:motionTarget="@id/recyclerView"/>

                            <KeyAttribute
                                motion:alpha="0"
                                motion:framePosition="0"
                                motion:motionTarget="@id/view"/>
                            <KeyAttribute
                                motion:alpha="0"
                                motion:framePosition="75"
                                motion:motionTarget="@id/view"/>
                            <KeyAttribute
                                motion:alpha="1"
                                motion:framePosition="100"
                                motion:motionTarget="@id/view"/>
                        </KeyFrameSet>
                    </Transition>
                

MotionLayout: Future Comes Today

MotionLayout: Tools

Lottie: download animations easily

  • Запросы кэшируются
  • Множественные одинаковые запросы будут объединяться и парсинг анимации будет выполнен один раз
  • Анимацию можно комбинировать с ValueAnimator
  • Анимацию можно проигрывать по частям
  • LottieAnimationView поддерживает два scale types: centerCrop & centerInside
  1. src/main/res/raw - json ресурс анимации
  2. src/main/assets - json ресурс анимации
  3. src/main/assets - zip ресурс анимации
  4. URL - для json или zip
  5. String - json виде строки
  6. InputStream - для json или zip

Lottie: LottieAnimationView

From res/raw: lottie_rawRes


                    <com.airbnb.lottie.LottieAnimationView
                        android:id="@+id/lottieAnimationView"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"

                        app:lottie_rawRes="@raw/animation"

                        <-- Loop indefinitely -->
                        app:lottie_loop="true"
                        <-- Start playing as soon as the animation is loaded -->
                        app:lottie_autoPlay="true" />
                

From assets/: lottie_fileName


                    <com.airbnb.lottie.LottieAnimationView
                        android:id="@+id/lottieAnimationView"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        app:lottie_fileName="animation.json"

                        <-- Repeat count: Integer -->
                        app:lottie_repeatCount="-1"
                        <-- Repeat mode: reverse / restart -->
                        app:lottie_repeatMode="reverse" />
                
  • LottieAnimationView - расширение ImageView
  • LottieDrawable - Drawable, который можно использовать для любого View
  • LottieComposition - абстрактная модель анимации (создается через LottieCompositionFactory)

Summary

  • Анимации должны быть информативными и упрощать жизнь пользователю
  • Используйте современные анимации
  • Помните о скорости отрисовки кадров
  • Lottie - аналог Glide для анимаций

Дополнительные ссылки

Reveal.initialize({ center: true })