一个有吃豆人删除动画的ListView
这是一个无聊的效果,由一个无聊的程序猿,在无聊的情况下写的。
虽然这效果不中看中用,不过就当学习了。
先上图
效果一目了然,主要是:
(1)移除item时执行吃豆人动画
(2)滚动时吃豆人也相应移动
(3)应对可见与不可见状态间的切换
简单原理分析:
(1)吃豆人、豆、和左边的白色矩形(当然所有颜色都是可以改的,你想换成图片也行)都是用canvas画出来的。
(2)问:canvas那里来的?答:ListView的canvas。具体是重写ListView的这个方法:
protected void dispatchDraw(android.graphics.Canvas canvas)
再问:为什么不重写onDraw方法?
答:对于容器控件来说,在绘制时onDraw方法不一定会被调用。因为容器本身没什么可以画的(除非你设置了background或其它什么的),所以只需要调用dispatchDraw把绘制请求分发给子View,让子View把自己画出来就行了。所以重写ViewGroup和其子类时,重写dispatchDraw方法比较保险。
(3)使用Handler每隔一段时间发送一次重绘请求,并且叠加吃豆人、豆、和左边的白色矩形的坐标,直到到达边界
(4)监听scroll事件,这是为了滚动时更新吃豆人、豆、和左边的白色矩形的坐标
(5)写个接口,在吃豆人吃完时可以通知调用者
开始实现:
(1)进行一些包括画笔等的初始化:
/**
* 初始化
*/
private void init() {
paintRect = new Paint();
paintRect.setColor(Color.WHITE);
paintRect.setAntiAlias(true);
paintC = new Paint();
paintC.setColor(Color.YELLOW);
paintC.setAntiAlias(true);
rectDrawRect = new Rect();
rectFDrawArc = new RectF();
super.setOnScrollListener(mScrollListener);
}
两支笔,一支画白色矩形,一支画吃豆人和豆。
两个矩形,一个是画白色矩形的区域,一个是画吃豆人的区域。
最后调用的super.setOnScrollListener(mScrollListener)是设置滚动监听,为了外部也可以进行监听,用了一个成员变量来保存外部设置的OnScrollListener,并在滚动事件发生时同步通知外部的监听器。
/**
* 因为只能设置一个OnScrollListener给ListView 这个用来保存用户设置的OnScrollListener
* 并且在滚动事件中代为通知
*/
private OnScrollListener userScrollListener;
private OnScrollListener mScrollListener = new OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (userScrollListener != null)
// 通知外部监听器
userScrollListener.onScrollStateChanged(view, scrollState);
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// 滚动时要更新位置并且判断是否还是可见状态
if (removeP > -1) {
removeView = getChildAt(removeP - firstVisibleItem);
if (rectDrawRect != null && removeView != null) {
rectDrawRect.top = removeView.getTop();
rectDrawRect.bottom = removeView.getBottom();
}
show = ((removeP >= firstVisibleItem) && (removeP <= (firstVisibleItem + visibleItemCount)));
}
if (userScrollListener != null) {
// 通知外部监听器
userScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
}
(2)建立计算逻辑:
乍一看要计算各部分的坐标挺复杂的,但其实炒鸡简单。
我的思路是首先先思考有哪些是已知的,或者可以很简单就可以得到的。
比如在这里,我们要移除某个item,那我们必定是知道item的position。而通过这个position,我们就可以获得该item的view(怎么获得后面会说),包括view里所包含信息,如四个方向的坐标(left,top,right,bottom)、高度和宽度等(其它的我们不需要就不说了)。
获得这些信息后,再看我们需要的。
首先白色矩形,很容易看出白色矩形的左上和左下顶点是与item的view重合的,所以白色矩形的left,top,bottom应该等于item的view的。至于right,应该是由我们控制来动态变化的,范围应该是从0至item的right。
然后吃豆人更简单了,首先圆心应该在白色矩形右边的中点,这个很容易计算。半径应该为item的高度的一半,这个更容易。
最后,那些豆。跟吃豆人类似,只是半径小点,然后要循环着画直到边界。
好,先看提供给外部调用开始执行吃豆人动画的方法,当然我们的数据也要在这里初始化:
public void startEat(int position) throws Throwable {
if (removeP != -1) {
// 上一个还没吃完
throw new Throwable("You can not eat another item before last one done!");
}
if (position >= getFirstVisiblePosition() && position <= getLastVisiblePosition()) {
// 如果该item状态为可见
show = true;
// 得到要移除的item的view
removeView = getChildAt(position - getFirstVisiblePosition());
// 初始化左边白色矩形的坐标
rectDrawRect.set(removeView.getLeft(), removeView.getTop(), 0, removeView.getBottom());
// 记录item的右边坐标
itemRight = removeView.getRight();
// 记录item的高度
itemHeight = rectDrawRect.bottom - rectDrawRect.top;
// 记录item在数据源中的位置
removeP = position;
// 发送消息,开始吃了
mHandler.sendEmptyMessage(JUST_EAT_IT);
} else {
// item不可见,没必要执行动画,直接通知吃完了
show = false;
removeP = -1;
if (mEatFinishLintener != null) {
mEatFinishLintener.onEatFinish(position);
}
}
}
有一点要说的就是如何通过position得到相应的view。
我们都知道在ViewGroup内部有一个View的数组存储着子View(如果你不知道,那你现在知道了),可以用getChildAt(index)方法取出相应索引的View。
那么现在问题来了,我们要怎么用挖掘机挖出item的view?
好!前排小明举手了,他举手了,此刻爱因斯坦、华罗庚、牛顿灵魂附体,他不是一个人在答题,他不是一个人.........
只见他骄傲地站了起来,挺了挺他那引以为傲的胸脯,自信地答道:View childView = getChildAt(position);
顿时老师欣慰地笑了,你果然没让我失望,果然答错了,现在你可以滚出去了!(感觉到这世界对小明深深的恨意)
小明的答案是错的,原因呢由我小猿来跟大家说一说:
因为ListView里的View数组保存的并不是所有item的view,而只保存了可见的item的view。即该数组的元素个数为ListView可见的item的个数,ListView第一个可见的item的view在该数组中的索引为0。
而这里的position是相对于整个ListView的所有item的(包括可见与不可见的),所以我们要先将position换算成在子view数组中的位置。具体就是减去第一个可见的item的position,而ListView也提供了获取第一个可见item的position的方法(注:这里的position都指的是相对于所有的item而言的)getFirstVisiblePosition()。
所以正确答案应该是:View child = getChildAt(position - getFirstVisiblePosition());(不明白的自己假设几个具体的例子想想就明白了)
小明,你可以安心的去了,叫你跟我抢小红!小红在35岁之前都是我的,35岁之后你想要可以给你。
从此,我跟小红过上了幸福的生活。
让我先陶醉会再继续......
好了,回归正题,至此我们获得了item的View并且对白色矩形的位置坐了初始化。
接下来我们要利用Handler来持续发送消息叠加白色矩形的right,而白色矩形的right也是我们计算吃豆人和豆的坐标信息的基础。
(3)开始进行数据叠加和持续请求重绘
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
// TODO 自动生成的方法存根
switch (msg.what) {
case JUST_EAT_IT:
// 请求重绘
invalidate(rectDrawRect.left, rectDrawRect.top, itemRight, rectDrawRect.bottom);
// 如果已经吃完了
if (rectDrawRect.right >= itemRight) {
show = false;
if (mEatFinishLintener != null) {
// 通知请求者已经吃完了
mEatFinishLintener.onEatFinish(removeP);
}
removeP = -1;
}
// 如果还没吃完
else {
// 判断吃豆人张嘴的边界
if (sweepAngle >= 360) {
csAngle = 10;
cAngle = -20;
} else if (sweepAngle <= 270) {
csAngle = -10;
cAngle = 20;
}
// 计算新的张嘴角度
startAngle += csAngle;
sweepAngle += cAngle;
// 左边矩形向右变长
rectDrawRect.right += 20;
// 发送消息绘制下一帧
mHandler.sendEmptyMessageDelayed(JUST_EAT_IT, 40);
}
break;
}
return false;
}
});
同样地,吃豆人的张嘴动作也是通过在Handler中动态改变startAngle(开始绘制的角度)和sweepAngle(要绘制的度数)来实现地,具体的逻辑自己推敲下并不难。
最后呢,数据基本都计算完成了,开始画画了。
(4)我画啊画:
protected void dispatchDraw(android.graphics.Canvas canvas) {
super.dispatchDraw(canvas);
if (show) {
// 画左边矩形
canvas.drawRect(rectDrawRect, paintRect);
// 计算出画吃豆人的矩形
rectFDrawArc.set(rectDrawRect.right - (itemHeight >> 1), rectDrawRect.top, rectDrawRect.right
+ (itemHeight >> 1), rectDrawRect.bottom);
// 画吃豆人
canvas.drawArc(rectFDrawArc, startAngle, sweepAngle, true, paintC);
// 计算豆的半径和位置
int radius = itemHeight >> 3;//半径
int offset = radius * 4;//间隔
float cy = rectDrawRect.top + (itemHeight >> 1);//圆心的Y坐标
int circlesLeft = rectDrawRect.right + (itemHeight >> 1);
for (int i = itemRight - radius - 10; i >= circlesLeft; i -= offset) {
// 从右向左画豆豆
canvas.drawCircle(i, cy, radius, paintC);
}
}
};
show呢是标识当前该item是否可见,若当前item不可见,计算仍会继续,但并不请求绘制,画了看不到。
计算吃豆人的矩形坐标放在这里而不放在Handler是因为在滚动时Handler会有短暂延时,会导致吃豆人有短暂错位情况发生。
这里使用了一些位操作纯粹是为了提高逼格。
这个纯粹是我无聊写着玩的,其实还有很多地方需要优化,所以欢迎吐槽!
这个无聊的效果的原理其实可以用来实现一些其它ListView效果的,比如一种ListView可以把某个item的view固定在顶部。简单说下思路,以后有机会再详细说。
(1)监听滚动事件,主要判断firstVisibleItem,根据外部提供的逻辑来判断那个item要停靠在顶部
(2)获取要停靠的item的view(后面用mHeadView代指)(不知道怎么获取的再看一遍我与小明和小红之间那些不得不说的故事)
(3)计算mHeadView的位置(其实除了顶部,你想显示在其它地方只要是在屏幕内也是完全可以的)
(4)在dispatchDraw方法中用drawChild(canvas, mHeadView, getDrawingTime());方法画出来。
这篇这么长,还买一送一,所以,快举起你们点赞的鼠标手