RecyclerView中的项出现时如何实现动画效果

303

当RecyclerView中的项目出现时,我应该如何使它们动起来?

默认的项动画器仅在在设置了RecyclerView数据之后添加或删除数据时进行动画处理。

如何实现这一点?

11个回答

345

编辑:

根据ItemAnimator文档

该类定义在更改适配器时,项目上发生的动画。

因此,除非您将项目逐个添加到RecyclerView并在每个迭代中刷新视图,否则我不认为ItemAnimator是满足您需求的解决方案。

以下是使用CustomAdapter在项目出现时如何对RecyclerView项目执行动画的方法:

public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.ViewHolder>
{
    private Context context;

    // The items to display in your RecyclerView
    private ArrayList<String> items;
    // Allows to remember the last item shown on screen
    private int lastPosition = -1;

    public static class ViewHolder extends RecyclerView.ViewHolder
    {
        TextView text;
        // You need to retrieve the container (ie the root ViewGroup from your custom_item_layout)
        // It's the view that will be animated
        FrameLayout container;

        public ViewHolder(View itemView)
        {
            super(itemView);
            container = (FrameLayout) itemView.findViewById(R.id.item_layout_container);
            text = (TextView) itemView.findViewById(R.id.item_layout_text);
        }
    }

    public CustomAdapter(ArrayList<String> items, Context context)
    {
        this.items = items;
        this.context = context;
    }

    @Override
    public CustomAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
    {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.custom_item_layout, parent, false);
        return new ViewHolder(v);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position)
    {
        holder.text.setText(items.get(position));

        // Here you apply the animation when the view is bound
        setAnimation(holder.itemView, position);
    }

    /**
     * Here is the key method to apply the animation
     */
    private void setAnimation(View viewToAnimate, int position)
    {
        // If the bound view wasn't previously displayed on screen, it's animated
        if (position > lastPosition)
        {
            Animation animation = AnimationUtils.loadAnimation(context, android.R.anim.slide_in_left);
            viewToAnimate.startAnimation(animation);
            lastPosition = position;
        }
    }
}

您的自定义项布局将如下所示:

<FrameLayout
    android:id="@+id/item_layout_container"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/item_layout_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceListItemSmall"
        android:gravity="center_vertical"
        android:minHeight="?android:attr/listPreferredItemHeightSmall"/>

</FrameLayout>

有关CustomAdapters和RecyclerView的更多信息,请参阅官方文档中的培训

快速滚动问题

使用此方法可能会导致快速滚动时出现问题。当动画正在进行时,视图可能会被重用。为了避免这种情况,建议在分离时清除动画。

    @Override
    public void onViewDetachedFromWindow(final RecyclerView.ViewHolder holder)
    {
        ((CustomViewHolder)holder).clearAnimation();
    }

关于CustomViewHolder:

    public void clearAnimation()
    {
        mRootLayout.clearAnimation();
    }

新回答:

看一下Gabriele Mariotti的仓库,我相信你会找到所需的内容。他为RecyclerView提供了简单的ItemAnimators,例如SlideInItemAnimator或SlideScaleItemAnimator。


22
请问您是否遇到并解决了RecyclerView中动画卡顿的问题?我曾使用类似的ListView代码,无论滚动速度有多快,我的项目都能够成功实现动画效果,但是在RecyclerView中,如果我快速滚动,有些项目有时会停留在屏幕上,位于其他项目之上,并且它们不会完全隐藏 - 就像动画在完成之前就停止了一样。实际上,我尝试将填充字段的代码块注释掉(以加快onBindViewHolder方法的执行速度),因此我只保留了动画的代码。 - Tomislav
4
谢谢这个了不起的动画,看起来在缓慢滚动时很好,但在快速滚动时,RecyclerView的项目会重叠。您是否遇到过这个问题?有什么建议可以解决这个问题吗? - Giru Bhai
42
请重写onViewDetachedFromWindow方法,并在视图上调用clearAnimation。问题在于当RecyclerView尝试重用视图时,可能会有正在运行的动画。 - Xample
1
@AshokVarma 即将消失的视图将被传递到 onViewDetachedFromWindow - Xample
1
在RecyclerView中从外部(例如从适配器)动画化项目是非常糟糕的做法,因为RecyclerView在动画化时很容易重新绑定您的持有者。这会导致这些持有者处于未定义状态和奇怪的错误。我不知道如何在视图出现时动画化它们,但我敢打赌,不要在RecyclerView之外动画化这些视图,这将节省您很多神经。 - s.maks
显示剩余26条评论

131

仅使用XML的简单制作

访问Gist链接

res/anim/layout_animation.xml

<?xml version="1.0" encoding="utf-8"?>
    <layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
        android:animation="@anim/item_animation_fall_down"
        android:animationOrder="normal"
        android:delay="15%" />

