web-dev-qa-db-ja.com

RecyclerViewでのソート(notifyDataSetChanged)中にカスタムアニメーションを提供する方法

現在、デフォルトのアニメーターAndroid.support.v7.widget.DefaultItemAnimator、これが並べ替えの際の結果です

DefaultItemAnimatorアニメーションビデオ:https://youtu.be/EccI7RUcdbg

public void sortAndNotifyDataSetChanged() {
    int i0 = 0;
    int i1 = models.size() - 1;

    while (i0 < i1) {
        DemoModel o0 = models.get(i0);
        DemoModel o1 = models.get(i1);

        models.set(i0, o1);
        models.set(i1, o0);

        i0++;
        i1--;

        //break;
    }

    // adapter is created via adapter = new RecyclerViewDemoAdapter(models, mRecyclerView, this);
    adapter.notifyDataSetChanged();
}

ただし、並べ替え時の既定のアニメーション(notifyDataSetChanged)ではなく、次のようにカスタムアニメーションを提供することを好みます。古いアイテムは右側からスライドし、新しいアイテムは上にスライドします。

予想されるアニメーションビデオ:https://youtu.be/9aQTyM7K4B

RecylerViewなしでこのようなアニメーションを実現する方法

数年前、私はLinearLayout + Viewを使用してこの効果を達成しました。現時点では、RecyclerViewはまだありません。

これがアニメーションの設定方法です

PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1.0f, 0f);
PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", 0f, (float) width);
ObjectAnimator animOut = ObjectAnimator.ofPropertyValuesHolder(this, alpha, translationX);

animOut.setDuration(duration);
animOut.setInterpolator(accelerateInterpolator);
animOut.addListener(new AnimatorListenerAdapter() {
    public void onAnimationEnd(Animator anim) {
        final View view = (View) ((ObjectAnimator) anim).getTarget();

        Message message = (Message)view.getTag(R.id.TAG_MESSAGE_ID);
        if (message == null) {
            return;
        }

        view.setAlpha(0f);
        view.setTranslationX(0);
        NewsListFragment.this.refreshUI(view, message);
        final Animation animation = AnimationUtils.loadAnimation(NewsListFragment.this.getActivity(),
            R.anim.slide_up);
        animation.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                view.setVisibility(View.VISIBLE);
                view.setTag(R.id.TAG_MESSAGE_ID, null);
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
        view.startAnimation(animation);
    }
});

layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animOut);

this.nowLinearLayout.setLayoutTransition(layoutTransition);

そして、これがアニメーションがトリガーされる方法です。

// messageView is view being added earlier in nowLinearLayout
for (int i = 0, ei = messageViews.size(); i < ei; i++) {
    View messageView = messageViews.get(i);
    messageView.setTag(R.id.TAG_MESSAGE_ID, messages.get(i));
    messageView.setVisibility(View.INVISIBLE);
}

疑問に思っていましたが、RecylerViewで同じ効果をどのように実現できますか?

21
Cheok Yan Cheng

スクロールごとに並べ替えをリセットしたくない場合は、次の方向を見てください( GITHUBデモプロジェクト )。

何らかの_RecyclerView.ItemAnimator_を使用しますが、animateAdd()およびanimateRemove()関数を書き換える代わりに、animateChange()およびanimateChangeImpl()を実装できます。ソート後、adapter.notifyItemRangeChanged(0, mItems.size());を呼び出してアニメーションをトリガーできます。したがって、アニメーションをトリガーするコードは非常にシンプルになります。

_for (int i = 0, j = mItems.size() - 1; i < j; i++, j--)
    Collections.swap(mItems, i, j);

adapter.notifyItemRangeChanged(0, mItems.size());
_

