Android animate recyclerview changes

RecyclerView Item Change Animations With a Custom Item Animator

I recently worked on a project for kids which uses lots of animations, and there were also cases where a view within the recyclerview item needs to be animated. I had previously watched this great talk, and I recalled that I shouldn’t do this inside the adapter but should instead use a custom ItemAnimator. So I began to dig the subject of custom item animators. However, most of the tutorials available were about adding/removing/moving item animations and the very few that I could find on item change animations were overcomplicated! I had to watch several times that long and dense talk I shared above and dig lots of tutorials until I finally come up with a simple solution. That’s why I’m sharing with you a simplified example of recyclerview item change animation and step-by-step explanation.

I prepared a super simple sample with a list of items and each item has a clickable heart for liking/disliking the item. We are going to see how to animate the heart the proper way, with a custom ItemAnimator. I used Lottie Animations for like animation. But it is irrelevant to the subject of this tutorial. You could use an animated vector drawable or any other animation api of your choice.

Simplest way of creating a custom item animator is to extend DefaultItemAnimator(). This is the default one that is used by recyclerview and already handles animating adding, removing, moving items gracefully. For item change animations, DefaultItemAnimator uses cross-fade animations. This makes sense, as recyclerview doesn’t know what will change exactly. I want to change this behavior and tell the recyclerview what will change and how it should animate that change.

So my first step will be to extend DefaultItemAnimator() and override animateChange() method:

You can see that this method receives an oldHolder and a newHolder. As the DefaultItemAnimator uses cross-fade for item change animations, it creates two instances of the viewholder for the same position and it cross-fades between the two. As I don’t want a cross-fade animation, I don’t want two instances of the viewholder. I want to keep the same viewholder instance and I want to start animating the heart on that one. To do that, I need to override canReuseUpdatedViewHolder method and return true:

Now the oldHolder and newHolder parameters in animateChange method will be the same. What about preInfo and postInfo? ItemHolderInfo is a class that holds information about an item’s bounds. RecyclerView records this information about the item before the change ( preInfo) and after the change ( postInfo), then compares them and animates in between. However, with the default implementation it records only item bounds and animates positional changes. In our case, we need to know the state of the heart image, in other words, whether the item was already liked before or not. So, we’re going to override recordPreLayoutInformation method and save the state of the item (heart) before the change. We’ll also need a custom ItemHolderInfo for saving that.

recordPreLayoutInformation() is called in many cases, not only in item change case. As I want to intervene only in the item change case, I check for FLAG_CHANGED. But that is not enough either. Change animation can also be called when notifyDataSetChanged() is called for instance. I don’t want to check heart state and switch like state in those cases.

Читайте также:  Прошивка андроид через flash tools

The argument payloads come to play at this point. Payloads help us communicate to the recyclerview what exactly has changed. notifyItemChanged method has an optional payload argument. This has the type Object, so you could communicate the change as you wish, like an integer or string key, or a Bundle.. Inside the recordPreLayoutInformation method, we’ll check for this payload and register the information when we need it.

So, in this case, when the heart is clicked, I notify the adapter that the item has changed with an additional payload argument. That is an integer key I had defined for heart animation. Then inside recordPreLayoutInformation() method I check for this payload, and then check the state of the heart before the change, to see whether it was already in the liked state. And I’m saving this information in the ProductItemHolderInfo class. (If you’re not familiar with Lottie animations, and don’t understand 1–2 lines above, it is not important at all. You could get the state of your view in another way, depending on your case. I’ll also share an alternative solution later)

Now, I can finally fill my animateChange method.

Inside animateChange method, I get one of the viewholders and cast it to my custom viewholder. (It doesn’t matter whether you use newOlder or oldHolder anymore, since they will be the same) Then I check the preInfo. If the preInfo tells me that this item was already liked before, I’ll change it to dislike state. If it was not liked before, I’ll start like animation. I didn’t use a reverse animation for disliking, but you might use two different (reverse) animations in your case.

If you set this animator to your recyclerview and run at this point, it should already work. However, there is one more important detail to implement this properly, to avoid possible weird bugs. Recyclerview keeps track of animating views and doesn’t recycle them until they are done animating. So an ItemAnimator should tell the recyclerview when an animation is finished. DefaultAnimator does this, but as we override animateChange method, we need to handle that case ourselves. To do that, we’ll simply set an animator listener and when animation is completed we’ll call dispatchAnimationFinished(holder):

