Правильная перезапись анимации RecyclerView

У меня есть RecycleView, в котором отображается список элементов. Я указываю аниматор по умолчанию для RecyclerView следующим образом:

recyclerView.setItemAnimator( new DefaultItemAnimator() );

Все работает отлично, но я хочу использовать свои собственные анимации для добавления / удаления / обновления элементов в списке.

Я определил собственный класс аниматора следующим образом:

    public class MyAnimator extends RecyclerView.ItemAnimator {

    @Override
    public  boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
        return false;
    }

    @Override
    public  boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder, @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
        return false;
    }

    @Override
    public  boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
        return false;
    }

    @Override
    public  boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
        return false;
    }

    @Override
    public  void runPendingAnimations() {

    }

    @Override
    public  void endAnimation(RecyclerView.ViewHolder item) {

    }

    @Override
    public  void endAnimations() {

    }

    @Override
    public  boolean isRunning() {
        return false;
    }
}

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

Насколько я понял, animateDisappearance - это метод, который вызывается, когда элемент удаляется из списка. Если я верну false, он должен просто пропустить анимацию, насколько я понял, правильно?

Я вообще на правильном пути? когда я ищу примеры этого на github, результатов очень мало, и в целом я не могу найти ни одного базового примера кода, как это сделать, а те, которые я нашел, представляют собой все тысячи строк кода.

Как я могу просто перезаписать анимацию добавления / удаления по умолчанию моей собственной без использования каких-либо внешних библиотек? Спасибо!

Обновлено:

Мне удалось переопределить анимацию по умолчанию следующим образом:

        recyclerView.setItemAnimator(new DefaultItemAnimator() {
            @Override
            public boolean animateRemove(RecyclerView.ViewHolder holder) {
                holder.itemView.clearAnimation();
                final RecyclerView.ViewHolder h = holder;
                holder.itemView.animate()
                        .alpha(0)
                        .setInterpolator(new AccelerateInterpolator(2.f))
                        .setDuration(1350)
                        .setListener(new AnimatorListenerAdapter() {
                            @Override
                            public void onAnimationEnd(Animator animation) {
                                dispatchRemoveFinished(h);
                            }
                        })
                        .start();
                //
                return false;
            }
        } );

Анимация работает отлично, но почему-то кажется, что «dispatchRemoveFinished» запускается мгновенно, поэтому вместо настройки остальных элементов ПОСЛЕ анимации, они делают это мгновенно, как только представление удаляется. Есть ли способ исправить это?

github.com/wasabeef/recyclerview-animators/tree/master/… ... есть несколько примеров для этого.
Martin Zeitler 05.08.2018 19:21

Чтобы использовать любой из них, мне нужно использовать класс BaseItemAnimator, который представляет собой почти 1000 строк кода: github.com/wasabeef/recyclerview-animators/blob/… Это единственный способ переопределить анимацию по умолчанию? Кажется, должен быть более короткий путь, не так ли?

0x29a 05.08.2018 19:30

это DefaultItemAnimator: github.com/aosp-mirror/platform_frameworks_base/blob/master/‌… - где вы можете пропустить реализацию некоторых методов интерфейса и просто вызвать super.methodName() внутри них ... если только вызов класса super, просто возвращающий false, не сработает.

Martin Zeitler 05.08.2018 19:39

Извините, я не уверен, что полностью понял. Вы имеете в виду переопределение методов DefaultItemAnimator (как я сделал в отредактированном вопросе) и вызов super в конце?

0x29a 05.08.2018 21:04

Вам необходимо реализовать animatePersistence для анимации движения при удалении элемента.

Pawel 05.08.2018 21:42

@ 0x29a я имел в виду, что вам нужно будет вызвать, например. super.animateRemove(holder);, на всякий случай вы бы метод не реализовали. большинство методов там возвращают void или boolean (а не только какое-то статическое логическое значение).

Martin Zeitler 06.08.2018 00:05
4
6
2 000
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

При реализации вашего RecyclerView.ItemAnimator вы должны следовать нескольким правилам, иначе состояние RecyclerView испортится:

  1. Все эти пустые методы, возвращающие false, должны как минимум вызывать dispatchAnimationFinished(viewHolder), чтобы очистить состояние анимации.

  2. Если эти методы должны запускать анимацию, вы должны dispatchAnimationStarted(viewHolder), сохранить запрос анимации и вернуть true, чтобы получить вызов runPendingAnimations(), где анимация должна начинаться.

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

Вот образец ItemAnimator, который анимирует только удаление и перемещение. Обратите внимание на внутренний класс, который действует как хранитель данных анимации и слушатель состояний анимации:

public class RecAnimator extends RecyclerView.ItemAnimator {

private final static String TAG = "RecAnimator";

private final static int ANIMATION_TYPE_DISAPPEAR = 1;
private final static int ANIMATION_TYPE_MOVE = 2;

// must keep track of all pending/ongoing animations.
private final ArrayList<AnimInfo> pending = new ArrayList<>();
private final HashMap<RecyclerView.ViewHolder, AnimInfo> disappearances = new HashMap<>();
private final HashMap<RecyclerView.ViewHolder, AnimInfo> persistences = new HashMap<>();

@Override
public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
    pending.add(new AnimInfo(viewHolder, ANIMATION_TYPE_DISAPPEAR, 0));
    dispatchAnimationStarted(viewHolder);
    // new pending animation added, return true to indicate we want a call to runPendingAnimations()
    return true;
}

