RecyclerView零點突破(動畫+邊線篇)
0、前言:
動畫和邊線估計有點冷門,很多人都將就湊合,今天我就來深入講解一下吧
邊線的方案是網上流傳的一種,個人感覺也是最好的,並稍稍改進了一點
本篇使用的測試佈局見上篇: ofollow,noindex">RecyclerView零點突破(基本使用篇)
留圖鎮樓
鎮樓1 | 鎮樓2 |
---|---|
|
|
本系列分為3篇:
- RecyclerView零點突破(基本使用篇)
- RecyclerView零點突破(動畫+邊線篇)
- RecyclerView零點突破(詳細分析篇)
1、動畫--解析內建 DefaultItemAnimator
與自定義
一共就不到700行程式碼,應該能hold住吧
為了方便研究,將 DefaultItemAnimator
拷貝一份到工程中
整體瞭解一下:
DefaultItemAnimator
--> SimpleItemAnimator
--> RecyclerView.ItemAnimator
幾個核心的回撥函式如下:
animator.png
1.1.新增的時候:
預設效果是下面的條目整體下移,之後插入的條目淡入(透明度0~1)
預設插入動畫.gif
1.1.1:檢視新增時函式的執行情況
新增分析.png
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裡的動畫是在移動結束後呼叫的
自定義新增動畫.gif
-->[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:三個建構函式:
分割線.png
2.1.2:三種樣式:
Image 17.png
2.2:程式碼實現
2.2.1:使用
//mIdRvGoods.addItemDecoration(new RVItemDivider(this, RVItemDivider.Type.HORIZONTAL,10,Color.BLACK)); //mIdRvGoods.addItemDecoration(new RVItemDivider(this, RVItemDivider.Type.HORIZONTAL)); mIdRvGoods.addItemDecoration(new RVItemDivider(this, RVItemDivider.Type.BOTH,R.drawable.shape_div)); //mIdRvGoods.addItemDecoration(new RVItemDivider(this, RVItemDivider.Type.BOTH,10,Color.BLACK));
2.2.2:程式碼實現
/** * 作者:張風捷特烈<br/> * 時間:2018/12/3 0003:10:36<br/> * 郵箱:[email protected]<br/> * 說明:RecyclerView的分割線 */ public class RVItemDivider extends RecyclerView.ItemDecoration { public enum Type { VERTICAL,//豎直線 HORIZONTAL,//水平線 BOTH;//水平+垂直 } 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 parentRecyclerView * @param state狀態 */ @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); if (mOrientation == Type.VERTICAL||mOrientation == Type.BOTH) { outRect.set(0, 0, 0, mDividerHeight);//橫線矩框 } else { 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 BOTH: drawVertical(canvas, parent);//豎線矩框 drawHorizontal(canvas, parent);//橫線矩框 break; 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.本文成長記錄及勘誤表
Android_Material_Design_Test/tree/master/app/src/main/java/com/toly1994/vvi_mds/pkg_08_other" target="_blank" rel="nofollow,noindex">專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--github | 2018-12-5 | RecyclerView零點突破(動畫+邊線篇) |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的掘金 | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援
icon_wx_200.png