res/anim/item_animation_fall_down.xml

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500">

    <translate
        android:fromYDelta="-20%"
        android:toYDelta="0"
        android:interpolator="@android:anim/decelerate_interpolator"
        />

    <alpha
        android:fromAlpha="0"
        android:toAlpha="1"
        android:interpolator="@android:anim/decelerate_interpolator"
        />

    <scale
        android:fromXScale="105%"
        android:fromYScale="105%"
        android:toXScale="100%"
        android:toYScale="100%"
        android:pivotX="50%"
        android:pivotY="50%"
        android:interpolator="@android:anim/decelerate_interpolator"
        />

</set>

在布局和RecyclerView中使用:

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layoutAnimation="@anim/layout_animation"
    app:layout_behavior="@string/appbar_scrolling_view_behavior" />

2
通过更改动画文件。 参考:https://dev59.com/oW435IYBdhLWcg3w4Uas - iamnaran
1
这绝对是最实用的答案。 - Oliver Metz
2
如何让它每次打开列表时都能正常工作,因为现在它只在第一次运行时有效。 - hewa jalal
44
如果不在数据集更改后调用recyclerView.scheduleLayoutAnimation(),动画将无法正常工作。请注意不要改变原本的意思,让翻译更易懂。 - zeleven
1
这个方案是否适用于当项目被回收并重新显示时?我尝试了这个解决方案,对于最初的动画效果非常好。但是在滚动后,当项目重新显示时,它们没有动画效果。 - Jason p
显示剩余5条评论

74

我在下面的代码中实现了当Recyclerview的项首次出现时淡入动画。也许对某些人有用。

private final static int FADE_DURATION = 1000; //FADE_DURATION in milliseconds

@Override
public void onBindViewHolder(ViewHolder holder, int position) {

    holder.getTextView().setText("some text");

    // Set the view to fade in
    setFadeAnimation(holder.itemView);            
}

private void setFadeAnimation(View view) {
    AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f);
    anim.setDuration(FADE_DURATION);
    view.startAnimation(anim);
}

你也可以使用以下的 setScaleAnimation() 替换 setFadeAnimation(),通过缩放动画从一个点开始来实现物品的出现动画:

private void setScaleAnimation(View view) {
    ScaleAnimation anim = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
    anim.setDuration(FADE_DURATION);
    view.startAnimation(anim);
}

以上代码存在一些缺陷,因为当你滚动 RecyclerView 时,项目会经常淡入淡出或缩放。如果你愿意,可以添加代码以仅在包含 RecyclerView 的片段或活动首次创建时允许动画发生(例如,在创建时获取系统时间,并仅允许前 FADE_DURATION 毫秒进行动画)。


1
我对你的答案进行了小修改,使得动画只在向下滚动时起作用,请查看我的回答 - Basheer AL-MOMANI
1
在快速向上和向下滚动时,这会破坏布局(覆盖列表项,某些项目具有错误的文本颜色)。 - mrid
这不是适当或推荐的动画RecyclerView项的方式。您必须使用ItemAnimator类。 - Eco4ndly

33

我根据pbm的回答创建了一个动画,只进行了少量的修改,使得动画仅运行一次。

换句话说,滚动到页面后才会出现该动画

private int lastPosition = -1;

private void setAnimation(View viewToAnimate, int position) {
    // If the bound view wasn't previously displayed on screen, it's animated
    if (position > lastPosition) {
        ScaleAnimation anim = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        anim.setDuration(new Random().nextInt(501));//to make duration random number between [0,501)
        viewToAnimate.startAnimation(anim);
        lastPosition = position;
    }
}

onBindViewHolder中调用该函数。
@Override
public void onBindViewHolder(ViewHolder holder, int position) {

holder.getTextView().setText("some text");

// call Animation function
setAnimation(holder.itemView, position);            
}

1
这里的lastPosition是什么?它是arrayList.size() - 1吗? - Sumit Shukla
1
lastPosition 表示已渲染视图的数量,因此在开始时它的值为 -1,每次渲染新视图时我们都会启动动画并增加位置。 - Basheer AL-MOMANI

17

这对我来说在recyclerview首次加载时有效,但在添加新项时无效。(虽然我猜那就是问题所在,做得好) - C. Skjerdal
这是完美的答案。 - i.am.jabi

10

建议从这里开始: https://github.com/wasabeef/recyclerview-animators/blob/master/animators/src/main/java/jp/wasabeef/recyclerview/adapters/AnimationAdapter.java

你甚至不需要整个库,那个类就足够了。 然后,如果你实现了一个自己的适配器类,并像这样提供一个动画:

@Override
protected Animator[] getAnimators(View view) {
    return new Animator[]{
            ObjectAnimator.ofFloat(view, "translationY", view.getMeasuredHeight(), 0)
    };
}

@Override
public long getItemId(final int position) {
    return getWrappedAdapter().getItemId(position);
}

当滚动时,您将看到项目从底部出现,并避免快速滚动所带来的问题。


6
将此方法添加到您的RecyclerView适配器中。
private void setZoomInAnimation(View view) {
        Animation zoomIn = AnimationUtils.loadAnimation(context, R.anim.zoomin);// animation file 
        view.startAnimation(zoomIn);
    }

最后,在onBindViewHolder中添加以下代码行: setZoomInAnimation(holder.itemView);