@Override
public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder, @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
    dispatchAnimationFinished(viewHolder);
    return false;
}

@Override
public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
    if (preLayoutInfo.top != postLayoutInfo.top) {
        // required movement
        int topDiff = preLayoutInfo.top - postLayoutInfo.top;
        AnimInfo per = persistences.get(viewHolder);
        if (per != null && per.isRunning) {
            // there is already an ongoing animation - update it instead
            per.top = per.holder.itemView.getTranslationY() + topDiff;
            per.start();
            // discard this animatePersistence call
            dispatchAnimationFinished(viewHolder);
            return false;
        }
        pending.add(new AnimInfo(viewHolder, ANIMATION_TYPE_MOVE, topDiff));
        dispatchAnimationStarted(viewHolder);
        // new pending animation added, return true to indicate we want a call to runPendingAnimations()
        return true;
    }
    dispatchAnimationFinished(viewHolder);
    return false;
}

@Override
public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
    dispatchAnimationFinished(oldHolder);
    dispatchAnimationFinished(newHolder);
    return false;
}

@Override
public void runPendingAnimations() {
    for (AnimInfo ai: pending) {
        ai.start();
    }
    pending.clear();
}

@Override
public void endAnimation(RecyclerView.ViewHolder item) {
    AnimInfo ai = disappearances.get(item);
    if (ai != null && ai.isRunning) {
        ai.holder.itemView.animate().cancel();
    }
    ai = persistences.get(item);
    if (ai != null && ai.isRunning) {
        ai.holder.itemView.animate().cancel();
    }
}

@Override
public void endAnimations() {
    for (AnimInfo ai: disappearances.values())
        if (ai.isRunning)
            ai.holder.itemView.animate().cancel();

    for (AnimInfo ai: persistences.values())
        if (ai.isRunning)
            ai.holder.itemView.animate().cancel();
}

@Override
public boolean isRunning() {
    return !pending.isEmpty() &&
            !disappearances.isEmpty() &&
            !persistences.isEmpty();
}

/** 
 * This is container for each animation. It's also cancel/end listener for them.
 * */
private final class AnimInfo implements Animator.AnimatorListener {
    private final RecyclerView.ViewHolder holder;
    private final int animationType;
    private float top;
    private boolean isRunning = false;

    private AnimInfo(RecyclerView.ViewHolder holder, int animationType, float top) {
        this.holder = holder;
        this.animationType = animationType;
        this.top = top;
    }

    void start(){
        View itemView = holder.itemView;
        itemView.animate().setListener(this);
        switch (animationType) {
            case ANIMATION_TYPE_DISAPPEAR:
                itemView.setPivotY(0f);
                itemView.animate().scaleX(0f).scaleY(0f).setDuration(getRemoveDuration());
                disappearances.put(holder, this);   // must keep track of all animations
                break;
            case ANIMATION_TYPE_MOVE:
                itemView.setTranslationY(top);
                itemView.animate().translationY(0f).setDuration(getMoveDuration());
                persistences.put(holder, this);     // must keep track of all animations
                break;
        }
        isRunning = true;
    }

    private void resetViewHolderState(){
        // reset state as if no animation was ran
        switch (animationType) {
            case ANIMATION_TYPE_DISAPPEAR:
                holder.itemView.setScaleX(1f);
                holder.itemView.setScaleY(1f);
                break;
            case ANIMATION_TYPE_MOVE:
                holder.itemView.setTranslationY(0f);
                break;
        }
    }

    @Override
    public void onAnimationEnd(Animator animation) {
        switch (animationType) {
            case ANIMATION_TYPE_DISAPPEAR:
                disappearances.remove(holder);
                break;
            case ANIMATION_TYPE_MOVE:
                persistences.remove(holder);
                break;
        }
        resetViewHolderState();
        holder.itemView.animate().setListener(null); // clear listener
        dispatchAnimationFinished(holder);
        if (!isRunning())
            dispatchAnimationsFinished();
        isRunning = false;
    }

    @Override
    public void onAnimationCancel(Animator animation) {
        // jump to end state
        switch (animationType) {
            case ANIMATION_TYPE_DISAPPEAR:
                holder.itemView.setScaleX(0f);
                holder.itemView.setScaleY(0f);
                break;
            case ANIMATION_TYPE_MOVE:
                holder.itemView.setTranslationY(0f);
                break;
        }
    }

    @Override
    public void onAnimationStart(Animator animation) {

    }

    @Override
    public void onAnimationRepeat(Animator animation) {

    }
}
}

Вы также можете переопределить класс SimpleItemAnimator, который анализирует методы animate... в animateMove, animateRemove и т. д. Для вас.

Другие вопросы по теме