Введение

RecyclerView умеет превращать обычные операции с данными в плавные анимации. Когда вы добавляете элемент в список, остальные элементы плавно расступаются. При удалении — схлопываются. Эта магия происходит благодаря ItemAnimator — механизму, который я сегодня разберу до последнего винтика.

В этой статье мы погрузимся в архитектуру системы анимаций RecyclerView, проследим путь от вызова notifyItemInserted() до финальной анимации на экране.

Трехфазная архитектура: фундамент всей системы

В основе системы анимаций лежит простая идея: чтобы анимировать изменение, нужно знать состояние "до" и состояние "после". RecyclerView реализует это через трехфазный процесс layout:

1. Pre-Layout (dispatchLayoutStep1) — захват начального состояния
2. Real Layout (dispatchLayoutStep2) — расчет финальных позиций
3. Post-Layout (dispatchLayoutStep3) — сравнение и запуск анимаций

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

Фаза 1: Pre-Layout — фотографируем исходное состояние

Pre-Layout запускается сразу после вызова notify* методов адаптера, но до того, как элементы начнут физически перемещаться. В этот момент RecyclerView делает две важные вещи: сохраняет текущие позиции видимых элементов и, если включены предсказательные анимации, пытается предугадать, какие элементы появятся на экране.

Сохранение текущего состояния

// В dispatchLayoutStep1()
if (mState.mRunSimpleAnimations) {
    for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
        ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
        
        // Записываем текущие координаты каждого видимого элемента
        ItemHolderInfo animationInfo = mItemAnimator
            .recordPreLayoutInformation(mState, holder, changeFlags, payloads);
        mViewInfoStore.addToPreLayout(holder, animationInfo);
    }
}

ItemHolderInfo — это простой контейнер, который хранит снимок состояния элемента:

public static class ItemHolderInfo {
    public int left, top, right, bottom;  // Координаты View
    public int changeFlags;               // Причина изменения
}

Например, если у вас список из четырех элементов и вы вставляете новый в позицию 1, Pre-Layout сохранит реальные координаты элементов на экране в данный момент: элемент A на позиции 0, элемент B на позиции 50, C на 100, D на 150.

Предсказательные анимации: заглядываем в будущее

После сохранения текущего состояния RecyclerView делает хитрый трюк. Он просит LayoutManager разместить элементы в специальном режиме, где удаляемые элементы остаются на месте, а LayoutManager может добавить элементы за границами экрана:

if (mState.mRunPredictiveAnimations) {
    mState.mInPreLayout = true;  // Включаем предсказательный режим
    mLayout.onLayoutChildren(mRecycler, mState);
    
    // Ищем элементы, которые "появились" в предсказании
    for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
        if (!mViewInfoStore.isInPreLayout(viewHolder)) {
            // Этот элемент появится на экране после операции
            mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, info);
        }
    }
}

Зачем это нужно? Представьте, что вы добавляете элемент в начало списка. Элементы сдвинутся вниз, и снизу "въедет" новый элемент, который раньше был за границей экрана. Predictive layout позволяет заранее разместить этот элемент и записать его позицию, чтобы потом анимировать его появление.

Фаза 2: Real Layout — размещаем элементы по-настоящему

Это самая простая фаза. RecyclerView выключает все специальные режимы и просто размещает элементы в их финальных позициях:

private void dispatchLayoutStep2() {
    mState.mInPreLayout = false;  // Выключаем предсказательный режим
    mLayout.onLayoutChildren(mRecycler, mState);  // Обычная раскладка
}

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

Фаза 3: Post-Layout — сравниваем и запускаем анимации

Теперь RecyclerView снова обходит все видимые элементы и сохраняет их финальные позиции:

// В dispatchLayoutStep3()
if (mState.mRunSimpleAnimations) {
    for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
        ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
        
        // Записываем финальные координаты
        ItemHolderInfo animationInfo = mItemAnimator
            .recordPostLayoutInformation(mState, holder);
        mViewInfoStore.addToPostLayout(holder, animationInfo);
    }
}

После сбора всей информации начинается самое интересное — анализ и принятие решений.

ViewInfoStore.process(): мозг системы анимаций

Метод process() — это место, где RecyclerView анализирует собранную информацию и решает, какую анимацию запустить для каждого элемента. Вся логика построена на анализе флагов, которые указывают на наличие pre и post информации.

Структура данных для анализа

ViewInfoStore хранит для каждого ViewHolder запись InfoRecord:

static class InfoRecord {
    static final int FLAG_PRE = 1 << 2;        // Есть pre-layout данные
    static final int FLAG_POST = 1 << 3;       // Есть post-layout данные  
    static final int FLAG_APPEAR = 1 << 1;     // Элемент появился в pre-layout
    static final int FLAG_DISAPPEARED = 1;     // Элемент исчез
    
    int flags;
    ItemHolderInfo preInfo;   // Состояние ДО
    ItemHolderInfo postInfo;  // Состояние ПОСЛЕ
}

Логика принятия решений

Метод process() анализирует комбинации флагов и вызывает соответствующий callback:

