MotionLayout + RecyclerView = красивые анимированные списки

от автора

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

От переводчика: репозиторий автора статьи — https://github.com/mjmanaog/foodbuddy.
Я его форкнул, чтобы перевести. Возможно, кому-то «русская версия» подойдёт больше.

Что такое MotionLayout?

Если вкратце, то MotionLayout — это подкласс ConstraintLayout, который позволяет с помощью XML описывать движения и анимацию расположенных на нём элементов. Подробнее — в документации и вот здесь с примерами.

Итак, начнём.

Шаг 1: создадим новый проект

Назовём его, как душе угодно. В качестве активити выберем Empty Activity.

Шаг 2: добавим необходимые зависимости

В gradle-файл приложения добавим:

implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1'

И запустим синхронизацию (Sync Now в правом верхнем углу).

Шаг 3: создадим лэйаут

Наш будущий элемент списка будет выглядеть так:

Элемент списка RecyclerView
Элемент списка RecyclerView

В папке res/layout создадим файл item_food.

Внутри он выглядит так
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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/clMain"     android:layout_width="match_parent"     android:layout_height="wrap_content"     app:layoutDescription="@xml/item_food_scene">      <ImageView         android:id="@+id/ivFood"         android:layout_width="150dp"         android:layout_height="150dp"         android:layout_marginTop="8dp"         android:elevation="10dp"         android:scaleType="fitXY"         app:layout_constraintBottom_toBottomOf="parent"         app:layout_constraintStart_toStartOf="parent"         app:layout_constraintTop_toTopOf="parent"         app:srcCompat="@drawable/img_salmon_salad" />      <androidx.cardview.widget.CardView         android:id="@+id/cardView"         android:layout_width="match_parent"         android:layout_height="150dp"         android:layout_marginStart="100dp"         android:layout_marginLeft="100dp"         android:layout_marginTop="16dp"         android:layout_marginEnd="16dp"         android:layout_marginRight="16dp"         android:layout_marginBottom="16dp"         app:cardCornerRadius="20dp"         app:layout_constraintBottom_toBottomOf="parent"         app:layout_constraintEnd_toEndOf="parent"         app:layout_constraintStart_toStartOf="parent"         app:layout_constraintTop_toTopOf="parent">          <androidx.constraintlayout.widget.ConstraintLayout             android:layout_width="match_parent"             android:layout_height="match_parent">              <TextView                 android:id="@+id/tvTitle"                 android:layout_width="0dp"                 android:layout_height="wrap_content"                 android:layout_marginStart="60dp"                 android:layout_marginLeft="60dp"                 android:layout_marginTop="24dp"                 android:layout_marginEnd="8dp"                 android:layout_marginRight="8dp"                 android:textSize="18sp"                 android:textStyle="bold"                 app:layout_constraintEnd_toEndOf="parent"                 app:layout_constraintStart_toStartOf="parent"                 app:layout_constraintTop_toTopOf="parent"                 tools:text="Салат с лососем" />              <TextView                 android:id="@+id/tvDescription"                 android:layout_width="0dp"                 android:layout_height="wrap_content"                 android:layout_marginStart="60dp"                 android:layout_marginLeft="60dp"                 android:layout_marginEnd="16dp"                 android:layout_marginRight="8dp"                 android:ellipsize="end"                 android:maxLines="3"                 app:layout_constraintEnd_toEndOf="parent"                 app:layout_constraintStart_toStartOf="parent"                 app:layout_constraintTop_toBottomOf="@+id/tvTitle"                 tools:text="Лосось с овощами — идеальное сочетание для полноценного питания. Простой рецепт блюда, которое содержит в себе богатый набор питательных веществ: белки, жиры и клетчатку." />              <TextView                 android:id="@+id/tvCalories"                 android:layout_width="wrap_content"                 android:layout_height="wrap_content"                 android:layout_marginStart="8dp"                 android:layout_marginLeft="8dp"                 android:layout_marginBottom="16dp"                 android:textStyle="bold"                 app:layout_constraintBottom_toBottomOf="parent"                 app:layout_constraintStart_toEndOf="@+id/imageView6"                 tools:text="80 ккал" />              <ImageView                 android:id="@+id/imageView6"                 android:layout_width="24dp"                 android:layout_height="24dp"                 android:layout_marginStart="60dp"                 android:layout_marginLeft="60dp"                 android:layout_marginBottom="16dp"                 app:layout_constraintBottom_toBottomOf="parent"                 app:layout_constraintStart_toStartOf="parent"                 app:srcCompat="@drawable/ic_calories" />              <ImageView                 android:id="@+id/imageView7"                 android:layout_width="24dp"                 android:layout_height="24dp"                 android:layout_marginStart="24dp"                 android:layout_marginLeft="24dp"                 android:layout_marginBottom="16dp"                 app:layout_constraintBottom_toBottomOf="parent"                 app:layout_constraintStart_toEndOf="@+id/tvCalories"                 app:srcCompat="@drawable/ic_star" />              <TextView                 android:id="@+id/tvRate"                 android:layout_width="wrap_content"                 android:layout_height="wrap_content"                 android:layout_marginStart="8dp"                 android:layout_marginLeft="8dp"                 android:layout_marginBottom="16dp"                 app:layout_constraintBottom_toBottomOf="parent"                 app:layout_constraintStart_toEndOf="@+id/imageView7"                 tools:text="4.5" />         </androidx.constraintlayout.widget.ConstraintLayout>     </androidx.cardview.widget.CardView>  </androidx.constraintlayout.widget.ConstraintLayout>

