RecyclerView零點突破(動畫+邊線篇)

張風捷特烈發表於2018-12-05

0、前言:

動畫和邊線估計有點冷門,很多人都將就湊合,今天我就來深入講解一下吧
邊線的方案是網上流傳的一種,個人感覺也是最好的,並稍稍改進了一點
本篇使用的測試佈局見上篇:RecyclerView零點突破(基本使用篇)

留圖鎮樓
鎮樓1 鎮樓2
RecyclerView零點突破(動畫+邊線篇)
RecyclerView零點突破(動畫+邊線篇)
本系列分為3篇:

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抖動
縮放抖動 移動抖動
RecyclerView零點突破(動畫+邊線篇)
RecyclerView零點突破(動畫+邊線篇)

感覺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
RecyclerView零點突破(動畫+邊線篇)
RecyclerView零點突破(動畫+邊線篇)
//定軸旋轉
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
RecyclerView零點突破(動畫+邊線篇)
RecyclerView零點突破(動畫+邊線篇)
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.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.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的掘金 個人網站
3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援


icon_wx_200.png

相關文章