RecyclerView零点突破(动画+边线篇)
2018-12-05 本文已影响99人
e4e52c116681
0、前言:
动画和边线估计有点冷门,很多人都将就凑合,今天我就来深入讲解一下吧
边线的方案是网上流传的一种,个人感觉也是最好的,并稍稍改进了一点
本篇使用的测试布局见上篇:RecyclerView零点突破(基本使用篇)
留图镇楼
镇楼1 | 镇楼2 |
---|---|
![]() |
![]() |
本系列分为3篇:
- RecyclerView零点突破(基本使用篇)
- RecyclerView零点突破(动画+边线篇)
- RecyclerView零点突破(详细分析篇)

1、动画--解析内置DefaultItemAnimator
与自定义
一共就不到700行代码,应该能hold住吧
为了方便研究,将DefaultItemAnimator
拷贝一份到工程中
整体了解一下:
DefaultItemAnimator
-->SimpleItemAnimator
-->RecyclerView.ItemAnimator
几个核心的回调函数如下:

1.1.添加的时候:
默认效果是下面的条目整体下移,之后插入的条目淡入(透明度0~1)

1.1.1:查看添加时函数的执行情况

animateMove、endAnimationy一对调用了10次
animateAdd、endAnimation一对调用了1次
最后调用了runPendingAnimations
animateMove的最大的条目position是:11,也就是当前页面的最大Position
经多次测试:
插入位置之后的所有当前页的条目都会响应animateMove方法,且执行的先后顺序是随机的
插入目标的条目响应animateAdd方法
1.1.2:animateAdd分析
-->[DefaultItemAnimator#animateAdd]
-----------------------------------------------------
@Override
public boolean animateAdd(final ViewHolder holder) {
resetAnimation(holder);//重置动画
holder.itemView.setAlpha(0);//将该条目透明度设为0,也就是点击时的空白区域
mPendingAdditions.add(holder);//将这个透明的条目加入mPendingAdditions列表
return true;
}
-->[DefaultItemAnimator#animateAdd]
-----------------------------------------------------
private void resetAnimation(ViewHolder holder) {
if (sDefaultInterpolator == null) {
sDefaultInterpolator = new ValueAnimator().getInterpolator();
}
holder.itemView.animate().setInterpolator(sDefaultInterpolator);
endAnimation(holder);
}
-->[待添加的ViewHolder列表]
-----------------------------------------------------
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
1.1.3:mPendingAdditions的endAnimation分析
@Override
public void endAnimation(ViewHolder item) {
Log.e(TAG, "endAnimation: ");
final View view = item.itemView;//条目视图
view.animate().cancel();//先取消条目视图的动画
//略n行....
//添加的条目布局列表:mPendingAdditions
if (mPendingAdditions.remove(item)) {//移除该条目
view.setAlpha(1);//将该条目透明度设为1
dispatchAddFinished(item);
}
//略n行....
dispatchFinishedWhenDone();
}
1.1.4:mPendingAdditions在runPendingAnimations中
-->[ArrayList<ViewHolder>列表]
-----------------------------------------------------
ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
-->[DefaultItemAnimator#runPendingAnimations]
-----------------------------------------------------
@Override
public void runPendingAnimations() {
//mPendingAdditions不为空,可以添加
boolean additionsPending = !mPendingAdditions.isEmpty();
//additionsPending为false可导致直接返回,不执行动画
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
return;
}
//略n行....
if (additionsPending) {
final ArrayList<ViewHolder> additions = new ArrayList<>();
additions.addAll(mPendingAdditions);//将mPendingAdditions的视图装到additions
mAdditionsList.add(additions);//mAdditionsList的盒子装additions
mPendingAdditions.clear();//mPendingAdditions光荣下岗
Runnable adder = new Runnable() {//居然是Runnable...记住这小子的名字[adder]
@Override
public void run() {
for (ViewHolder holder : additions) {//遍历:additions
animateAddImpl(holder);//----动画的核心----
}
additions.clear();//清空additions
mAdditionsList.remove(additions);//移除additions
}
};
if (removalsPending || movesPending || changesPending) {//如果有其他的动画待执行
long removeDuration = removalsPending ? getRemoveDuration() : 0;
long moveDuration = movesPending ? getMoveDuration() : 0;
long changeDuration = changesPending ? getChangeDuration() : 0;
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
View view = additions.get(0).itemView;
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
} else {
adder.run();//[adder]走起
}
}
}
-->[要进行添加动画的ViewHolder]
-----------------------------------------------------
ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
-->[DefaultItemAnimator#animateAddImpl]
-----------------------------------------------------
void animateAddImpl(final ViewHolder holder) {
final View view = holder.itemView;//获取布局视图
final ViewPropertyAnimator animation = view.animate();//获取视图的animate
mAddAnimations.add(holder);//mAddAnimations篮子装一下
animation.alpha(1).setDuration(getAddDuration())//tag1:默认时长120ms---执行透明度动画
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchAddStarting(holder);
}
@Override//取消动画时将Alpha设为1
public void onAnimationCancel(Animator animator) {
view.setAlpha(1);
}
@Override//办完事,清场,该走的走,该清的清
public void onAnimationEnd(Animator animator) {
animation.setListener(null);
dispatchAddFinished(holder);
mAddAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
-->[android.support.v7.widget.RecyclerView.ItemAnimator#getAddDuration]
------------------------tag1-----------------------------
private long mAddDuration = 120;
public long getAddDuration() {
return mAddDuration;
}
1.2:自定义添加动画
1.2.1:定点旋转
既然分析到它是怎么动起来的,当然可以改一下,比如:
注意:animateAddImpl里的动画是在移动结束后调用的

-->[RItemAnimator#animateAddImpl]
-----------------------------------------------------
animation.rotation(360).setDuration(1000)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
view.setAlpha(1);
1.2.2抖动
缩放抖动 | 移动抖动 |
---|---|
![]() |
![]() |
感觉ViewPropertyAnimator用得不怎么爽,还是用AnimatorSet+ObjectAnimator吧
用AnimatorSet装一下效果,可以实现更复杂的多动画叠加,然后添加监听,和源码保持一致
一直想做条目抖动效果,总是实现了,如果不会用ObjectAnimator的童鞋,可以参见
void animateAddImpl(final ViewHolder holder) {
final View view = holder.itemView;
mAddAnimations.add(holder);
ObjectAnimator translationX = ObjectAnimator//创建实例
//(View,属性名,初始化值,结束值)
.ofFloat(view, "translationX", 0, 20, -20, 0, 20, -20, 0, 20, -20, 0)
.setDuration(300);//设置时长
ObjectAnimator scaleX = ObjectAnimator//创建实例
//(View,属性名,初始化值,结束值)
.ofFloat(view, "scaleX", 1, 0.95f, 1.05f, 1, 0.95f, 1.05f, 1, 0.95f, 1.05f,1)
.setDuration(300);//设置时长
AnimatorSet set = new AnimatorSet();
set.playTogether(scaleX,translationX);//两个效果一起
//set.playSequentially(translationX);//添加动画
//set.playSequentially(scaleX);//添加动画
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
view.setAlpha(1);
}
@Override
public void onAnimationEnd(Animator animation) {
dispatchAddFinished(holder);
mAddAnimations.remove(holder);
dispatchFinishedWhenDone();
}
@Override
public void onAnimationStart(Animator animation) {
view.setAlpha(1);
dispatchAddStarting(holder);
}
});
set.start();
}
1.2.3:定轴旋转
rotationX | rotationY |
---|---|
![]() |
![]() |
//定轴旋转
ObjectAnimator rotationY = ObjectAnimator//创建实例
//(View,属性名,初始化值,结束值)
.ofFloat(view, "rotationY", 0,360)
.setDuration(1000);//设置时长
ObjectAnimator rotationX = ObjectAnimator//创建实例
//(View,属性名,初始化值,结束值)
.ofFloat(view, "rotationX", 0,360)
.setDuration(1000);//设置时长
1.3:插入下item的动画:
效果1 | 效果2 |
---|---|
![]() |
![]() |
1.3.1:简析:
分析同添加:运动核心在DefaultItemAnimator#animateMoveImpl方法里,相关集合:
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
-->[下面的条目执行:animateMove()]
-----------------------------------------------------
@Override
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
int toX, int toY) {
final View view = holder.itemView;//获取item视图View
fromX += (int) holder.itemView.getTranslationX();
fromY += (int) holder.itemView.getTranslationY();
resetAnimation(holder);
int deltaX = toX - fromX;
int deltaY = toY - fromY;
if (deltaX == 0 && deltaY == 0) {
dispatchMoveFinished(holder);
return false;
}//尺寸计算
if (deltaX != 0) {//对item视图进行平移
view.setTranslationX(-deltaX);
}
if (deltaY != 0) {
view.setTranslationY(-deltaY);
}
//mPendingMoves添加MoveInfo---移动的相关信息封装在MoveInfo中,相当于封装属性的空壳类
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
return true;
}
-->[mPendingMoves在runPendingAnimations()中的表现]
-----------------------------------------------------
boolean movesPending = !mPendingMoves.isEmpty();
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
// nothing to animate
return;
}
//和添加是一个套路---核心运动方法在:animateMoveImpl
if (movesPending) {
final ArrayList<MoveInfo> moves = new ArrayList<>();
moves.addAll(mPendingMoves);
mMovesList.add(moves);
mPendingMoves.clear();
Runnable mover = new Runnable() {
@Override
public void run() {
for (MoveInfo moveInfo : moves) {
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
moveInfo.toX, moveInfo.toY);
}
moves.clear();
mMovesList.remove(moves);
}
};
-->[mPendingMoves在runPendingAnimations()中的表现]
-----------------------------------------------------
void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
final View view = holder.itemView;
final int deltaX = toX - fromX;
final int deltaY = toY - fromY;
if (deltaX != 0) {
view.animate().translationX(0);
}
if (deltaY != 0) {
view.animate().translationY(0);
}
final ViewPropertyAnimator animation = view.animate();
mMoveAnimations.add(holder);
//运动的逻辑(此处无特效):
animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchMoveStarting(holder);
}
@Override
public void onAnimationCancel(Animator animator) {
if (deltaX != 0) {
view.setTranslationX(0);
}
if (deltaY != 0) {
view.setTranslationY(0);
}
}
@Override
public void onAnimationEnd(Animator animator) {
animation.setListener(null);
dispatchMoveFinished(holder);
mMoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
1.3.2:效果1:
-->[animateMoveImpl()中]
-----------------------------------------------------
//定轴旋转
ObjectAnimator//创建实例
.ofFloat(view, "rotationX", 0, 360)
.setDuration(1000).start();//设置时长
1.3.3:效果2:
-->[animateMoveImpl()中]
-----------------------------------------------------
ObjectAnimator//创建实例
.ofFloat(view, "ScaleX", 1, 0.5f, 1.2f,0.8f,1)
.setDuration(1000).start();//设置时长
ObjectAnimator//创建实例
.ofFloat(view, "ScaleY", 1, 0.5f, 1.2f,0.8f,1)
.setDuration(1000).start();//设置时长
1.4:小结
移除貌似没有对当前item的特效,对item下面的特效还是在animateMoveImpl
更新数据的item的特效在:animateChangeImpl()都是一个套路,这里就不赘述了
将上篇的视图改改就能实现镇楼图了,这里也不赘述了
其实看懂了DefaultItemAnimator,item的动画也不是很难
貌似有个动画库,个人感觉没有必要,拿DefaultItemAnimator稍微改几句就行了
毕竟需求是不断变动的,一个库不可能涵盖所以需求,而且很多用不到的特效还占空间
微妙的修整还是要懂才行,能应对变化的只有变化本身,记住修改效果的地方:
更新数据:animateChangeImpl()
添加数据:animateAddImpl()
移动:animateMoveImpl()
2.边线的绘制:
缺陷:对于网格和瀑布流结尾处处理欠佳(不过这两种布局一般都不用边线)
2.1:效果一览
2.1.1:三个构造函数:

2.1.2:三种样式:

2.2:代码实现
2.2.1:使用
//mIdRvGoods.addItemDecoration(new RVItemDivider(this, RVItemDivider.Type.HORIZONTAL,10,Color.BLACK));
//mIdRvGoods.addItemDecoration(new RVItemDivider(this, RVItemDivider.Type.VERTICAL));
//水平加竖直
mIdRvGoods.addItemDecoration(new RVItemDivider(this, RVItemDivider.Type.VERTICAL,10,Color.BLACK));
mIdRvGoods.addItemDecoration(new RVItemDivider(this, RVItemDivider.Type.HORIZONTAL, 10, Color.BLACK));
2.2.2:代码实现
/**
* 作者:张风捷特烈<br/>
* 时间:2018/12/3 0003:10:36<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:RecyclerView的分割线
*/
public class RVItemDivider extends RecyclerView.ItemDecoration {
public enum Type {
VERTICAL,//竖直线
HORIZONTAL,//水平线
}
private Paint mPaint;//画笔
private Drawable mDivider;//Drawable分割线
private int mDividerHeight = 1;//分割线高度,默认为1px
private Type mOrientation;//线的方向
private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
public RVItemDivider(Context context, Type orientation) {
mOrientation = orientation;
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
}
public RVItemDivider(Context context, Type orientation, int drawableId) {
this(context, orientation);
mDivider = ContextCompat.getDrawable(context, drawableId);
mDividerHeight = mDivider.getIntrinsicHeight();
}
/**
* 自定义分割线
*
* @param context 上下文
* @param orientation 列表方向
* @param dividerHeight 分割线高度
* @param dividerColor 分割线颜色
*/
public RVItemDivider(Context context, Type orientation, int dividerHeight, int dividerColor) {
this(context, orientation);
mDividerHeight = dividerHeight;
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(dividerColor);
mPaint.setStyle(Paint.Style.FILL);
}
/**
* 获取分割线尺寸
*
* @param outRect 线的矩框
* @param view 线
* @param parent RecyclerView
* @param state 状态
*/
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
switch (mOrientation) {
case HORIZONTAL:
outRect.set(0, 0, 0, mDividerHeight);//横线矩框
break;
case VERTICAL:
outRect.set(0, 0, mDividerHeight, 0);//横线矩框
}
}
/**
* 绘制分割线
*
* @param canvas 画布
* @param parent RecyclerView
* @param state 状态
*/
@Override
public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
super.onDraw(canvas, parent, state);
switch (mOrientation) {
case VERTICAL:
drawVertical(canvas, parent);//竖线矩框
break;
case HORIZONTAL:
drawHorizontal(canvas, parent);//横线矩框
break;
}
}
/**
* 绘制水平线
*
* @param canvas 画布
* @param parent RecyclerView
*/
private void drawHorizontal(Canvas canvas, RecyclerView parent) {
final int left = parent.getPaddingLeft();
final int right = parent.getMeasuredWidth() - parent.getPaddingRight();
final int childNum = parent.getChildCount();
for (int i = 0; i < childNum; i++) {//遍历所有的孩子
final View child = parent.getChildAt(i);
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
//线的左上角坐标(itemView底部+边距,itemView底部+边距+线高)
final int top = child.getBottom() + layoutParams.bottomMargin;
final int bottom = top + mDividerHeight;
if (mDivider != null) {//有mDivider时---绘制mDivider
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
if (mPaint != null) {//有mPaint时---绘制矩形
canvas.drawRect(left, top, right, bottom, mPaint);
}
}
}
/**
* 绘制竖直线--------同理
*
* @param canvas 画布
* @param parent RecyclerView
*/
private void drawVertical(Canvas canvas, RecyclerView parent) {
final int top = parent.getPaddingTop();
final int bottom = parent.getMeasuredHeight() - parent.getPaddingBottom();
final int childSize = parent.getChildCount();
for (int i = 0; i < childSize; i++) {
final View child = parent.getChildAt(i);
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
final int left = child.getRight() + layoutParams.rightMargin;
final int right = left + mDividerHeight;
if (mDivider != null) {
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
if (mPaint != null) {
canvas.drawRect(left, top, right, bottom, mPaint);
}
}
}
}
后记:捷文规范
1.本文成长记录及勘误表
项目源码 | 日期 | 备注 |
---|---|---|
V0.1--github | 2018-12-5 | RecyclerView零点突破(动画+边线篇) |
2.更多关于我
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
我的github | 我的简书 | 我的掘金 | 个人网站 |
3.声明
1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持
