1. 程式人生 > >RecyclerView用法和原始碼深度解析

RecyclerView用法和原始碼深度解析

目錄介紹

  • 1.RecycleView的結構
  • 2.Adapter
    • 2.1 RecyclerView.Adapter扮演的角色
    • 2.2 重寫的方法
    • 2.3 notifyDataSetChanged()重新整理資料
    • 2.4 資料變更通知之觀察者模式
      • a.首先看.notifyDataSetChanged()原始碼
      • b.接著檢視.notifyChanged()原始碼
      • c.接著檢視setAdapter()原始碼中的setAdapterInternal(adapter, false, true)方法
      • d.notify……方法被呼叫,重新整理資料
  • 3.ViewHolder
    • 3.1 ViewHolder的作用
    • 3.2 ViewHolder與複用
    • 3.3 ViewHolder簡單封裝
  • 4.LayoutManager
    • 4.1 作用
    • 4.2 LayoutManager樣式
    • 4.3 LayoutManager當前有且僅有一個抽象函式
    • 4.4 setLayoutManager(LayoutManager layout)原始碼
  • 5.ItemDecoration
    • 5.1 作用
    • 5.2 RecyclerView.ItemDecoration是一個抽象類
    • 5.3 addItemDecoration()原始碼分析
      • a.首先看addItemDecoration原始碼
      • b.接著看下markItemDecorInsetsDirty這個方法
      • c.接著看下mRecycler.markItemDecorInsetsDirty();這個方法
      • d.回過頭在看看addItemDecoration中requestLayout方法
      • e.在 RecyclerView 中搜索 mItemDecorations 集合
  • 6.ItemAnimator
    • 6.1 作用
    • 6.2 觸發的三種事件
  • 7.其他知識點
    • 7.1 Recycler && RecycledViewPool
    • 7.2 Recyclerview.getLayoutPosition()區別
  • 8.RecyclerView巢狀方案滑動衝突解決方案
    • 8.1 如何判斷RecyclerView控制元件滑動到頂部和底部
    • 8.2 RecyclerView巢狀RecyclerView 條目自動上滾的Bug
    • 8.3 ScrollView巢狀RecyclerView滑動衝突
    • 8.4 ViewPager巢狀水平RecyclerView橫向滑動到底後不滑動ViewPager
  • 9.RecyclerView複雜佈局封裝庫案例
    • 9.1 能夠實現業務的需求和功能
    • 9.2 具備的優勢分析
  • 10.針對阿里VLayout程式碼分析
  • 11.版本更新說明
    • v1.0.0 2016年5月5日
    • v1.1.0 更新於2017年2月1日
    • v1.1.1 更新於2017年6月9日
    • v2.0.0 更新於2018年8月21日
    • v2.1.0 更新於2018年9月29日

1.RecycleView的結構

  • 關於RecyclerView,大家都已經很熟悉了,用途十分廣泛,大概結構如下所示
    • RecyclerView.Adapter - 處理資料集合並負責繫結檢視
    • ViewHolder - 持有所有的用於繫結資料或者需要操作的View
    • LayoutManager - 負責擺放檢視等相關操作
    • ItemDecoration - 負責繪製Item附近的分割線
    • ItemAnimator - 為Item的一般操作新增動畫效果,如,增刪條目等
  • 如圖所示,直觀展示結構
    • image
  • 針對上面幾個屬性,最簡單用法如下所示
    recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
    LinearLayoutManager layoutManager = new LinearLayoutManager(this);
    //設定layoutManager
    recyclerView.setLayoutManager(layoutManager);
    final RecycleViewItemLine line = new RecycleViewItemLine(this, LinearLayout.HORIZONTAL,1,this.getResources().getColor(R.color.colorAccent));
    //設定新增分割線
    recyclerView.addItemDecoration(line);
    adapter = new MultipleItemAdapter(this);
    //設定adapter
    recyclerView.setAdapter(adapter);
    //新增資料並且重新整理adapter
    adapter.addAll(list);
    adapter.notifyDataSetChanged();
    
    
    //adapter
    //onCreateViewHolder(ViewGroup parent, int viewType)這裡的第二個引數就是View的型別,可以根據這個型別判斷去建立不同item的ViewHolder
    public class MultipleItemAdapter extends RecyclerView.Adapter<recyclerview.viewholder> {
        public static enum ITEM_TYPE {
            ITEM_TYPE_IMAGE,
            ITEM_TYPE_TEXT
        }
         
        private final LayoutInflater mLayoutInflater;
        private final Context mContext;
        private ArrayList<String> mTitles;
     
        public MultipleItemAdapter(Context context) {
            mContext = context;
            mLayoutInflater = LayoutInflater.from(context);
        }
     
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if (viewType == ITEM_TYPE.ITEM_TYPE_IMAGE.ordinal()) {
                return new ImageViewHolder(mLayoutInflater.inflate(R.layout.item_image, parent, false));
            } else {
                return new TextViewHolder(mLayoutInflater.inflate(R.layout.item_text, parent, false));
            }
        }
     
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            if (holder instanceof TextViewHolder) {
                ((TextViewHolder) holder).mTextView.setText(mTitles[position]);
            } else if (holder instanceof ImageViewHolder) {
                ((ImageViewHolder) holder).mTextView.setText(mTitles[position]);
            }
        }
     
        @Override
        public int getItemViewType(int position) {
            return position % 2 == 0 ? ITEM_TYPE.ITEM_TYPE_IMAGE.ordinal() : ITEM_TYPE.ITEM_TYPE_TEXT.ordinal();
        }
     
        @Override
        public int getItemCount() {
            return mTitles == null ? 0 : mTitles.length;
        }
        
        public void addAll(ArrayList<String> list){
            if(mTitles!=null){
                mTitles.clear();
            }else {
                mTitles = new ArrayList<>();
            }
            mTitles.addAll(list);
        }
     
        public static class TextViewHolder extends RecyclerView.ViewHolder {
            @InjectView(R.id.text_view)
            TextView mTextView;
            TextViewHolder(View view) {
                super(view);
                ButterKnife.inject(this, view);
            }
        }
     
        public static class ImageViewHolder extends RecyclerView.ViewHolder {
            @InjectView(R.id.text_view)
            TextView mTextView;
            @InjectView(R.id.image_view)
            ImageView mImageView;
            ImageViewHolder(View view) {
                super(view);
                ButterKnife.inject(this, view);
            }
        }
    }
    