void process(ProcessCallback callback) {
    for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
        ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
        InfoRecord record = mLayoutHolderMap.removeAt(index);
        
        if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
            // Элемент был видим ДО и ПОСЛЕ
            if (!preInfo.equals(postInfo)) {
                // Позиция изменилась — нужна анимация перемещения
                callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
            }
            
        } else if ((record.flags & FLAG_PRE) != 0) {
            // Есть только pre информация — элемент исчез
            callback.processDisappeared(viewHolder, record.preInfo, null);
            
        } else if ((record.flags & FLAG_POST) != 0) {
            // Есть только post информация — элемент появился
            callback.processAppeared(viewHolder, null, record.postInfo);
            
        } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
            // Элемент появился в предсказательном pre-layout и остался в post-layout
            // Это элемент, который "въехал" на экран
            callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
        }
        
        InfoRecord.recycle(record);  // Возврат в пул
    }
}

Цепочка передачи управления

После того, как process() определил тип анимации, начинается передача управления через несколько уровней абстракции. Каждый уровень добавляет свою логику:

RecyclerView.ProcessCallback отвечает за подготовку ViewHolder'а к анимации. Он блокирует переработку элемента, очищает временные хранилища и передает управление ItemAnimator'у.

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

@Override
public boolean animateDisappearance(ViewHolder viewHolder,
        ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
    
    int oldLeft = preLayoutInfo.left;
    int oldTop = preLayoutInfo.top;
    View disappearingItemView = viewHolder.itemView;
    int newLeft = postLayoutInfo == null ? disappearingItemView.getLeft() : postLayoutInfo.left;
    int newTop = postLayoutInfo == null ? disappearingItemView.getTop() : postLayoutInfo.top;
  
    if (!viewHolder.isRemoved() && (oldLeft != newLeft || oldTop != newTop)) {
        // Элемент не удален, но сместился — это анимация перемещения
        return animateMove(viewHolder, oldLeft, oldTop, newLeft, newTop);
    } else {
        // Элемент действительно удален — анимация удаления
        return animateRemove(viewHolder);
    }
}

Конкретная реализация аниматора (например, ваш кастомный аниматор) получает уже обработанные вызовы animateMove(), animateRemove(), animateAdd() и создает реальные анимации через ViewPropertyAnimator.

Пять типов анимаций

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

PERSISTENT — элементы, которые были видимы до и после изменения. Если их позиция изменилась, запускается анимация перемещения.

REMOVED — элементы, удаленные через notifyItemRemoved(). Для них запускается анимация исчезновения.

ADDED — новые элементы, добавленные через notifyItemInserted(). Для них запускается анимация появления.

DISAPPEARING — элементы, которые существуют в данных, но стали невидимыми как побочный эффект других изменений. Например, элемент был вытеснен за нижний край экрана при добавлении элементов сверху.

APPEARING — элементы, которые существовали в данных, но стали видимыми как побочный эффект. Например, элемент поднялся на экран при удалении элементов сверху.

Условия активации анимаций

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

// Простые анимации включаются, если:
mState.mRunSimpleAnimations = 
    mFirstLayoutComplete                    // Это не первый layout
    && mItemAnimator != null               // Установлен ItemAnimator
    && (есть_изменения_для_анимации)       // Есть что анимировать
    && (!полная_замена_данных              // Не вызван notifyDataSetChanged()
        || mAdapter.hasStableIds());       // Или адаптер имеет стабильные ID

// Предсказательные анимации требуют дополнительно:
mState.mRunPredictiveAnimations = 
    mState.mRunSimpleAnimations            // Простые анимации включены
    && !полная_замена_данных              // Точно не полная замена
    && mLayout.supportsPredictive();       // LayoutManager поддерживает

Это означает, что при первом показе списка или после notifyDataSetChanged() анимаций не будет. Также предсказательные анимации работают только с LayoutManager'ами, которые их поддерживают (LinearLayoutManager и GridLayoutManager поддерживают, а StaggeredGridLayoutManager — нет).

Почему архитектура именно такая?

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

Во-первых, разделение ответственности позволяет каждому компоненту фокусироваться на своей задаче. ViewInfoStore только анализирует данные, RecyclerView управляет жизненным циклом ViewHolder'ов, SimpleItemAnimator добавляет логику различения типов анимаций, а конкретная реализация отвечает только за визуальную часть.

Во-вторых, такая архитектура обеспечивает гибкость. Вы можете переопределить поведение на любом уровне, не затрагивая остальные. Нужны другие визуальные эффекты? Создайте свой аниматор, наследуясь от SimpleItemAnimator. Нужна принципиально другая логика? Наследуйтесь напрямую от ItemAnimator.

В-третьих, система обеспечивает корректное управление ресурсами. RecyclerView точно знает, когда ViewHolder анимируется, и не будет его перерабатывать до завершения анимации. Это предотвращает глюки, когда анимирующийся элемент внезапно исчезает или меняет содержимое.

Заключение

ItemAnimator в RecyclerView — это элегантная система, построенная на простом принципе сравнения состояний "до" и "после". Трехфазная архитектура layout обеспечивает сбор всей необходимой информации, ViewInfoStore анализирует эту информацию и принимает решения, а многоуровневая система callbacks гарантирует, что каждый компонент выполняет свою роль.

Понимание этой архитектуры открывает возможности для создания уникальных анимационных эффектов и помогает быстро находить причины проблем с анимациями. Теперь вы знаете, что происходит между вызовом notifyItemInserted() и красивой анимацией на экране.


Если статья была полезной, не забудьте поставить плюс. Есть вопросы по анимациям в RecyclerView? Пишите в комментариях, разберем вместе!

Комментарии (1)


  1. sevenlis
    21.08.2025 21:02

    legacy Java в Android - это... хорошо.

    когда не совсем отдупляешь, что там за фасадом фреймворк чудит, да еще и на Kotlin, это... настораживает ))