動畫和邊線估計有點冷門,不少人都將就湊合,今天我就來深刻講解一下吧
邊線的方案是網上流傳的一種,我的感受也是最好的,並稍稍改進了一點
本篇使用的測試佈局見上篇:RecyclerView零點突破(基本使用篇)java
鎮樓1 | 鎮樓2 |
---|---|
DefaultItemAnimator
與自定義一共就不到700行代碼,應該能hold住吧
爲了方便研究,將DefaultItemAnimator
拷貝一份到工程中android
DefaultItemAnimator
-->SimpleItemAnimator
-->RecyclerView.ItemAnimator
幾個核心的回調函數以下:git
默認效果是下面的條目總體下移,以後插入的條目淡入(透明度0~1)github
animateMove、endAnimationy一對調用了10次
animateAdd、endAnimation一對調用了1次
最後調用了runPendingAnimations
animateMove的最大的條目position是:11,也就是當前頁面的最大Position
經屢次測試:
插入位置以後的全部當前頁的條目都會響應animateMove方法,且執行的前後順序是隨機的
插入目標的條目響應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<>();
複製代碼
@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();
}
複製代碼
-->[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;
}
複製代碼
既然分析到它是怎麼動起來的,固然能夠改一下,好比:
注意:animateAddImpl裏的動畫是在移動結束後調用的編程
-->[RItemAnimator#animateAddImpl]
-----------------------------------------------------
animation.rotation(360).setDuration(1000)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
view.setAlpha(1);
複製代碼
縮放抖動 | 移動抖動 |
---|---|
感受ViewPropertyAnimator用得不怎麼爽,仍是用AnimatorSet+ObjectAnimator吧
用AnimatorSet裝一下效果,能夠實現更復雜的多動畫疊加,而後添加監聽,和源碼保持一致
一直想作條目抖動效果,老是實現了,若是不會用ObjectAnimator的童鞋,能夠參見canvas
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();
}
複製代碼
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 | 效果2 |
---|---|
分析同添加:運動核心在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();
}
複製代碼
-->[animateMoveImpl()中]
-----------------------------------------------------
//定軸旋轉
ObjectAnimator//建立實例
.ofFloat(view, "rotationX", 0, 360)
.setDuration(1000).start();//設置時長
複製代碼
-->[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();//設置時長
複製代碼
移除貌似沒有對當前item的特效,對item下面的特效仍是在animateMoveImpl
更新數據的item的特效在:animateChangeImpl()都是一個套路,這裏就不贅述了 將上篇的視圖改改就能實現鎮樓圖了,這裏也不贅述了bash
其實看懂了DefaultItemAnimator,item的動畫也不是很難
貌似有個動畫庫,我的感受沒有必要,拿DefaultItemAnimator稍微改幾句就好了
畢竟需求是不斷變更的,一個庫不可能涵蓋因此需求,並且不少用不到的特效還佔空間
微妙的修整仍是要懂才行,能應對變化的只有變化自己,記住修改效果的地方:
更新數據:animateChangeImpl()
添加數據:animateAddImpl()
移動:animateMoveImpl()
複製代碼
缺陷:對於網格和瀑布流結尾到處理欠佳(不過這兩種佈局通常都不用邊線)微信
//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));
複製代碼
/**
* 做者:張風捷特烈<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);
}
}
}
}
複製代碼
項目源碼 | 日期 | 備註 |
---|---|---|
V0.1--github | 2018-12-5 | RecyclerView零點突破(動畫+邊線篇) |
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
個人github | 個人簡書 | 個人掘金 | 我的網站 |
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大編程愛好者共同交流
3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正
4----看到這裏,我在此感謝你的喜歡與支持app