如何实现ListView Item的动画?

本文通过分析AnimationListView控件,讲解如何在Android中为ListView的Item添加动画效果。重点介绍了AdapterWrapper内部类、关键属性以及动画实现的prepareAnimation和doAnimation方法,包括animatePreLayout和animatePostLayout两个子方法,利用属性动画实现ListView项的增删动画。

本文是通过分析github上的cypressious-AnimationListView控件来讲述如何给ListView的Item添加动画效果。先看看它的效果吧

图片来自cypressious的github项目示例图
这里写图片描述

这个AnimationListView是我使用AnimationListView来实现的一个例子,在AnimationListView.java中添加了一些注释,方便理解。在例子中还包含了SwipeLayout控件,所以最后的实现效果是这样的。

这里写图片描述

ok,下面开始分析实现原理。(本文主要分析ListView的动画实现,不会讲述swipeLayout——也就是横向滑动删除的效果。)

1.AdapterWrapper内部类

上面提供了代码的下载链接,最好还是下载一份对照着来看文章的分析。

AnimationListView.java文件一打开我们就会看到一个叫做AdapterWrapper的内部类,这个类是对用户的Adapter做一下包装,避免在动画执行的时候,用户调用adapter.notifyDataSetChanged()方法更新数据。
主要原理就是,启用一个AdapterWrapper类将用户的Adapter进行包装,而真正与ListView进行绑定的是新的AdapterWrapper对象。换句话说就是用户的Adapter是无法影响ListView的。当然,为了使我们的ListView可以根据用户的Adpater的来更新界面,在AdapterWrapper中创建了一个DataSetObserver对象,并将其注册给用户的Adapter,这样一来用户调用他的adapter.notifyDataSetChanged()时,我们的ListView可以更新到最新的界面。
这样我们只需要在DataSetObserver对象种做一些过滤处理,即可屏蔽用户Adapter的数据变更的影响了。
下面是这部分的具体代码

 private static class AdapterWrapper extends BaseAdapter {
        private final ListAdapter adapter;
        private boolean mayNotify = true;

        //实例化一个observer,用来观察原adapter的数据变化
        private final DataSetObserver observer = new DataSetObserver() {
            @Override
            public void onChanged() {
                if (mayNotify) { //动画执行时,会屏蔽原adapter的数据变化
                    notifyDataSetChanged();
                }
            }

            @Override
            public void onInvalidated() {
                notifyDataSetInvalidated();
            };
        };

        public AdapterWrapper(final ListAdapter adapter) {
            this.adapter = adapter;
            //将observer注册到原adapter中,以观察其数据变化
            adapter.registerDataSetObserver(observer);
        }
        //设置是否屏蔽数据更新
        public void setMayNotify(final boolean mayNotify) {
            this.mayNotify = mayNotify;
        }

        @Override
        public int getCount() {
            return adapter.getCount();
        }

        @Override
        public Object getItem(final int position) {
            return adapter.getItem(position);
        }

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

        @Override
        public boolean hasStableIds() {
            return adapter.hasStableIds();
        }

        @Override
        public View getView(final int position, final View convertView, final ViewGroup parent) {
            return adapter.getView(position, convertView, parent);
        }

    }

在知道DataSetObserver的作用之后,这部分代码应该就很好理解了,其余的都是对原Adapter的方法进行一下包装而已。

2.比较重要的属性

    //map<id,item的y坐标>
    protected final Map<Long, Float> yMap = new HashMap<Long, Float>();
    //map<id,position>
    protected final Map<Long, Integer> positionMap = new HashMap<Long, Integer>();
    //collection<id>
    protected final Collection<Long> beforeVisible = new HashSet<>();//可见Item之前的Item集合
    protected final Collection<Long> afterVisible = new HashSet<>();//可见Item之后的Item集合
    //等待执行的操作器(Manipulator)列表
    private final List<Manipulator> pendingManipulations = new ArrayList<>();//带动画
    private final List<Manipulator> pendingManipulationsWithoutAnimation = new ArrayList<>();//无动画

根据属性名和注释大家应该就可以知道这些属性的用处了,但是这里要注意一点:
id—–每个item数据的id值,这个必须要有,而且必须是唯一的。
相信从上面的代码也可以看出来,四条数据都跟id有关。
而且,在编写我们的adapter时,需要重写hasStableIds()方法,并且返回true。

3.动画实现

先简单说一下这个接口