Шаг 4: преобразуем ConstraintLayout в MotionLayout

Чтобы преобразовать ConstraintLayout в MotionLayout:

  • переключитесь в режим Split или Design;

  • в дереве компонентов (Component Tree) щёлкните правой кнопкой мыши на корневой элемент (в данном случае — clMain);

  • в появившемся меню выберите Convert to MotionLayout.

Как преобразовать ConstraintLayout в MotionLayout
Как преобразовать ConstraintLayout в MotionLayout

Теперь мы можем работать с MotionLayout.

Содержимое файла item_food поменялось
<?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"     xmlns:tools="http://schemas.android.com/tools"     android:id="@+id/clMain"     android:layout_width="match_parent"     android:layout_height="wrap_content"     app:layoutDescription="@xml/item_food_scene">      <ImageView         android:id="@+id/ivFood"         android:layout_width="150dp"         android:layout_height="150dp"         android:layout_marginTop="8dp"         android:elevation="10dp"         android:scaleType="fitXY"         app:layout_constraintBottom_toBottomOf="parent"         app:layout_constraintStart_toStartOf="parent"         app:layout_constraintTop_toTopOf="parent"         app:srcCompat="@drawable/img_salmon_salad" />      <androidx.cardview.widget.CardView         android:id="@+id/cardView"         android:layout_width="match_parent"         android:layout_height="150dp"         android:layout_marginStart="100dp"         android:layout_marginLeft="100dp"         android:layout_marginTop="16dp"         android:layout_marginEnd="16dp"         android:layout_marginRight="16dp"         android:layout_marginBottom="16dp"         app:cardCornerRadius="20dp"         app:layout_constraintBottom_toBottomOf="parent"         app:layout_constraintEnd_toEndOf="parent"         app:layout_constraintStart_toStartOf="parent"         app:layout_constraintTop_toTopOf="parent">          <androidx.constraintlayout.widget.ConstraintLayout             android:layout_width="match_parent"             android:layout_height="match_parent">              <TextView                 android:id="@+id/tvTitle"                 android:layout_width="0dp"                 android:layout_height="wrap_content"                 android:layout_marginStart="60dp"                 android:layout_marginLeft="60dp"                 android:layout_marginTop="24dp"                 android:layout_marginEnd="8dp"                 android:layout_marginRight="8dp"                 android:textSize="18sp"                 android:textStyle="bold"                 app:layout_constraintEnd_toEndOf="parent"                 app:layout_constraintStart_toStartOf="parent"                 app:layout_constraintTop_toTopOf="parent"                 tools:text="Салат с лососем" />              <TextView                 android:id="@+id/tvDescription"                 android:layout_width="0dp"                 android:layout_height="wrap_content"                 android:layout_marginStart="60dp"                 android:layout_marginLeft="60dp"                 android:layout_marginEnd="16dp"                 android:layout_marginRight="8dp"                 android:ellipsize="end"                 android:maxLines="3"                 app:layout_constraintEnd_toEndOf="parent"                 app:layout_constraintStart_toStartOf="parent"                 app:layout_constraintTop_toBottomOf="@+id/tvTitle"                 tools:text="Лосось с овощами — идеальное сочетание для полноценного питания. Простой рецепт блюда, которое содержит в себе богатый набор питательных веществ: белки, жиры и клетчатку." />              <TextView                 android:id="@+id/tvCalories"                 android:layout_width="wrap_content"                 android:layout_height="wrap_content"                 android:layout_marginStart="8dp"                 android:layout_marginLeft="8dp"                 android:layout_marginBottom="16dp"                 android:textStyle="bold"                 app:layout_constraintBottom_toBottomOf="parent"                 app:layout_constraintStart_toEndOf="@+id/imageView6"                 tools:text="80 ккал" />              <ImageView                 android:id="@+id/imageView6"                 android:layout_width="24dp"                 android:layout_height="24dp"                 android:layout_marginStart="60dp"                 android:layout_marginLeft="60dp"                 android:layout_marginBottom="16dp"                 app:layout_constraintBottom_toBottomOf="parent"                 app:layout_constraintStart_toStartOf="parent"                 app:srcCompat="@drawable/ic_calories" />              <ImageView                 android:id="@+id/imageView7"                 android:layout_width="24dp"                 android:layout_height="24dp"                 android:layout_marginStart="24dp"                 android:layout_marginLeft="24dp"                 android:layout_marginBottom="16dp"                 app:layout_constraintBottom_toBottomOf="parent"                 app:layout_constraintStart_toEndOf="@+id/tvCalories"                 app:srcCompat="@drawable/ic_star" />              <TextView                 android:id="@+id/tvRate"                 android:layout_width="wrap_content"                 android:layout_height="wrap_content"                 android:layout_marginStart="8dp"                 android:layout_marginLeft="8dp"                 android:layout_marginBottom="16dp"                 app:layout_constraintBottom_toBottomOf="parent"                 app:layout_constraintStart_toEndOf="@+id/imageView7"                 tools:text="4.5" />         </androidx.constraintlayout.widget.ConstraintLayout>     </androidx.cardview.widget.CardView>  </androidx.constraintlayout.motion.widget.MotionLayout>