2.Adapter

2.1 RecyclerView.Adapter扮演的角色

  • 一是,根據不同ViewType建立與之相應的的Item-Layout
  • 二是,訪問資料集合並將資料繫結到正確的View上

2.2 重寫的方法

  • 一般常用的重寫方法有以下這麼幾個:
    public VH onCreateViewHolder(ViewGroup parent, int viewType)
    建立Item檢視,並返回相應的ViewHolder
    public void onBindViewHolder(VH holder, int position)
    繫結資料到正確的Item檢視上。
    public int getItemCount()
    返回該Adapter所持有的Itme數量
    public int getItemViewType(int position)
    用來獲取當前項Item(position引數)是哪種型別的佈局
    

2.3 notifyDataSetChanged()重新整理資料

  • 當時據集合發生改變時,我們通過呼叫.notifyDataSetChanged(),來重新整理列表,因為這樣做會觸發列表的重繪,所以並不會出現任何動畫效果,因此需要呼叫一些以notifyItem*()作為字首的特殊方法,比如:
    • public final void notifyItemInserted(int position) 向指定位置插入Item
    • public final void notifyItemRemoved(int position) 移除指定位置Item
    • public final void notifyItemChanged(int position) 更新指定位置Item

2.4 資料變更通知之觀察者模式

  • a.首先看.notifyDataSetChanged()原始碼
    /** @see #notifyItemChanged(int)
     * @see #notifyItemInserted(int)
     * @see #notifyItemRemoved(int)
     * @see #notifyItemRangeChanged(int, int)
     * @see #notifyItemRangeInserted(int, int)
     * @see #notifyItemRangeRemoved(int, int)
     */
    public final void notifyDataSetChanged() {
        mObservable.notifyChanged();
    }
    
  • b.接著檢視.notifyChanged();原始碼
    • 被觀察者AdapterDataObservable,內部持有觀察者AdapterDataObserver集合
    static class AdapterDataObservable extends Observable<AdapterDataObserver> {
        public boolean hasObservers() {
            return !mObservers.isEmpty();
        }
    
        public void notifyChanged() {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    
        public void notifyItemRangeChanged(int positionStart, int itemCount) {
            notifyItemRangeChanged(positionStart, itemCount, null);
        }
    
        public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
            }
        }
    
        public void notifyItemRangeInserted(int positionStart, int itemCount) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
            }
        }
    }
    
    • 觀察者AdapterDataObserver,具體實現為RecyclerViewDataObserver,當資料來源發生變更時,及時響應介面變化
    public static abstract class AdapterDataObserver {
        public void onChanged() {
            // Do nothing
        }
    
        public void onItemRangeChanged(int positionStart, int itemCount) {
            // do nothing
        }
    
        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
            onItemRangeChanged(positionStart, itemCount);
        }
    }
    
  • c.接著檢視setAdapter()原始碼中的setAdapterInternal(adapter, false, true)方法
    • setAdapter原始碼
    public void setAdapter(Adapter adapter) {
        // bail out if layout is frozen
        setLayoutFrozen(false);
        setAdapterInternal(adapter, false, true);
        requestLayout();
    }
    
    • setAdapterInternal(adapter, false, true)原始碼
    private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
            boolean removeAndRecycleViews) {
        if (mAdapter != null) {
            mAdapter.unregisterAdapterDataObserver(mObserver);
            mAdapter.onDetachedFromRecyclerView(this);
        }
        if (!compatibleWithPrevious || removeAndRecycleViews) {
            removeAndRecycleViews();
        }
        mAdapterHelper.reset();
        final Adapter oldAdapter = mAdapter;
        mAdapter = adapter;
        if (adapter != null) {
            //註冊一個觀察者RecyclerViewDataObserver
            adapter.registerAdapterDataObserver(mObserver);
            adapter.onAttachedToRecyclerView(this);
        }
        if (mLayout != null) {
            mLayout.onAdapterChanged(oldAdapter, mAdapter);
        }
        mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
        mState.mStructureChanged = true;
        markKnownViewsInvalid();
    }
    
  • d.notify……方法被呼叫,重新整理資料
    • 當資料變更時,呼叫notify**方法時,Adapter內部的被觀察者會遍歷通知已經註冊的觀察者的對應方法,這時介面就會響應變更。

3.ViewHolder