public static interface Manipulator<T extends ListAdapter> {
        void manipulate(T adapter);
    }

这个主要是给用户实现的,在manipulate()方法中实现数据的更改(增加或删除)。可以说用户对listView的修改都是通过这个接口传递进来的。
然后,再来看处理动画的主要方法

    //处理动画
    public <T extends ListAdapter> void manipulate(final Manipulator<T> manipulator) {
        if (!animating) {
            prepareAnimation();

            manipulator.manipulate((T) adapter.adapter);

            doAnimation();
        } else {
            pendingManipulations.add(manipulator);
        }
    }

上面我们说用户对listView的修改都是通过Manipulator接口来实现,而具体如何传入到listView中就是通过调用这个方法了,它的参数就是一个Manipulator对象。
在Manipulator对象执行的前后都有一个与动画相关的方法,一个是准备动画prepareAnimation(),一个是动画执行doAnimation()。

3.1 perpareAnimation方法

这个不是特别复杂,我们直接上代码吧

private void prepareAnimation() {
        yMap.clear();
        positionMap.clear();
        beforeVisible.clear();
        afterVisible.clear();

        adapter.setMayNotify(false); //禁用listView更新界面  用户的adapter被AdapterWrapper代替,是否更新数据由AdapterWrapper管理

        final int childCount = getChildCount();//获取屏幕内的item数量

        final int firstVisiblePosition = getFirstVisiblePosition();

        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final long id = adapter.getItemId(firstVisiblePosition + i);

            yMap.put(id, ViewHelper.getY(child)); //保存屏幕内Item的Y坐标
            positionMap.put(id, firstVisiblePosition + i);//保存屏幕内Item的position位置
        }

        for (int i = 0; i < firstVisiblePosition; i++) {
            final long id = adapter.getItemId(i);
            beforeVisible.add(id); //保存第一个可见Item之前的Item的id值
        }

        final int count = adapter.getCount();

        for (int i = firstVisiblePosition + childCount; i < count; i++) {
            final long id = adapter.getItemId(i);
            afterVisible.add(id);  //保存最后一个可见Item之后的Item的id值
        }

    }

主要就是对之前的属性进行赋值,为后面的动画执行做准备。

3.2 doAnimation方法
private void doAnimation() {
        setEnabled(false);//屏蔽listView的事件操作
        animating = true;
        //设置动画执行时间
        final float durationUnit = (float) MAX_ANIM_DURATION / getHeight();

        animatePreLayout(durationUnit, new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(final Animator animation) {
            //重点注意代码
                adapter.notifyDataSetChanged();

                getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {

                    @Override
                    public boolean onPreDraw() {
                        getViewTreeObserver().removeOnPreDrawListener(this);

                        animatePostLayout(durationUnit);

                        return true;
                    }

                });
            }

        });

    }

这里重点要看的是animatePreLayout方法和它的参数AnimatorListenerAdapter对象。
animatePreLayout内部我们暂且不管,肯定是一些动画的具体控制和实现。
这里主要看看AnimatorListenerAdapter的onAnimationEnd方法中的内容。
根据代码可知,首先调用了adapter.notifyDataSetChanged(),将变化后的数据更新到ListView中来显示,那么这里就给我们一个重要的信息——animatePreLayout()中的动画是在ListView界面变化之前执行的
然后是addOnPreDrawListener()方法,这个就像它的方法名展示的一样,在ListView被绘制之前需要执行的代码。在内部仅有一个相关方法——animatePostLayout(durationUnit)方法,另外那条语句是将OnPreDrawListener注销掉,与动画无关。
到这里我们发现真正的重点方法其实是:animatePreLayout()和animatePostLayout()

3.2.1 animatePreLayout

这个方法主要是负责两件事情:
a.将需要删除的Item隐藏(alpha属性动画)
b.将被挤出屏幕的Item移除屏幕(translation属性动画)
这个方法是在ListView界面变化之前执行的,虽然界面没有变化,但是Adapter中的数据已经改变了。
具体就需要大家自己看代码了,我添加了一些注释方便理解。