В папке res Студия создала папку xml и положила в неё файл item_food_scene.xml:

Студия предупреждает (Warnings в нижней части экрана), что у элементов ImageView не заполнен тег contentDescription. Можете проигнорировать эти сообщения, а можете добавить в XML-разметке соответствующие теги (для чего они нужны, читайте здесь).

Шаг 5: добавим анимацию на ImageView

  1. В дереве элементов выберите ivFood (ImageView с основной картинкой);

  2. В редакторе MotionLayout выберите end;

  3. У выделенного элемента ivFood выделите правую (End) опорную точку и перетащите её за правую (End) границу родительского элемента;

  4. Картинка должна встать по центру родительского элемента;

  5. Поменяйте значение атрибутов layout_height и layout_width на 300dp.

От переводчика: начальное состояние ImageView (его положение, ширина и высота) осталось без изменений, а его конечное состояние изменилось: он встанет по центру и увеличится в размере в два раза (с 150dp до 300dp).

Шаг 6: посмотрим, что получилось

Чтобы воспроизвести анимацию, которую мы только что настроили:

  1. В редакторе MotionLayout выделите толстую стрелку, которая соединяет прямоугольники с надписями start и end;

  2. В редакторе ниже станет доступным блок Transition;

  3. Нажмите кнопку Play, чтобы воспроизвести анимацию.

Шаг 7: добавим анимацию на CardView

