Подтвердить что ты не робот

Обнаружение анимации в Android RecyclerView

RecyclerView, в отличие от ListView, не имеет простого способа установить для него пустое представление, поэтому нужно вручную управлять им, делая видимым пустое представление в случае, если количество элементов адаптера равно 0.

Внедряя это, сначала я попытался вызвать пустую логику вида сразу после изменения структуры подкласса (ArrayList в моем случае), например:

btnRemoveFirst.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        devices.remove(0); // remove item from ArrayList
        adapter.notifyItemRemoved(0); // notify RecyclerView adapter
        updateEmptyView();
    }
});

Он делает то же самое, но имеет недостаток: когда последний элемент удаляется, перед просмотром появляется пустой вид , после чего удаляется. Поэтому я решил подождать окончания анимации и обновить интерфейс.

К моему удивлению, я не смог найти хороший способ прослушать анимационные события в RecyclerView. Первое, что приходит на ум, - использовать метод isRunning следующим образом:

btnRemoveFirst.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        devices.remove(0); // remove item from ArrayList
        adapter.notifyItemRemoved(0); // notify RecyclerView adapter
        recyclerView.getItemAnimator().isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
            @Override
            public void onAnimationsFinished() {
                updateEmptyView();
            }
        });
    }
});

К сожалению, обратный вызов в этом случае выполняется немедленно, потому что в этот момент внутренний ItemAnimator все еще не находится в состоянии "running". Итак, вопросы: как правильно использовать метод ItemAnimator.isRunning() и есть лучший способ добиться желаемого результата, т.е. показать пустой вид после удаления анимации одного элемента закончил

4b9b3361

Ответ 1

В настоящее время я нашел единственный способ решить эту проблему - расширить ItemAnimator и передать его в RecyclerView следующим образом:

recyclerView.setItemAnimator(new DefaultItemAnimator() {
    @Override
    public void onAnimationFinished(RecyclerView.ViewHolder viewHolder) {
        updateEmptyView();
    }
});

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


PS: мой коллега предложил обернуть ItemAnimator внутри декоратора следующим образом:

recyclerView.setItemAnimator(new ListenableItemAnimator(recyclerView.getItemAnimator()));

Было бы неплохо, несмотря на то, что это кажется излишним для такой тривиальной задачи, но создание декоратора в этом случае невозможно в любом случае, потому что ItemAnimator имеет метод setListener() который защищен пакетом, поэтому я, очевидно, не могу также обернуть его как несколько заключительных методов.

Ответ 2

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

Я попробовал ответить Роману Петренко, но в этом случае он не работает. Проблема в том, что onAnimationFinished вызывается для каждой записи в представлении переработчика. Большинство записей не изменились, поэтому onAnimationFinished вызывается более или менее мгновенно. Но для добавления и удаления анимация занимает немного времени, поэтому она называется позже.

Это приводит как минимум к двум проблемам. Предположим, у вас есть метод doStuff() который вы хотите запустить после завершения анимации.

  1. Если вы просто вызываете doStuff() в onAnimationFinished вы будете вызывать его один раз для каждого элемента в представлении переработчика, что может не соответствовать тому, что вы хотите сделать.

  2. Если вы просто вызываете doStuff() при первом onAnimationFinished вы можете вызывать это задолго до завершения последней анимации.

Если бы вы могли знать, сколько элементов нужно анимировать, вы можете убедиться, что вызываете doStuff() после завершения последней анимации. Но я не нашел способа узнать, сколько оставшихся анимаций находится в очереди.

Мое решение этой проблемы состоит в том, чтобы позволить представлению переработчика сначала начать анимацию с помощью new Handler().post(), а затем настроить прослушиватель с помощью isRunning() который вызывается, когда анимация готова. После этого он повторяет процесс, пока все виды не будут анимированы.

void changeAdapterData() {
    // ...
    // Changes are made to the data held by the adapter
    recyclerView.getAdapter().notifyDataSetChanged();

    // The recycler view have not started animating yet, so post a message to the
    // message queue that will be run after the recycler view have started animating.
    new Handler().post(waitForAnimationsToFinishRunnable);
}

private Runnable waitForAnimationsToFinishRunnable = new Runnable() {
    @Override
    public void run() {
        waitForAnimationsToFinish();
    }
};

// When the data in the recycler view is changed all views are animated. If the
// recycler view is animating, this method sets up a listener that is called when the
// current animation finishes. The listener will call this method again once the
// animation is done.
private void waitForAnimationsToFinish() {
    if (recyclerView.isAnimating()) {
        // The recycler view is still animating, try again when the animation has finished.
        recyclerView.getItemAnimator().isRunning(animationFinishedListener);
        return;
    }

    // The recycler view have animated all it views
    onRecyclerViewAnimationsFinished();
}

// Listener that is called whenever the recycler view have finished animating one view.
private RecyclerView.ItemAnimator.ItemAnimatorFinishedListener animationFinishedListener =
        new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
    @Override
    public void onAnimationsFinished() {
        // The current animation have finished and there is currently no animation running,
        // but there might still be more items that will be animated after this method returns.
        // Post a message to the message queue for checking if there are any more
        // animations running.
        new Handler().post(waitForAnimationsToFinishRunnable);
    }
};