3.1 ViewHolder的作用

  • ViewHolder作用大概有這些:
    • adapter應當擁有ViewHolder的子類,並且ViewHolder內部應當儲存一些子view,避免時間代價很大的findViewById操作
    • 其RecyclerView內部定義的ViewHolder類包含很多複雜的屬性,內部使用場景也有很多,而我們經常使用的也就是onCreateViewHolder()方法和onBindViewHolder()方法,onCreateViewHolder()方法在RecyclerView需要一個新型別。item的ViewHolder時呼叫來建立一個ViewHolder,而onBindViewHolder()方法則當RecyclerView需要在特定位置的item展示資料時呼叫。

3.2 ViewHolder與複用

  • 在複寫RecyclerView.Adapter的時候,需要我們複寫兩個方法:
    • onCreateViewHolder
    • onBindViewHolder
    • 這兩個方法從字面上看就是建立ViewHolder和繫結ViewHolder的意思
  • 複用機制是怎樣的?
    • 模擬場景:只有一種ViewType,上下滑動的時候需要的ViewHolder種類是隻有一種,但是需要的ViewHolder物件數量並不止一個。所以在後面建立了5個ViewHolder之後,需要的數量夠了,無論怎麼滑動,都只需要複用以前建立的物件就行了。那麼逗比程式設計師們思考一下,為什麼會出現這種情況呢
    • 看到了下面log之後,第一反應是在這個ViewHolder物件的數量“夠用”之後就停止呼叫onCreateViewHolder方法,但是onBindViewHolder方法每次都會呼叫的
    • image
  • 檢視一下createViewHolder原始碼
    • 發現這裡並沒有限制
    public final VH createViewHolder(ViewGroup parent, int viewType) {
        TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
        final VH holder = onCreateViewHolder(parent, viewType);
        holder.mItemViewType = viewType;
        TraceCompat.endSection();
        return holder;
    }
    
  • 對於ViewHolder物件的數量“夠用”之後就停止呼叫onCreateViewHolder方法,可以檢視
    • 獲取為給定位置初始化的檢視。
    • 此方法應由{@link LayoutManager}實現使用,以獲取檢視來表示來自{@LinkAdapter}的資料。
    • 如果共享池可用於正確的檢視型別,則回收程式可以重用共享池中的廢檢視或分離檢視。如果介面卡沒有指示給定位置上的資料已更改,則回收程式將嘗試發回一個以前為該資料初始化的報廢檢視,而不進行重新繫結。
    public View getViewForPosition(int position) {
        return getViewForPosition(position, false);
    }
    
    View getViewForPosition(int position, boolean dryRun) {
        return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }
    
    @Nullable
    ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
        //程式碼省略了,有需要的小夥伴可以自己看看,這裡面邏輯實在太複雜呢
    }
    

3.3 ViewHolder簡單封裝

  • 關於ViewHolder簡單的封裝程式碼如下所示:
    public abstract class BaseMViewHolder<M> extends RecyclerView.ViewHolder {
    
    
        // SparseArray 比 HashMap 更省記憶體,在某些條件下效能更好,只能儲存 key 為 int 型別的資料,
        // 用來存放 View 以減少 findViewById 的次數
    
        private SparseArray<View> viewSparseArray;
    
        BaseMViewHolder(View itemView) {
            super(itemView);
            if(viewSparseArray==null){
                viewSparseArray = new SparseArray<>();
            }
        }
    
        public BaseMViewHolder(ViewGroup parent, @LayoutRes int res) {
            super(LayoutInflater.from(parent.getContext()).inflate(res, parent, false));
            if(viewSparseArray==null){
                viewSparseArray = new SparseArray<>();
            }
        }
    
        /**
         * 子類設定資料方法
         * @param data
         */
        public void setData(M data) {}
    
        /**
         * 第二種findViewById方式
         * 根據 ID 來獲取 View
         * @param viewId viewID
         * @param <T>    泛型
         * @return 將結果強轉為 View 或 View 的子型別
         */
        @SuppressWarnings("unchecked")
        protected <T extends View> T getView(int viewId) {
            // 先從快取中找,找打的話則直接返回
            // 如果找不到則 findViewById ,再把結果存入快取中
            View view = viewSparseArray.get(viewId);
            if (view == null) {
                view = itemView.findViewById(viewId);
                viewSparseArray.put(viewId, view);
            }
            return (T) view;
        }
    
    
        /**
         * 獲取上下文context
         * @return          context
         */
        protected Context getContext(){
            return itemView.getContext();
        }
    
    
        /**
         * 獲取資料索引的位置
         * @return          position
         */
        protected int getDataPosition(){
            RecyclerView.Adapter adapter = getOwnerAdapter();
            if (adapter!=null && adapter instanceof RecyclerArrayAdapter){
                return getAdapterPosition() - ((RecyclerArrayAdapter) adapter).getHeaderCount();
            }
            return getAdapterPosition();
        }
    
    
        /**
         * 獲取adapter物件
         * @param <T>
         * @return                  adapter
         */
        @Nullable
        private  <T extends RecyclerView.Adapter> T getOwnerAdapter(){
            RecyclerView recyclerView = getOwnerRecyclerView();
            //noinspection unchecked
            return recyclerView != null ? (T) recyclerView.getAdapter() : null;
        }
    
    
        @Nullable
        private RecyclerView getOwnerRecyclerView(){
            try {
                Field field = RecyclerView.ViewHolder.class.getDeclaredField("mOwnerRecyclerView");
                field.setAccessible(true);
                return (RecyclerView) field.get(this);
            } catch (NoSuchFieldException ignored) {
                ignored.printStackTrace();
            } catch (IllegalAccessException ignored) {
                ignored.printStackTrace();
            }
            return null;
        }
    
    
        /**
         * 新增子控制元件的點選事件
         * @param viewId                        控制元件id
         */
        protected void addOnClickListener(@IdRes final int viewId) {
            final View view = getView(viewId);
            if (view != null) {
                if (!view.isClickable()) {
                    view.setClickable(true);
                }
                view.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if(getOwnerAdapter()!=null){
                            if (((RecyclerArrayAdapter)getOwnerAdapter()).getOnItemChildClickListener() != null) {
                                ((RecyclerArrayAdapter)getOwnerAdapter()).getOnItemChildClickListener()
                                        .onItemChildClick(v, getDataPosition());
                            }
                        }
                    }
                });
            }
        }
    
        //省略部分程式碼
        //關於adapter封裝可以檢視我的開源adpater封裝庫:https://github.com/yangchong211/YCBaseAdapter
        //關於recyclerView封裝庫,可以檢視我的開源庫:https://github.com/yangchong211/YCRefreshView
    }
    