Порядок действий схож:

  1. В дереве компонентов выделите cardView (constraintView с заголовком, описанием, калорийностью и оценкой);

  2. В редакторе MotionLayout выберите end;

  3. Выделите cardView в появившемся разделе ConstraintSet;

  4. В разделе атрибутов элемента перейдите к группе Transforms;

  5. Поменяйте значение атрибута alpha на 0.

От переводчика: у карточки с описанием блюда конечное состояние (end) от начального (start) отличается только значением параметра alpha. В конечном состоянии она будет скрыта (и скрываться она будет плавно).

Шаг 8: добавим обработчик нажатий

Чтобы анимация включалась, надо настроить обработчик нажатий:

  1. В разделе атрибутов под OnClick добавьте новое поле (кнопка «+»);

  2. В параметра targetId выберите значение ivFood;

  3. Добавьте ещё одно поле;

  4. Для параметра ClickAction выберите значение toggle.

В результате получится такая анимация:

Шаг 9: добавим RecyclerView в activity_main.xml

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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">      <androidx.recyclerview.widget.RecyclerView         android:id="@+id/rvMain"         android:layout_width="match_parent"         android:layout_height="match_parent"         app:layout_constraintBottom_toBottomOf="parent"         app:layout_constraintEnd_toEndOf="parent"         app:layout_constraintStart_toStartOf="parent"         app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>

Шаг 10: создадим класс и фиктивные данные для примера

package com.mjmanaog.foodbuddy.data.model  import com.mjmanaog.foodbuddy.R  data class FoodModel(         val title: String,         val description: String,         val calories: String,         val rate: String,         val imgId: Int )  val foodDummyData: ArrayList<FoodModel> = arrayListOf(         FoodModel(                 "Салат с лососем",                 "Лосось с овощами — идеальное сочетание для полноценного питания. Простой рецепт блюда, которое содержит в себе богатый набор питательных веществ: белки, жиры и клетчатку.",                 "80 ккал",                 "4.5",                 R.drawable.img_salmon_salad         ),         FoodModel(                 "Куриная грудка-барбекю",                 "От курочки, приготовленной на гриле или запечённой в духовке все всегда в восторге, если только она не сухая или пережаренная.",                 "80 ккал",                 "4.5",                 R.drawable.img_chicken         ),         FoodModel(                 "Курица с рисом на пару",                 "Приготовление на пару — здоровый метод приготовления пищи. Он сохраняет её аромат, нежность и полезные вещества. К тому же для приготовления блюд не используется масло.",                 "80 ккал",                 "4.5",                 R.drawable.img_chicken_rice         ),         FoodModel(                 "Салат Цезарь",                 "Зелёный салат из листьев салата ромэн и гренок, заправленный лимонным соком (или соком лайма), оливковым маслом, яйцом, Вустерширским соусом, анчоусами, чесноком, дижонской горчицей, сыром Пармезан и чёрным перцем.",                 "80 ккал",                 "4.5",                 R.drawable.img_salad         ),         FoodModel(                 "Просто полезная еда",                 "Лосось с овощами — идеальное сочетание для полноценного питания. Простой рецепт блюда, которое содержит в себе богатый набор питательных веществ: белки, жиры и клетчатку.",                 "80 ккал",                 "4.5",                 R.drawable.img_healthy         ) )

Шаг 11: создадим адаптер и ViewHolder

Тут ничего экзотического нет. Используем нашу FoodModel

Шаг 12: заполним RecyclerView элементами

class MainActivity : AppCompatActivity() {     private var foodAdapter: FoodAdapter = FoodAdapter()     override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         setContentView(R.layout.activity_main)          rvMain.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)         rvMain.adapter = foodAdapter         foodAdapter.addAll(foodDummyData)     } }

В результате этих несложных действий получилась такая анимация:

GIF из статьи не стал добавлять, потому что

Она вести 11 Мб.

Ещё кое-что

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

Надеюсь, материал из этой статьи кому-то окажется полезным. Будет круто, если вы узнаете из неё что-то новое.

Спасибо за внимание.

ссылка на оригинал статьи https://habr.com/ru/post/550846/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *