android - 正确覆盖 RecyclerView 动画

标签 android android-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”,因此它们不会在动画后调整其余元素,而是在 View 被移除后立即执行。有什么办法可以解决这个问题吗?

最佳答案

在实现您的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... 方法解析为 animateMoveanimateRemove 等的类。

关于android - 正确覆盖 RecyclerView 动画,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51696530/

相关文章:

android - 迁移到 Gradle 插件 3.0

java - 如何在Android Studio中安装JDOM

java - RecyclerView 更新而不刷新适配器列表

java - 在recyclerview末尾添加进度条

android - 在 Recyclerview 中使用 videoview

java - java.sql.PreparedStatement 的 Android 实现

java - 我应该让java对象为空吗?

java - 是否可以仅更改 TextView 的一部分?

java - 如何在Android中使用Retrofit在recyclerView中设置数据

android - 无法从 Firebase DB 填充回收器 View