// The recycler view is done animating, it now time to doStuff().
private void onRecyclerViewAnimationsFinished() {
    doStuff();
}

Ответ 3

Что сработало для меня, это следующее:

  • обнаружит, что держатель вида удален.
  • в этом случае зарегистрировать прослушиватель, который будет уведомлен, когда dispatchAnimationsFinished() вызывается
  • когда все анимации закончены, вызовите слушателя для выполнения задачи (updateEmptyView())

public class CompareItemAnimator extends DefaultItemAnimator implements RecyclerView.ItemAnimator.ItemAnimatorFinishedListener {

private OnItemAnimatorListener mOnItemAnimatorListener;

public interface OnItemAnimatorListener {
    void onAnimationsFinishedOnItemRemoved();
}

@Override
public void onAnimationsFinished() {
    if (mOnItemAnimatorListener != null) {
        mOnItemAnimatorListener.onAnimationsFinishedOnItemRemoved();
    }
}

public void setOnItemAnimatorListener(OnItemAnimatorListener onItemAnimatorListener) {
    mOnItemAnimatorListener = onItemAnimatorListener;
}

@Override
public void onRemoveFinished(RecyclerView.ViewHolder viewHolder) {
    isRunning(this);
}}

Ответ 4

Чтобы расширить ответ на вопрос Романа Петренко, у меня тоже нет поистине универсального ответа, но я нашел шаблон Factory полезным способом, по крайней мере, очистить часть трещины, которая является этой проблемой.

public class ItemAnimatorFactory {

    public interface OnAnimationEndedCallback{
        void onAnimationEnded();
    }
    public static RecyclerView.ItemAnimator getAnimationCallbackItemAnimator(OnAnimationEndedCallback callback){
        return new FadeInAnimator() {
            @Override
            public void onAnimationFinished(RecyclerView.ViewHolder viewHolder) {
                callback.onAnimationEnded();
                super.onAnimationEnded(viewHolder);
            }
        };
    }
}

В моем случае я использую библиотеку, которая предоставляет FadeInAnimator, который я уже использовал. Я использую римское решение в методе Factory, чтобы подключиться к событию onAnimationEnded, а затем передать событие в цепочку.

Затем, когда я настраиваю свой recyclerview, я указываю обратный вызов как мой метод для обновления представления на основе подсчета элементов recyclerview:

mRecyclerView.setItemAnimator(ItemAnimatorFactory.getAnimationCallbackItemAnimator(this::checkSize));

Опять же, он не является полностью универсальным для всех и всех ItemAnimators, но он, по крайней мере, "консолидирует крип", поэтому, если у вас есть несколько разных аниматоров элементов, вы можете просто реализовать метод Factory здесь, следуя одному шаблону, и тогда ваша конфигурация recyclerview просто указывает, какой элемент ItemAnimator вы хотите.

Ответ 5

Вот небольшой метод расширения Kotlin, основанный на ответе nibarius.

fun RecyclerView.executeAfterAllAnimationsAreFinished(
    callback: (RecyclerView) -> Unit
) = post(
    object : Runnable {
        override fun run() {
            if (isAnimating) {
                // itemAnimator is guaranteed to be non-null after isAnimating() returned true
                itemAnimator!!.isRunning {
                    post(this)
                }
            } else {
                callback([email protected])
            }
        }
    }
)

Ответ 6

В моей ситуации я хотел удалить несколько элементов (и добавить новые) после завершения анимации. Но isAnimating Событие стало началом для каждого владельца, поэтому функция @SqueezyMo не будет делать трюк, чтобы выполнить свое действие одновременно по всем пунктам. Таким образом, я реализовал Listener в моем Animator с методом, чтобы проверить, была ли сделана последняя анимация.

мультипликатор

class ClashAnimator(private val listener: Listener) : DefaultItemAnimator() {

    internal var winAnimationsMap: MutableMap<RecyclerView.ViewHolder, AnimatorSet> =
        HashMap()
    internal var exitAnimationsMap: MutableMap<RecyclerView.ViewHolder, AnimatorSet> =
        HashMap()

    private var lastAddAnimatedItem = -2

    override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean {
        return true
    }

    interface Listener {
        fun dispatchRemoveAnimationEnded()
    }

    private fun dispatchChangeFinishedIfAllAnimationsEnded(holder: ClashAdapter.ViewHolder) {
        if (winAnimationsMap.containsKey(holder) || exitAnimationsMap.containsKey(holder)) {
            return
        }
        listener.dispatchRemoveAnimationEnded() //here I dispatch the Event to my Fragment

        dispatchAnimationFinished(holder)
    }

    ...
}

Фрагмент

class HomeFragment : androidx.fragment.app.Fragment(), Injectable, ClashAdapter.Listener, ClashAnimator.Listener {
    ...
    override fun dispatchRemoveAnimationEnded() {
        mAdapter.removeClash() //will execute animateRemove
        mAdapter.addPhotos(photos.subList(0,2), picDimens[1]) //will execute animateAdd
    }
}