4.LayoutManager

4.1 作用

  • LayoutManager的職責是擺放Item的位置,並且負責決定何時回收和重用Item。
  • RecyclerView 允許自定義規則去放置子 view,這個規則的控制者就是 LayoutManager。一個 RecyclerView 如果想展示內容,就必須設定一個 LayoutManager

4.2 LayoutManager樣式

  • LinearLayoutManager 水平或者垂直的Item檢視。
  • GridLayoutManager 網格Item檢視。
  • StaggeredGridLayoutManager 交錯的網格Item檢視。

4.3 LayoutManager當前有且僅有一個抽象函式

  • 具體如下:
    public LayoutParams generateDefaultLayoutParams()
    

4.4 setLayoutManager(LayoutManager layout)原始碼

  • a.setLayoutManager入口原始碼
    • 分析:當之前設定過 LayoutManager 時,移除之前的檢視,並快取檢視在 Recycler 中,將新的 mLayout 物件與 RecyclerView 繫結,更新快取 View 的數量。最後去呼叫 requestLayout ,重新請求 measure、layout、draw。
    public void setLayoutManager(LayoutManager layout) {
        if (layout == mLayout) {
            return;
        }
        // 停止滑動
        stopScroll();
        if (mLayout != null) {
            // 如果有動畫,則停止所有的動畫
            if (mItemAnimator != null) {
                mItemAnimator.endAnimations();
            }
            // 移除並回收檢視
            mLayout.removeAndRecycleAllViews(mRecycler);
            // 回收廢棄檢視
            mLayout.removeAndRecycleScrapInt(mRecycler);
            //清除mRecycler
            mRecycler.clear();
            if (mIsAttached) {
                mLayout.dispatchDetachedFromWindow(this, mRecycler);
            }
            mLayout.setRecyclerView(null);
            mLayout = null;
        } else {
            mRecycler.clear();
        }
        mChildHelper.removeAllViewsUnfiltered();
        mLayout = layout;
        if (layout != null) {
            if (layout.mRecyclerView != null) {
                throw new IllegalArgumentException("LayoutManager " + layout +
                        " is already attached to a RecyclerView: " + layout.mRecyclerView);
            }
            mLayout.setRecyclerView(this);
            if (mIsAttached) {
                mLayout.dispatchAttachedToWindow(this);
            }
        }
        //更新新的快取資料
        mRecycler.updateViewCacheSize();
        //重新請求 View 的測量、佈局、繪製
        requestLayout();
    }
    

5.ItemDecoration

5.1 作用

  • 通過設定recyclerView.addItemDecoration(new DividerDecoration(this));來改變Item之間的偏移量或者對Item進行裝飾。
  • 當然,你也可以對RecyclerView設定多個ItemDecoration,列表展示的時候會遍歷所有的ItemDecoration並呼叫裡面的繪製方法,對Item進行裝飾。

5.2 RecyclerView.ItemDecoration是一個抽象類

  • 該抽象類常見的方法如下所示:
    public void onDraw(Canvas c, RecyclerView parent)
    裝飾的繪製在Item條目繪製之前呼叫,所以這有可能被Item的內容所遮擋
    public void onDrawOver(Canvas c, RecyclerView parent)
    裝飾的繪製在Item條目繪製之後呼叫,因此裝飾將浮於Item之上
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)
    與padding或margin類似,LayoutManager在測量階段會呼叫該方法,計算出每一個Item的正確尺寸並設定偏移量。
    