アニメーションコードの場合は_Android.support.v7.widget.DefaultItemAnimator_を使用できますが、このクラスにはプライベートanimateChangeImpl()があるため、コードをコピーして貼り付け、このメソッドを変更するか、リフレクションを使用する必要があります。または、ItemAnimatorの例で_@Andreas Wenger_が行ったように、独自のSlidingAnimatorクラスを作成できます。ここでのポイントは、2つのアニメーションがあるコードにanimateChangeImpl Simmilarを実装することです。

1)古いビューを右にスライドします

_private void animateChangeImpl(final ChangeInfo changeInfo) {
    final RecyclerView.ViewHolder oldHolder = changeInfo.oldHolder;
    final View view = oldHolder == null ? null : oldHolder.itemView;
    final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
    final View newView = newHolder != null ? newHolder.itemView : null;

    if (view == null) return;
    mChangeAnimations.add(oldHolder);

    final ViewPropertyAnimatorCompat animOut = ViewCompat.animate(view)
            .setDuration(getChangeDuration())
            .setInterpolator(interpolator)
            .translationX(view.getRootView().getWidth())
            .alpha(0);

    animOut.setListener(new VpaListenerAdapter() {
        @Override
        public void onAnimationStart(View view) {
            dispatchChangeStarting(oldHolder, true);
        }

        @Override
        public void onAnimationEnd(View view) {
            animOut.setListener(null);
            ViewCompat.setAlpha(view, 1);
            ViewCompat.setTranslationX(view, 0);
            dispatchChangeFinished(oldHolder, true);
            mChangeAnimations.remove(oldHolder);

            dispatchFinishedWhenDone();

            // starting 2-nd (Slide Up) animation
            if (newView != null)
                animateChangeInImpl(newHolder, newView);
        }
    }).start();
}
_

2)新しいビューを上にスライドします

_private void animateChangeInImpl(final RecyclerView.ViewHolder newHolder,
                                 final View newView) {

    // setting starting pre-animation params for view
    ViewCompat.setTranslationY(newView, newView.getHeight());
    ViewCompat.setAlpha(newView, 0);

    mChangeAnimations.add(newHolder);

    final ViewPropertyAnimatorCompat animIn = ViewCompat.animate(newView)
            .setDuration(getChangeDuration())
            .translationY(0)
            .alpha(1);

    animIn.setListener(new VpaListenerAdapter() {
        @Override
        public void onAnimationStart(View view) {
            dispatchChangeStarting(newHolder, false);
        }

        @Override
        public void onAnimationEnd(View view) {
            animIn.setListener(null);
            ViewCompat.setAlpha(newView, 1);
            ViewCompat.setTranslationY(newView, 0);
            dispatchChangeFinished(newHolder, false);
            mChangeAnimations.remove(newHolder);
            dispatchFinishedWhenDone();
        }
    }).start();
}
_

これは、作業スクロールと同様のアニメーションを含むデモ画像です https://i.gyazo.com/04f4b767ea61569c00d3b4a4a86795ce.gifhttps://i.gyazo.com/57a52b8477a361c383d44664392db0be.gif =

編集:

RecyclerViewのパフォーマンスを高速化するには、adapter.notifyItemRangeChanged(0, mItems.size());の代わりに、おそらく次のようなものを使用する必要があります。

_LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
int firstVisible = layoutManager.findFirstVisibleItemPosition();
int lastVisible = layoutManager.findLastVisibleItemPosition();
int itemsChanged = lastVisible - firstVisible + 1; 
// + 1 because we start count items from 0

adapter.notifyItemRangeChanged(firstVisible, itemsChanged);
_
7
varren

まず第一に:

  • このソリューションは、データセットが変更された後もまだ表示されているアイテムが右側にスライドアウトし、後で再び下からスライドインすることを前提としています(これは、少なくとも私があなたが求めていることを理解しています)。
  • この要件のため、この問題に対する簡単で適切な解決策を見つけることができませんでした(少なくとも最初の反復中)。私が見つけた唯一の方法は、アダプターをだまして、それが意図されていない何かをするためにフレームワークと戦うことでした。これが、最初の部分(通常の動作)が、RecyclerViewのデフォルトの方法でニースアニメーションを実現する方法を説明する理由です。 2番目の部分では、データセットが変更された後にすべてのアイテムにスライドアウト/スライドインアニメーションを適用する方法について説明します。
  • 後で、ランダムなIDでアダプターをだます必要のない、より良いソリューションを見つけました(更新されたバージョンの一番下にジャンプします)。