6
当适配器绑定在recyclerview上时,对item进行动画可能不是最好的选择,因为这会导致recyclerview中的项目以不同的速度动画。在我的情况下,位于recyclerview末尾的项比顶部的项更快地动画到它们的位置,因为顶部的项需要移动的距离更远,所以看起来很不整齐。
我用来将每个项目动画到recyclerview中的原始代码可以在此处找到:

http://frogermcs.github.io/Instagram-with-Material-Design-concept-is-getting-real/

但是,如果链接失效,我会复制并粘贴代码。
第一步:将以下代码放置在onCreate方法中,以确保动画仅运行一次:
if (savedInstanceState == null) {
    pendingIntroAnimation = true;
}

步骤2:您需要将此代码放入您想要开始动画的方法中:

if (pendingIntroAnimation) {
    pendingIntroAnimation = false;
    startIntroAnimation();
}

在这个链接中,作者正在对工具栏图标进行动画处理,因此他把它放在了这个方法里:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.menu_main, menu);
    inboxMenuItem = menu.findItem(R.id.action_inbox);
    inboxMenuItem.setActionView(R.layout.menu_item_view);
    if (pendingIntroAnimation) {
        pendingIntroAnimation = false;
        startIntroAnimation();
    }
    return true;
}

步骤 3:现在编写 startIntroAnimation() 的逻辑:

private static final int ANIM_DURATION_TOOLBAR = 300;

private void startIntroAnimation() {
    btnCreate.setTranslationY(2 * getResources().getDimensionPixelOffset(R.dimen.btn_fab_size));

    int actionbarSize = Utils.dpToPx(56);
    toolbar.setTranslationY(-actionbarSize);
    ivLogo.setTranslationY(-actionbarSize);
    inboxMenuItem.getActionView().setTranslationY(-actionbarSize);

    toolbar.animate()
            .translationY(0)
            .setDuration(ANIM_DURATION_TOOLBAR)
            .setStartDelay(300);
    ivLogo.animate()
            .translationY(0)
            .setDuration(ANIM_DURATION_TOOLBAR)
            .setStartDelay(400);
    inboxMenuItem.getActionView().animate()
            .translationY(0)
            .setDuration(ANIM_DURATION_TOOLBAR)
            .setStartDelay(500)
            .setListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    startContentAnimation();
                }
            })
            .start();
}

我的首选方案:

我更倾向于对整个recyclerview进行动画处理,而不是对其中的项进行动画处理。

步骤1和2保持不变。

在步骤3中,一旦API调用返回数据,我将立即开始动画处理。

private void startIntroAnimation() {
    recyclerview.setTranslationY(latestPostRecyclerview.getHeight());
    recyclerview.setAlpha(0f);
    recyclerview.animate()
            .translationY(0)
            .setDuration(400)
            .alpha(1f)
            .setInterpolator(new AccelerateDecelerateInterpolator())
            .start();
}

这将使您的整个recyclerview动画效果,从屏幕底部飞入。

你正在谈论如何为整个RecyclerView添加动画效果。 - Longerian
是的 - 我同意。你应该阅读第一段,了解我为什么认为动画整个RecyclerView比每个项目都动画更好的想法。你可以尝试为每个项目添加动画,但效果不会很好。 - Simon
1
latestPostRecyclerview是什么? - Antonio
1
你有没有读我回答的第一段,我在那里说明为什么动画化项目视图不是一个好主意?另外,我建议先尝试接受的解决方案,然后你会意识到问题,再回来尝试这个解决方案。 - Simon

4

2019年,我建议将所有的项目动画放入ItemAnimator中。

让我们从在recycler-view中声明动画开始:

with(view.recycler_view) {
adapter = Adapter()
itemAnimator = CustomAnimator()
}

声明自定义动画器,然后,
class CustomAnimator() : DefaultItemAnimator() {

     override fun animateAppearance(
       holder: RecyclerView.ViewHolder,
       preInfo: ItemHolderInfo?,
       postInfo: ItemHolderInfo): Boolean{} // declare  what happens when a item appears on the recycler view

     override fun animatePersistence(
       holder: RecyclerView.ViewHolder,
       preInfo: ItemHolderInfo,
       postInfo: ItemHolderInfo): Boolean {} // declare animation for items that persist in a recycler view even when the items change

}

类似上面的动画,有消失动画animateDisappearance,添加动画animateAdd,改变动画animateChange和移动动画animateMove

一个重要的点是在其中调用正确的动画派发器。


您能提供一个使用此覆盖函数的自定义外观动画示例吗?我找不到一个例子,也不确定我是否只需要在函数中指定动画。 - m.i.n.a.r.
1
我在快速搜索中找到了这个,它展示了重载的工作原理。 - Dinesh

2
我认为最好这样使用:(在RecyclerView适配器中只重写一个方法)
override fun onViewAttachedToWindow(holder: ViewHolder) {
    super.onViewAttachedToWindow(holder)

    setBindAnimation(holder)
}

如果您想在RV中查看每个附加动画。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接