5.3 .addItemDecoration()原始碼分析

  • a.通過下面程式碼可知,mItemDecorations是一個ArrayList,我們將ItemDecoration也就是分割線物件,新增到其中。
    • 可以看到,當通過這個方法新增分割線後,會指定新增分割線在集合中的索引,然後再重新請求 View 的測量、佈局、(繪製)。注意: requestLayout會呼叫onMeasure和onLayout,不一定呼叫onDraw!
    public void addItemDecoration(ItemDecoration decor) {
        addItemDecoration(decor, -1);
    }
    
    //主要看這個方法,我的GitHub:https://github.com/yangchong211/YCBlogs
    public void addItemDecoration(ItemDecoration decor, int index) {
        if (mLayout != null) {
            mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"
                    + " layout");
        }
        if (mItemDecorations.isEmpty()) {
            setWillNotDraw(false);
        }
        if (index < 0) {
            mItemDecorations.add(decor);
        } else {
            // 指定新增分割線在集合中的索引
            mItemDecorations.add(index, decor);
        }
        markItemDecorInsetsDirty();
        // 重新請求 View 的測量、佈局、繪製
        requestLayout();
    }
    
  • b.接著看下markItemDecorInsetsDirty這個方法做了些什麼
    • 這個方法先獲取所有子View的數量,然後遍歷了 RecyclerView 和 LayoutManager 的所有子 View,再將其子 View 的 LayoutParams 中的 mInsetsDirty 屬性置為 true,最後呼叫了 mRecycler.markItemDecorInsetsDirty()方法處理複用邏輯。
    void markItemDecorInsetsDirty() {
        final int childCount = mChildHelper.getUnfilteredChildCount();
        //先遍歷了 RecyclerView 和 LayoutManager 的所有子 View
        for (int i = 0; i < childCount; i++) {
            final View child = mChildHelper.getUnfilteredChildAt(i);
            //將其子 View 的 LayoutParams 中的 mInsetsDirty 屬性置為 true
            ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
        }
        //呼叫了 mRecycler.markItemDecorInsetsDirty(),
        //Recycler 是 RecyclerView 的一個內部類,就是它管理著 RecyclerView 的複用邏輯
        mRecycler.markItemDecorInsetsDirty();
    }
    
  • c.接著看下markItemDecorInsetsDirty()這個方法
    • 該方法就是獲取RecyclerView 快取的集合,然後遍歷集合得到RecyclerView 的快取單位是 ViewHolder,獲取快取物件,在獲取到layoutParams,並且將其 mInsetsDirty 欄位一樣置為 true
    void markItemDecorInsetsDirty() {
        //就是 RecyclerView 快取的集合
        final int cachedCount = mCachedViews.size();
        for (int i = 0; i < cachedCount; i++) {
            //RecyclerView 的快取單位是 ViewHolder,獲取快取物件
            final ViewHolder holder = mCachedViews.get(i);
            //獲得 LayoutParams
            LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams();
            if (layoutParams != null) {
                //將其 mInsetsDirty 欄位一樣置為 true
                layoutParams.mInsetsDirty = true;
            }
        }
    }
    
  • d.回過頭在看看addItemDecoration中requestLayout方法
    • requestLayout 方法用一種責任鏈的方式,層層向上傳遞,最後傳遞到 ViewRootImpl,然後重新呼叫 view 的 measure、layout、draw 方法來展示佈局
    @CallSuper
    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();
    
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }
    
  • e.在 RecyclerView 中搜索 mItemDecorations 集合
    • 在onDraw中
    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }
    
    • 在draw方法中
    @Override
    public void draw(Canvas c) {
        super.draw(c);
    
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
         //省略部分程式碼
    }
    
    • 總結概括
      • 可以看到在 View 的以上兩個方法中,分別呼叫了 ItemDecoration 物件的 onDraw onDrawOver 方法。
      • 這兩個抽象方法,由我們繼承 ItemDecoration 來自己實現,他們區別就是 onDraw 在 item view 繪製之前呼叫,onDrawOver 在 item view 繪製之後呼叫。
      • 所以繪製順序就是 Decoration 的 onDraw,ItemView的 onDraw,Decoration 的 onDrawOver。

6.ItemAnimator

6.1 作用

  • ItemAnimator能夠幫助Item實現獨立的動畫

6.2 觸發的三種事件

  • 某條資料被插入到資料集合中
  • 從資料集合中移除某條資料
  • 更改資料集合中的某條資料

7.其他知識點

