热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

如何实现ListViewItem的动画?

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

本文是通过分析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
protected final Map yMap = new HashMap();
//map
protected final Map positiOnMap= new HashMap();
//collection
protected final Collection beforeVisible = new HashSet<>();//可见Item之前的Item集合
protected final Collection afterVisible = new HashSet<>();//可见Item之后的Item集合
//等待执行的操作器(Manipulator)列表
private final List pendingManipulatiOns= new ArrayList<>();//带动画
private final List 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 void manipulate(final Manipulator 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 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 final long id = adapter.getItemId(i);
beforeVisible.add(id); //保存第一个可见Item之前的Item的id值
}

final int count = adapter.getCount();

for (int i = firstVisiblePosition + childCount; 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> iter = yMap.entrySet().iterator(); iter.hasNext();) { //遍历屏幕中的Item
final Entry 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 + childCount) {
final float offset;

if (newPos 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 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展示数据的过程比较复杂,所以当需要添加动画时,会不知道该从何处入手。本人的目的也就是分析一下别人的思路,下次碰到类似的问题可以自己动手解决掉。
还有一些代码没有贴出来,有兴趣的可以下载我的示例自己动手试试。


推荐阅读
author-avatar
小超201209
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有