web-dev-qa-db-ja.com

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's adapter
        updateEmptyView();
    }
});

それは行いますが、欠点があります:最後の要素が削除されると、削除直後の空のビューがアニメーションの削除が終了する前に表示されます。そこで、アニメーションが終了するまで待ってから、UIを更新することにしました。

驚いたことに、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's adapter
        recyclerView.getItemAnimator().isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
            @Override
            public void onAnimationsFinished() {
                updateEmptyView();
            }
        });
    }
});

残念なことに、この場合のコールバックはすぐに実行されます。その瞬間、内部のItemAnimatorはまだ「実行中」状態ではないからです。したがって、質問は次のとおりです。ItemAnimator.isRunning()メソッドを適切に使用する方法そして、望ましい結果を達成するためのより良い方法、すなわち単一要素の削除アニメーションが終了した後、空のビューを表示

30
Roman Petrenko

現在、この問題を解決するために見つけた唯一の作業方法は、ItemAnimatorを拡張し、次のようにRecyclerViewに渡すことです。

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

しかし、ItemAnimatorによって使用されている具体的なRecyclerView実装から拡張する必要があるため、この手法は普遍的ではありません。 CoolItemAnimator内のプライベートな内部CoolRecyclerViewの場合、私のメソッドはまったく機能しません。


PS:私の同僚は、ItemAnimatordecorator 内に次のようにラップすることを提案しました。

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

このような些細なタスクには過剰すぎるように見えますが、ニースになりますが、この場合デコレータを作成することはできません。ItemAnimatorにはメソッドsetListener()があり、パッケージで保護されているため、明らかにラップできません。いくつかの最終的な方法もあります。

18
Roman Petrenko

1つまたは複数のアイテムが同時に削除または追加されたときに、リサイクラビューが完全にアニメーション化を完了したことを検出するもう少し一般的なケースがあります。

Roman Petrenkoの答えを試しましたが、この場合はうまくいきません。問題は、リサイクラビューの各エントリに対して onAnimationFinished が呼び出されることです。ほとんどのエントリは変更されていないため、onAnimationFinishedはほぼ瞬時に呼び出されます。ただし、追加と削除の場合、アニメーションには少し時間がかかるため、後で呼び出します。

これにより、少なくとも2つの問題が発生します。アニメーションの完了時に実行するdoStuff()というメソッドがあるとします。

  1. onAnimationFinisheddoStuff()を呼び出すだけで、リサイクラービュー内のすべてのアイテムに対して1回呼び出すことになります。

  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's 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's now time to doStuff().
private void onRecyclerViewAnimationsFinished() {
    doStuff();
}
12
nibarius

以下は、nibariusによる answer に基づいたKotlin拡張メソッドです。

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(this@executeAfterAllAnimationsAreFinished)
            }
        }
    }
)
2
SqueezyMo

私のために働いたのは次のとおりです:

  • ビューホルダーが取り外されたことを検出する
  • この場合、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);
}}
2
Dragoş A.

Roman Petrenkoの答えを拡張するために、私は真に普遍的な答えも持っていませんが、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を提供するライブラリを使用しています。ファクトリメソッドでRomanのソリューションを使用してonAnimationEndedイベントにフックし、イベントをチェーンに戻します。

次に、recyclerviewを構成するときに、recyclerviewのアイテム数に基づいてビューを更新するためのメソッドとしてコールバックを指定します。

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

繰り返しますが、すべてのItemAnimatorsで完全に普遍的ではありませんが、少なくとも「クラフスを統合」するため、複数の異なるアイテムアニメーターがある場合は、ここで同じパターンに従ってファクトリメソッドを実装し、recyclerview設定必要なItemAnimatorを指定するだけです。

0
Josh Kitchens

私の状況では、アニメーションが終了した後にアイテムの束を削除(および新しいアイテムを追加)したかったのです。しかし、isAnimatingイベントは各ホルダーごとにトリガーされるため、@ SqueezyMoの関数はすべてのアイテムに対して同時にアクションを実行するトリックを行いません。したがって、最後のアニメーションが完了したかどうかを確認するメソッドを使用して、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
    }
}
0
mrj