Now our custom item animator is ready. Don’t forget to set it to your recyclerview:

Other Possible Cases and Solutions

As usual, there are multiple possible solutions to a problem. In the example above, I got the state of the heart from the view itself. But another possible solution is to use two separate payload keys for LIKE and DISLIKE cases. In a real life scenario, you’ll probably persist and observe this information and you’ll know whether you’re about to like or dislike. You could then communicate this info with payload keys and animate accordingly.

You might have noticed that I have only used preInfo and I have only overriden recordPreLayoutInformation() in this sample . Though there is also a postInfo argument and a recordPostLayoutInformation() method. As we are simply shuffling between two states in this sample, it was sufficient to know previous state. However, there might be cases, where the before state and after state might not be predetermined, or there might be more than two possible states. In that case, you could also override recordPostLayoutInformation() and use the postInfo argument in the animateChange method to compare before and after states and animate accordingly.

Читайте также:  Ауди q5 установка андроид

More Realistic Scenario

Of course, real apps are nothing like my over-simplified sample. Suppose that these products were persisted in a local database. My adapter is getting the data from the database and observing it. I would probably have a field like isLiked in my Product entity, and when I like or dislike a product, I would set it accordingly in the database. As my list is observing the database, changes would be triggered in the list. Luckily, as I’m using DiffUtil, adapter knows which product has changed, it won’t update all products. However, with the current implementation of DiffUtil, it doesn’t know what exactly has changed. So it will completely rebind the item, the item will flicker and I won’t receive the payload I was using in my animator.

In my super-simple-sample I communicate what has changed by passing a payload in my click listener:

Technically, in the database version of the app, I can still call this when like is clicked, however, there would be a glitch, if I manually start an animation and then database triggers a change in the adapter. Remember that it is always healthier to have a single source of truth, which is the database in this scenario.

The solution to this problem lies in DiffUtil again. DiffUtil has another method which is usually omitted, called getChangePayload. By default, it passes null as payload, thus adapter updates the whole item. By overriding this method we can pass a payload to the adapter to communicate what has exactly changed. So DiffUtil in the database version would be something like:

Then, we would use these payloads in our item animator as before.

You can see the sample project on github. I hope this tutorial will help a few developers implement this faster than I had done at first. But I still strongly encourage you to watch the talk I mentioned above, if you have time. Thanks for reading!

Источник

Drag и Swipe в RecyclerView. Часть 2: контроллеры перетаскивания, сетки и пользовательские анимации

В первой части мы рассмотрели ItemTouchHelper и реализацию ItemTouchHelper.Callback, которая добавляет базовые функции drag & drop и swipe-to-dismiss в RecyclerView . В этой статье мы продолжим то, что было сделано в предыдущей, добавив поддержку расположения элементов в виде сетки, контроллеры перетаскивания, выделение элемента списка и пользовательские анимации смахивания (англ. swipe).

Контроллеры перетаскивания

При создании списка, поддерживающего drag & drop, обычно реализуют возможность перетаскивания элементов по касанию. Это способствует понятности и удобству использования списка в «режиме редактирования», а также рекомендуется material-гайдлайнами. Добавить контроллеры перетаскивания в наш пример сказочно легко.

Сперва обновим layout элемента (item_main.xml).

Изображение, используемое для контроллера перетаскивания, можно найти в Material Design иконках и добавить в проект с помощью удобного плагина генератора иконок в Android Studio.

Как кратко упоминалось в прошлой статье, вы можете использовать ItemTouchHelper.startDrag(ViewHolder) , чтобы программно запустить перетаскивание. Итак, всё, что нам нужно сделать, это обновить ViewHolder , добавив контроллер перетаскивания, и настроить простой обработчик касаний, который будет вызывать startDrag() .

Читайте также:  Zenmate vpn для андроид

Нам понадобится интерфейс для передачи события по цепочке:

Затем определите ImageView для контроллера перетаскивания в ItemViewHolder :

и обновите RecyclerListAdapter :

RecyclerListAdapter теперь должен выглядеть примерно так.