private void animatePreLayout(final float durationUnit, final AnimatorListener listener) {
        final AnimatorSet animatorSet = new AnimatorSet();

        final int firstVisiblePosition = getFirstVisiblePosition();
        final int childCount = getChildCount();

        for (final Iterator<Entry<Long, Float>> iter = yMap.entrySet().iterator(); iter.hasNext();) { //遍历屏幕中的Item
            final Entry<Long, Float> entry = iter.next();

            final long id = entry.getKey();
            final int oldPos = positionMap.get(id); //之前的位置,界面上的位置
            final View child = getChildAt(oldPos - firstVisiblePosition);
            final int newPos = getPositionForId(id);//数据中的位置,还未更新到界面上去

            //在数据中查找不到位置,则启动隐藏动画
            if (newPos == -1) { 
                final ObjectAnimator anim = animateAlpha(child, false);
                animatorSet.play(anim);

                iter.remove();
                positionMap.remove(id);
                continue;
            }

            //将需要移出屏幕的Item,通过动画移出屏幕
            // translate items that move out of bounds
            if (newPos < firstVisiblePosition || newPos > firstVisiblePosition + childCount) {
                final float offset;

                if (newPos < firstVisiblePosition) {
                    offset = -getHeight();
                } else {
                    offset = getHeight();
                }
                //AnimatorProxy是NineOldAndroids库中的View包装类,用来适应Android3.0以前的版本
                final AnimatorProxy proxy = AnimatorProxy.wrap(child);//why use proxy?
                final ObjectAnimator anim = ObjectAnimator
                                            .ofFloat(proxy, "translationY", 0f,offset);

                final int finalDuration = getDuration(0, getHeight() / 2, durationUnit);

                anim.setInterpolator(new AccelerateInterpolator());
                anim.setDuration((long) (finalDuration * animationDurationFactor));

                animatorSet.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(final Animator animation) {
                        child.post(new Runnable() {

                            @Override
                            public void run() {
                                proxy.setTranslationY(0f);//将child设置回原来的位置,但未更新UI!??
                            }
                        });
                    }
                });
                animatorSet.play(anim);

                iter.remove();
                positionMap.remove(id);
                continue;
            }
        }

        if (!animatorSet.getChildAnimations().isEmpty()) {
            animatorSet.addListener(listener);
            animatorSet.start();
        } else {
            listener.onAnimationEnd(animatorSet);//无动画需要执行,则直接调用listener的方法
        }
    }
3.2.2 animatePostLayout

这个方法也做了两件事情:
a.将新增的Item显示出来(alpha属性动画)
b.将需要移入屏幕显示的Item移入(translation属性动画)
而这个方法是在adapter.notifyDataSetChanged()方法调用之后,且在ListView绘制之前调用的。
然后,你们继续读代码吧。。。

private void animatePostLayout(final float durationUnit) {

        final AnimatorSet animatorSet = new AnimatorSet();

        for (int i = 0; i < getChildCount(); i++) {
            final View child = getChildAt(i);
            final long id = getItemIdAtPosition(getFirstVisiblePosition() + i);

            ObjectAnimator anim = null;

            ViewHelper.setAlpha(child, 1f);

            if (yMap.containsKey(id)) {
                // 移动屏幕中的Item

                // log("Moved within visible area id: " + id);
                final float oldY = yMap.remove(id);
                final float newY = ViewHelper.getY(child);

                if (oldY != newY) {
                    anim = animateY(child, oldY, newY, durationUnit);
                }

            } else {

                if (beforeVisible.contains(id)) {
                    // 从顶部移入的Item
                    final float newY = ViewHelper.getY(child);
                    final float oldY = -child.getHeight();

                    anim = animateY(child, oldY, newY, durationUnit);
                } else if (afterVisible.contains(id)) {
                    // 从底部移入的Item
                    final float newY = ViewHelper.getY(child);
                    final float oldY = getHeight();

                    anim = animateY(child, oldY, newY, durationUnit);
                } else {
                    // 新增的Item
                    ViewHelper.setAlpha(child, 0f);

                    anim = animateAlpha(child, true);
                    anim.setStartDelay(MIN_ANIM_DURATION);
                }

            }

            if (anim != null) {
                animatorSet.play(anim);
            }

        }

        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(final Animator animation) {
                finishAnimation();
            };
        });

        animatorSet.start();
    }

其实动画都是使用属性动画来实现的,也没有特别复杂的动画。主要是ListView展示数据的过程比较复杂,所以当需要添加动画时,会不知道该从何处入手。本人的目的也就是分析一下别人的思路,下次碰到类似的问题可以自己动手解决掉。
还有一些代码没有贴出来,有兴趣的可以下载我的示例自己动手试试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值