通常の動作

アニメーションを有効にするには、データセットがどのように変更されたかをRecyclerViewに伝える必要があります(実行するアニメーションの種類を認識させるため)。これは2つの方法で実行できます。

1)シンプルバージョン:adapter.setHasStableIds(true);を設定し、public long getItemId(int position)を介してアイテムのIDを提供する必要がありますAdapterからRecyclerViewへ。 RecyclerViewはこれらのIDを利用して、adapter.notifyDataSetChanged();の呼び出し中に削除/追加/移動されたアイテムを特定します

2)Advanced Version:adapter.notifyDataSetChanged();を呼び出す代わりに、データセットがどのように変更されたかを明示的に示すこともできます。 Adapterは、データセットの変更を説明するadapter.notifyItemChanged(int position)adapter.notifyItemInserted(int position)、...などのいくつかのメソッドを提供します

データセットの変更を反映するためにトリガーされるアニメーションは、ItemAnimatorによって管理されます。 RecyclerViewには、NiceのデフォルトDefaultItemAnimatorがすでに備わっています。さらに、カスタムItemAnimatorを使用してカスタムアニメーションの動作を定義することができます。

スライドアウト(右)、スライドイン(下)を実装するための戦略

右側のスライドは、データセットからアイテムが削除された場合に再生されるアニメーションです。データセットに追加されたアイテムでは、下からのアニメーションのスライドを再生する必要があります。最初に述べたように、すべての要素が右にスライドして、下からスライドすることが望ましいと思います。データセット変更の前後に表示されている場合でも。通常RecyclerViewは、表示され続けるアイテムのアニメーションを変更/移動するために再生されます。ただし、すべてのアイテムに削除/追加アニメーションを使用するため、変更後に新しい要素のみが存在し、以前に使用可能だったすべてのアイテムが削除されたとアダプターに思わせる必要があります。これは、アダプターの各アイテムにランダムなIDを提供することで実現できます。

@Override
public long getItemId(int position) {
    return Math.round(Math.random() * Long.MAX_VALUE);
}

次に、追加/削除されたアイテムのアニメーションを管理するカスタムItemAnimatorを提供する必要があります。表示されるSlidingAnimatorの構造はtheAndroid.support.v7.widget.DefaultItemAnimatorRecyclerViewで提供されます。また、これは概念の証明であり、アプリで使用する前に調整する必要があります。

public class SlidingAnimator extends SimpleItemAnimator {
    List<RecyclerView.ViewHolder> pendingAdditions = new ArrayList<>();
    List<RecyclerView.ViewHolder> pendingRemovals = new ArrayList<>();

    @Override
    public void runPendingAnimations() {
        final List<RecyclerView.ViewHolder> additionsTmp = pendingAdditions;
        List<RecyclerView.ViewHolder> removalsTmp = pendingRemovals;
        pendingAdditions = new ArrayList<>();
        pendingRemovals = new ArrayList<>();

        for (RecyclerView.ViewHolder removal : removalsTmp) {
            // run the pending remove animation
            animateRemoveImpl(removal);
        }
        removalsTmp.clear();

        if (!additionsTmp.isEmpty()) {
            Runnable adder = new Runnable() {
                public void run() {
                    for (RecyclerView.ViewHolder addition : additionsTmp) {
                        // run the pending add animation
                        animateAddImpl(addition);
                    }
                    additionsTmp.clear();
                }
            };
            // play the add animation after the remove animation finished
            ViewCompat.postOnAnimationDelayed(additionsTmp.get(0).itemView, adder, getRemoveDuration());
        }
    }