7.1 Recycler && RecycledViewPool

  • RecycledViewPool
    • RecyclerViewPool用於多個RecyclerView之間共享View。只需要建立一個RecyclerViewPool例項,然後呼叫RecyclerView的setRecycledViewPool(RecycledViewPool)方法即可。RecyclerView預設會建立一個RecyclerViewPool例項。
    • 下列原始碼,是我藉助於有道詞典翻譯部分註釋內容……
    • 看出mScrap是一個<viewType, List>的對映,mMaxScrap是一個<viewType, maxNum>的對映,這兩個成員變數代表可複用View池的基本資訊。呼叫setMaxRecycledViews(int viewType, int max)時,當用於複用的mScrap中viewType對應的ViewHolder個數超過maxNum時,會從列表末尾開始丟棄超過的部分。呼叫getRecycledView(int viewType)方法時從mScrap中移除並返回viewType對應的List的末尾項
    public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;
        static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();
        private int mAttachCount = 0;
    
        //丟棄所有檢視
        public void clear() {
            for (int i = 0; i < mScrap.size(); i++) {
                ScrapData data = mScrap.valueAt(i);
                data.mScrapHeap.clear();
            }
        }
    
        //設定丟棄前要在池中持有的檢視持有人的最大數量
        public void setMaxRecycledViews(int viewType, int max) {
            ScrapData scrapData = getScrapDataForType(viewType);
            scrapData.mMaxScrap = max;
            final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
            while (scrapHeap.size() > max) {
                scrapHeap.remove(scrapHeap.size() - 1);
            }
        }
    
        //返回給定檢視型別的RecycledViewPool所持有的當前檢視數
        public int getRecycledViewCount(int viewType) {
            return getScrapDataForType(viewType).mScrapHeap.size();
        }
    
        //從池中獲取指定型別的ViewHolder,如果沒有指定型別的ViewHolder,則獲取{@Codenull}
        @Nullable
        public ViewHolder getRecycledView(int viewType) {
            final ScrapData scrapData = mScrap.get(viewType);
            if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
                final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
                return scrapHeap.remove(scrapHeap.size() - 1);
            }
            return null;
        }
    
        //池持有的檢視持有者總數
        int size() {
            int count = 0;
            for (int i = 0; i < mScrap.size(); i++) {
                ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i).mScrapHeap;
                if (viewHolders != null) {
                    count += viewHolders.size();
                }
            }
            return count;
        }
    
        //向池中新增一個廢檢視儲存器。
        //如果那個ViewHolder型別的池已經滿了,它將立即被丟棄。
        public void putRecycledView(ViewHolder scrap) {
            final int viewType = scrap.getItemViewType();
            final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
            if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
                return;
            }
            if (DEBUG && scrapHeap.contains(scrap)) {
                throw new IllegalArgumentException("this scrap item already exists");
            }
            scrap.resetInternal();
            scrapHeap.add(scrap);
        }
    
        long runningAverage(long oldAverage, long newValue) {
            if (oldAverage == 0) {
                return newValue;
            }
            return (oldAverage / 4 * 3) + (newValue / 4);
        }
    
        void factorInCreateTime(int viewType, long createTimeNs) {
            ScrapData scrapData = getScrapDataForType(viewType);
            scrapData.mCreateRunningAverageNs = runningAverage(
                    scrapData.mCreateRunningAverageNs, createTimeNs);
        }
    
        void factorInBindTime(int viewType, long bindTimeNs) {
            ScrapData scrapData = getScrapDataForType(viewType);
            scrapData.mBindRunningAverageNs = runningAverage(
                    scrapData.mBindRunningAverageNs, bindTimeNs);
        }
    
        boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) {
            long expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs;
            return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
        }
    
        boolean willBindInTime(int viewType, long approxCurrentNs, long deadlineNs) {
            long expectedDurationNs = getScrapDataForType(viewType).mBindRunningAverageNs;
            return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
        }
    
        void attach(Adapter adapter) {
            mAttachCount++;
        }
    
        void detach() {
            mAttachCount--;
        }
    
    
        //分離舊介面卡並附加新介面卡。如果它只附加了一個介面卡,並且新介面卡使用與oldAdapter不同的ViewHolder,
        //則RecycledViewPool將清除其快取。
        void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,boolean compatibleWithPrevious) {
            if (oldAdapter != null) {
                detach();
            }
            if (!compatibleWithPrevious && mAttachCount == 0) {
                clear();
            }
            if (newAdapter != null) {
                attach(newAdapter);
            }
        }
    
        private ScrapData getScrapDataForType(int viewType) {
            ScrapData scrapData = mScrap.get(viewType);
            if (scrapData == null) {
                scrapData = new ScrapData();
                mScrap.put(viewType, scrapData);
            }
            return scrapData;
        }
    }
    
  • ViewCacheExtension
    • ViewCacheExtension是一個由開發者控制的可以作為View快取的幫助類。呼叫Recycler.getViewForPosition(int)方法獲取View時,Recycler先檢查attachedscrap和一級快取,如果沒有則檢查ViewCacheExtension.getViewForPositionAndType(Recycler, int, int),如果沒有則檢查RecyclerViewPool。注意:Recycler不會在這個類中做快取View的操作,是否快取View完全由開發者控制。
    public abstract static class ViewCacheExtension {
        abstract public View getViewForPositionAndType(Recycler recycler, int position, int type);
    }
    
  • Recycler
    • 後續再深入分析

7.2 Recyclerview.getLayoutPosition()問題

  • 在RecycleView中的相關方法中,有兩種型別的位置
    • 佈局位置:從LayoutManager的角度看,條目在最新佈局計算中的位置。
      • 返回佈局位置的方法使用最近一次佈局運算後的位置,如getLayoutPosition()和findViewHolderForLayoutPosition(int)。這些位置包含了最近一次佈局運算後的變化。你可以根據這些位置來與使用者正在螢幕上看到的保持一致。比如,你有一個條目列表,當用戶請求第5個條目時,你可以使用這些方法來匹配使用者看到的。
    • 介面卡位置:從介面卡的角度看,條目在是介面卡中的位置。
      • 另外一系列方法與AdapterPosition關聯,比如getAdapterPosition()和findViewHolderForAdapterPosition(int)。當你想獲得條目在更新後的介面卡中的位置使用這些方法,即使這些位置變化還沒反映到佈局中。比如,你想訪問介面卡中條目的位置時,就應該使用getAdapterPosition()。注意,notifyDataSetChanged()已經被呼叫而且還沒計算新佈局,這些方法或許不能夠計算介面卡位置。所以,你要小心處理這些方法返回NO_POSITION和null的情況。
    • 注意: 這兩種型別的位置是等同的,除非在分發adapter.notify*事件和更新佈局時。
  • 關於兩者的區別
    • 網上查了一些資料,發現相關內容很少,最後在stackoverflow上終於看到有大神這樣解釋兩者的區別
    • 具體區別就是adapter和layout的位置會有時間差(<16ms), 如果你改變了Adapter的資料然後重新整理檢視, layout需要過一段時間才會更新檢視, 在這段時間裡面, 這兩個方法返回的position會不一樣。
      • 在notifyDataSetChanged之後並不能馬上獲取Adapter中的position, 要等佈局結束之後才能獲取到
      • 在notifyItemInserted之後,Layout不能馬上獲取到新的position,因為佈局還沒更新(需要<16ms的時間重新整理檢視), 所以只能獲取到舊的,但是Adapter中的position就可以馬上獲取到最新的position。
    public final int getAdapterPosition() {
        if (mOwnerRecyclerView == null) {
            return NO_POSITION;
        }
        return mOwnerRecyclerView.getAdapterPositionFor(this);
    }
    
    public final int getLayoutPosition() {
        return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
    }
    
  • 可能會導致的錯誤
    • 這種情況有點難以復現,在 ViewHolder 中處理 item 的點選事件的時候,發現多個 item 同時點選就會出現閃退,debug 看到 position = -1
    • 解決辦法:使用 ViewHolder#getLayoutPosition() 獲取 position,而不要通過 ViewHolder#getAdapterPosition() 來獲取 position 的