Всё, что осталось сделать, это добавить OnStartDragListener во фрагмент:

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

Выделение элемента списка

Сейчас в нашем примере нет никакой визуальной индикации элемента, который перетаскивается. Очевидно, так быть не должно, но это легко исправить. С помощью ItemTouchHelper можно использовать стандартные эффекты подсветки элемента. На Lollipop и более поздних версиях Android, подсветка «расплывается» по элементу в процессе взаимодействия с ним; на более ранних версиях элемент просто меняет свой цвет на затемнённый.

Чтобы реализовать это в нашем примере, просто добавьте фон (свойство background ) в корневой FrameLayout элемента item_main.xml или установите его в конструкторе RecyclerListAdapter.ItemViewHolder. Это будет выглядеть примерно так:

Выглядит круто, но, возможно, вы захотите контролировать ещё больше. Один из способов сделать это — позволить ViewHolder обрабатывать изменения состояния элемента. Для этого ItemTouchHelper.Callback предоставляет ещё два метода:

  • onSelectedChanged(ViewHolder, int) вызывается каждый раз, когда состояние элемента меняется на drag (ACTION_STATE_DRAG) или swipe (ACTION_STATE_SWIPE). Это идеальное место, чтобы изменить состояние view -компонента на активное.
  • clearView(RecyclerView, ViewHolder) вызывается при окончании перетаскивания view -компонента, а также при завершении смахивания (ACTION_STATE_IDLE). Здесь обычно восстанавливается изначальное состояние вашего view -компонента.

А теперь давайте просто соберём всё это вместе.

Сперва создайте интерфейс, который будут реализовывать ViewHolders :

Затем в SimpleItemTouchHelperCallback реализуйте соотвутствующие методы:

Теперь осталось только, чтобы RecyclerListAdapter.ItemViewHolder реализовал ItemTouchHelperViewHolder :

В этом примере мы просто добавляем серый фон во время активности элемента, а затем его удаляем. Если ваш ItemTouchHelper и адаптер тесно связаны, вы можете легко отказаться от этой настройки и переключать состояние view -компонента прямо в ItemTouchHelper.Callback .

Сетки

Если теперь вы попытаетесь использовать GridLayoutManager , вы увидите, что он работает неправильно. Причина и решение проблемы просты: мы должны сообщить нашему ItemTouchHelper , что мы хотим поддерживать перетаскивание элементов влево и вправо. Ранее в SimpleItemTouchHelperCallback мы уже указывали:

Единственное изменение, необходимое для поддержки сеток, заключается в добавлении соответствующих флагов:

Тем не менее, swipe-to-dismiss не очень естественное поведение для элементов в виде сетки, поэтому swipeFlags разумнее всего обнулить:

Чтобы увидеть рабочий пример GridLayoutManager , смотрите RecyclerGridFragment. Вот как это выглядит при запуске:

Пользовательские анимации смахивания

ItemTouchHelper.Callback предоставляет действительно удобный способ для полного контроля анимации во время перетаскивания или смахивания. Поскольку ItemTouchHelper — это RecyclerView.ItemDecoration, мы можем вмешаться в процесс отрисовки view -компонента похожим образом. В следующей части мы разберём этот вопрос подробнее, а пока посмотрим на простой пример переопределения анимации смахивания по умолчанию, чтобы показать линейное исчезновение.

Параметры dX и dY — это текущий сдвиг относительно выделенного view -компонента, где:

  • -1.0f — это полное смахивание справа налево (от ItemTouchHelper.END к ItemTouchHelper.START )
  • 1.0f — это полное смахивание слева направо (от ItemTouchHelper.START к ItemTouchHelper.END )

Важно вызывать super для любого actionState , который вы не обрабатываете, для того, чтобы запускалась анимация по умолчанию.

В следующей части мы рассмотрим пример, в котором будем контролировать отрисовку элемента в момент перетаскивания.

Заключение

На самом деле, настройка ItemTouchHelper — это довольно весело. Чтобы не увеличивать объём этой статьи, я разделил её на несколько.

Исходный код

Весь код этой серии статей смотрите на GitHub-репозитории Android-ItemTouchHelper-Demo. Эта статья охватывает коммиты от ef8f149 до d164fba.

Источник

Оцените статью