    @Override
    public boolean animateAdd(RecyclerView.ViewHolder holder) {
        pendingAdditions.add(holder);
        // translate the new items vertically so that they later slide in from the bottom
        holder.itemView.setTranslationY(300);
        // also make them invisible
        holder.itemView.setAlpha(0);
        // this requests the execution of runPendingAnimations()
        return true;
    }

    @Override
    public boolean animateRemove(final RecyclerView.ViewHolder holder) {
        pendingRemovals.add(holder);
        // this requests the execution of runPendingAnimations()
        return true;
    }

    private void animateAddImpl(final RecyclerView.ViewHolder holder) {
        View view = holder.itemView;
        final ViewPropertyAnimatorCompat anim = ViewCompat.animate(view);
        anim
                // undo the translation we applied in animateAdd
                .translationY(0)
                // undo the alpha we applied in animateAdd
                .alpha(1)
                .setDuration(getAddDuration())
                .setInterpolator(new DecelerateInterpolator())
                .setListener(new ViewPropertyAnimatorListener() {
                    @Override
                    public void onAnimationStart(View view) {
                        dispatchAddStarting(holder);
                    }

                    @Override
                    public void onAnimationEnd(View view) {
                        anim.setListener(null);
                        dispatchAddFinished(holder);
                        // cleanup
                        view.setTranslationY(0);
                        view.setAlpha(1);
                    }

                    @Override
                    public void onAnimationCancel(View view) {
                    }
                }).start();
    }

    private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
        View view = holder.itemView;
        final ViewPropertyAnimatorCompat anim = ViewCompat.animate(view);
        anim
                // translate horizontally to provide slide out to right
                .translationX(view.getWidth())
                // fade out
                .alpha(0)
                .setDuration(getRemoveDuration())
                .setInterpolator(new AccelerateInterpolator())
                .setListener(new ViewPropertyAnimatorListener() {
                    @Override
                    public void onAnimationStart(View view) {
                        dispatchRemoveStarting(holder);
                    }

                    @Override
                    public void onAnimationEnd(View view) {
                        anim.setListener(null);
                        dispatchRemoveFinished(holder);
                        // cleanup
                        view.setTranslationX(0);
                        view.setAlpha(1);
                    }

                    @Override
                    public void onAnimationCancel(View view) {
                    }
                }).start();
    }


    @Override
    public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
        // don't handle animateMove because there should only be add/remove animations
        dispatchMoveFinished(holder);
        return false;
    }
    @Override
    public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
        // don't handle animateChange because there should only be add/remove animations
        if (newHolder != null) {
            dispatchChangeFinished(newHolder, false);
        }
        dispatchChangeFinished(oldHolder, true);
        return false;
    }
    @Override
    public void endAnimation(RecyclerView.ViewHolder item) { }
    @Override
    public void endAnimations() { }
    @Override
    public boolean isRunning() { return false; }
}

これが最終結果です。

enter image description here

更新:投稿をもう一度読んでいるとき、私はより良い解決策を見つけました

この更新されたソリューションでは、ランダムなIDを持つアダプターをだまして、すべてのアイテムが削除され、新しいアイテムのみが追加されたと考える必要はありません。 2)Advanced Versionを適用する場合-データセットの変更についてアダプタに通知する方法は、adapterに以前のすべてのアイテムが削除され、すべての新しいアイテムが追加されました:

int oldSize = oldItems.size();
oldItems.clear();
// Notify the adapter all previous items were removed
notifyItemRangeRemoved(0, oldSize);

oldItems.addAll(items);
// Notify the adapter all the new items were added
notifyItemRangeInserted(0, items.size());

// don't call notifyDataSetChanged
//notifyDataSetChanged();

以前に提示されたSlidingAnimatorは、変更をアニメーション化するために依然として必要です。

9
Andreas Wenger