8.RecyclerView巢狀方案滑動衝突解決方案

8.1 如何判斷RecyclerView控制元件滑動到頂部和底部

  • 有一種使用場景,購物商城的購物車頁面,當RecyclerView滑動到頂部時,讓重新整理控制元件消費事件;當RecyclerView滑動到底部時,讓下一頁控制元件[猜你喜歡]消費事件。
    • 程式碼如下所示:
    public class VerticalRecyclerView extends RecyclerView {
    
        private float downX;
        private float downY;
        /** 第一個可見的item的位置 */
        private int firstVisibleItemPosition;
        /** 第一個的位置 */
        private int[] firstPositions;
        /** 最後一個可見的item的位置 */
        private int lastVisibleItemPosition;
        /** 最後一個的位置 */
        private int[] lastPositions;
        private boolean isTop;
        private boolean isBottom;
    
        public VerticalRecyclerView(Context context) {
            this(context, null);
        }
    
        public VerticalRecyclerView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public VerticalRecyclerView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            LayoutManager layoutManager = getLayoutManager();
            if (layoutManager != null) {
                if (layoutManager instanceof GridLayoutManager) {
                    lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
                    firstVisibleItemPosition = ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition();
                } else if (layoutManager instanceof LinearLayoutManager) {
                    lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
                    firstVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
                } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                    StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
                    if (lastPositions == null) {
                        lastPositions = new int[staggeredGridLayoutManager.getSpanCount()];
                        firstPositions = new int[staggeredGridLayoutManager.getSpanCount()];
                    }
                    staggeredGridLayoutManager.findLastVisibleItemPositions(lastPositions);
                    staggeredGridLayoutManager.findFirstVisibleItemPositions(firstPositions);
                    lastVisibleItemPosition = findMax(lastPositions);
                    firstVisibleItemPosition = findMin(firstPositions);
                }
            } else {
                throw new RuntimeException("Unsupported LayoutManager used. Valid ones are LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager");
            }
    
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    downX = ev.getX();
                    downY = ev.getY();
                    //如果滑動到了最底部,就允許繼續向上滑動載入下一頁,否者不允許
                    getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_MOVE:
                    float dx = ev.getX() - downX;
                    float dy = ev.getY() - downY;
                    boolean allowParentTouchEvent;
                    if (Math.abs(dy) > Math.abs(dx)) {
                        if (dy > 0) {
                            //位於頂部時下拉,讓父View消費事件
                            allowParentTouchEvent = isTop = firstVisibleItemPosition == 0 && getChildAt(0).getTop() >= 0;
                        } else {
                            //位於底部時上拉,讓父View消費事件
                            int visibleItemCount = layoutManager.getChildCount();
                            int totalItemCount = layoutManager.getItemCount();
                            allowParentTouchEvent = isBottom = visibleItemCount > 0 && (lastVisibleItemPosition) >= totalItemCount - 1 && getChildAt(getChildCount() - 1).getBottom() <= getHeight();
                        }
                    } else {
                        //水平方向滑動
                        allowParentTouchEvent = true;
                    }
                    getParent().requestDisallowInterceptTouchEvent(!allowParentTouchEvent);
            }
            return super.dispatchTouchEvent(ev);
    
        }
    
        private int findMax(int[] lastPositions) {
            int max = lastPositions[0];
            for (int value : lastPositions) {
                if (value >= max) {
                    max = value;
                }
            }
            return max;
        }
    
        private int findMin(int[] firstPositions) {
            int min = firstPositions[0];
            for (int value : firstPositions) {
                if (value < min) {
                    min = value;
                }
            }
            return min;
        }
    
        public boolean isTop() {
            return isTop;
        }
    
        public boolean isBottom() {
            return isBottom;
        }
    }
    

8.2 RecyclerView巢狀RecyclerView條目自動上滾的Bug

  • RecyclerViewA巢狀RecyclerViewB 進入頁面自動跳轉到RecyclerViewB上面頁面會自動滾動。
    • 兩種解決辦法
    • 一,recyclerview去除焦點
      • recyclerview.setFocusableInTouchMode(false);
      • recyclerview.requestFocus();
    • 二,在程式碼裡面 讓處於ScrollView或者RecyclerView1 頂端的某個控制元件獲得焦點即可
      • 比如頂部的一個textview
      • tv.setFocusableInTouchMode(true);
      • tv.requestFocus();

