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()メソッドを適切に使用する方法そして、望ましい結果を達成するためのより良い方法、すなわち単一要素の削除アニメーションが終了した後、空のビューを表示?
現在、この問題を解決するために見つけた唯一の作業方法は、ItemAnimator
を拡張し、次のようにRecyclerView
に渡すことです。
recyclerView.setItemAnimator(new DefaultItemAnimator() {
@Override
public void onAnimationFinished(RecyclerView.ViewHolder viewHolder) {
updateEmptyView();
}
});
しかし、ItemAnimator
によって使用されている具体的なRecyclerView
実装から拡張する必要があるため、この手法は普遍的ではありません。 CoolItemAnimator
内のプライベートな内部CoolRecyclerView
の場合、私のメソッドはまったく機能しません。
PS:私の同僚は、ItemAnimator
を decorator 内に次のようにラップすることを提案しました。
recyclerView.setItemAnimator(new ListenableItemAnimator(recyclerView.getItemAnimator()));
このような些細なタスクには過剰すぎるように見えますが、ニースになりますが、この場合デコレータを作成することはできません。ItemAnimator
にはメソッドsetListener()
があり、パッケージで保護されているため、明らかにラップできません。いくつかの最終的な方法もあります。
1つまたは複数のアイテムが同時に削除または追加されたときに、リサイクラビューが完全にアニメーション化を完了したことを検出するもう少し一般的なケースがあります。
Roman Petrenkoの答えを試しましたが、この場合はうまくいきません。問題は、リサイクラビューの各エントリに対して onAnimationFinished
が呼び出されることです。ほとんどのエントリは変更されていないため、onAnimationFinished
はほぼ瞬時に呼び出されます。ただし、追加と削除の場合、アニメーションには少し時間がかかるため、後で呼び出します。
これにより、少なくとも2つの問題が発生します。アニメーションの完了時に実行するdoStuff()
というメソッドがあるとします。
onAnimationFinished
でdoStuff()
を呼び出すだけで、リサイクラービュー内のすべてのアイテムに対して1回呼び出すことになります。
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();
}
以下は、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)
}
}
}
)
私のために働いたのは次のとおりです:
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);
}}
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を指定するだけです。
私の状況では、アニメーションが終了した後にアイテムの束を削除(および新しいアイテムを追加)したかったのです。しかし、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
}
}