8.3 ScrollView巢狀RecyclerView滑動衝突

  • 第一種方式:
    • 重寫父控制元件,讓父控制元件 ScrollView 直接攔截滑動事件,不向下分發給 RecyclerView,具體是定義一個ScrollView子類,重寫其 onInterceptTouchEvent()方法
    public class NoNestedScrollview extends NestedScrollView {
    
        private int downX;
        private int downY;
        private int mTouchSlop;
        
        public NoNestedScrollview(Context context) {
            super(context);
            mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        }
        
        public NoNestedScrollview(Context context, AttributeSet attrs) {
            super(context, attrs);
            mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        }
        
        public NoNestedScrollview(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        }
        
        @Override
        public boolean onInterceptTouchEvent(MotionEvent e) {
            int action = e.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    downX = (int) e.getRawX();
                    downY = (int) e.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    //判斷是否滑動,若滑動就攔截事件
                    int moveY = (int) e.getRawY();
                    if (Math.abs(moveY - downY) > mTouchSlop) {
                        return true;
                    }
                    break;
                default:
                    break;
            }
            return super.onInterceptTouchEvent(e);
        }
    }
    
  • 第二種解決方式
    • a.禁止RecyclerView滑動
    recyclerView.setLayoutManager(new GridLayoutManager(mContext,2){
        @Override
        public boolean canScrollVertically() {
            return false;
        }
        
        @Override
        public boolean canScrollHorizontally() {
            return super.canScrollHorizontally();
        }
    });
    
    recyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayout.VERTICAL,false){
        @Override
        public boolean canScrollVertically() {
            return false;
        }
    });
    
    • b.重寫LayoutManager
      • 程式碼設定LayoutManager.setScrollEnabled(false);
    public class ScrollLayoutManager extends LinearLayoutManager {
         
        private boolean isScrollEnable = true;
         
        public ScrollLayoutManager(Context context) {
            super(context);
        }
         
        public ScrollLayoutManager(Context context, int orientation, boolean reverseLayout) {
            super(context, orientation, reverseLayout);
        }
         
        public ScrollLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
        }
         
        @Override
        public boolean canScrollVertically() {
            return isScrollEnable && super.canScrollVertically();
        }
         
        /**
         * 設定 RecyclerView 是否可以垂直滑動
         * @param isEnable
         */
        public void setScrollEnable(boolean isEnable) {
            this.isScrollEnable = isEnable;
        }
    }
    
  • 可能會出現的問題
    • 雖然上面兩種方式解決了滑動衝突,但是有的手機上出現了RecyclerView會出現顯示不全的情況。
    • 針對這種情形,使用網上的方法一種是使用 RelativeLayout 包裹 RecyclerView 並設定屬性:android:descendantFocusability=“blocksDescendants”
      • android:descendantFocusability=“blocksDescendants”,該屬>性是當一個view 獲取焦點時,定義 ViewGroup 和其子控制元件直接的關係,常用來>解決父控制元件的焦點或者點選事件被子空間獲取。
      • beforeDescendants: ViewGroup會優先其子控制元件獲取焦點
      • afterDescendants: ViewGroup只有當其子控制元件不需要獲取焦點時才獲取焦點
      • blocksDescendants: ViewGroup會覆蓋子類控制元件而直接獲得焦點
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:descendantFocusability="blocksDescendants">
        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv_hot_review"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:foregroundGravity="center" />
    </RelativeLayout>
    

8.4 viewPager巢狀水平RecyclerView橫向滑動到底後不滑動ViewPager

  • 繼承RecyclerView,重寫dispatchTouchEvent,根據ACTION_MOVE的方向判斷是否呼叫getParent().requestDisallowInterceptTouchEvent去阻止父view攔截點選事件
    @Override 
    public boolean dispatchTouchEvent(MotionEvent ev) { 
        /*---解決垂ViewPager巢狀直RecyclerView巢狀水平RecyclerView橫向滑動到底後不滑動ViewPager start ---*/ 
        ViewParent parent = this; 
        while(!((parent = parent.getParent()) instanceof ViewPager));
        // 迴圈查詢viewPager 
        parent.requestDisallowInterceptTouchEvent(true); 
        return super.dispatchTouchEvent(ev); 
    }
    

9.RecyclerView複雜佈局封裝庫案例

9.1 能夠實現業務的需求和功能

  • 1.1 支援上拉載入,下拉重新整理,可以自定義foot底部佈局,支援新增多個自定義header頭部佈局。
  • 1.2 支援切換不同的狀態,比如載入中[目前是ProgressBar,載入成功,載入失敗,載入錯誤等不同佈局狀態。當然也可以自定義這些狀態的佈局
  • 1.3 支援複雜介面使用,比如有的頁面包含有輪播圖,按鈕組合,橫向滑動,還有複雜list,那麼用這個控制元件就可以搞定。
  • 1.4 已經用於實際開發專案投資界,新芽,沙丘大學中……
  • 1.5 輕量級側滑刪除選單,直接巢狀item佈局即可使用,使用十分簡單。
  • 1.6 支援插入或者刪除某條資料,支援CoordinatorLayout炫酷的效果
  • 1.7 支援貼上頭部的需求效果
  • 1.8 RecyclerView實現條目Item拖拽排序與滑動刪除

9.2 具備的優勢分析

  • 自定義支援上拉載入更多,下拉重新整理,支援自由切換狀態【載入中,載入成功,載入失敗,沒網路等狀態】的控制元件,拓展功能[支援長按拖拽,側滑刪除]可以選擇性新增 。具體使用方法,可以直接參考demo。
  • 輕量級側滑刪除選單,支援recyclerView,listView,直接巢狀item佈局即可使用,整個側滑選單思路是:跟隨手勢將item向左滑動

10.針對阿里VLayout程式碼分析

11.版本更新說明

  • v1.0.0 2016年5月5日
  • v1.1.0 更新於2017年2月1日
  • v1.1.1 更新於2017年6月9日
  • v2.0.0 更新於2018年9月26日

關於其他內容介紹

01.關於部落格彙總連結

02.關於我的部落格