理解RecyclerView(三)—簡單封裝和LayoutManager源碼解析

人不需要活太多樣子,你認真做一件事,就會解釋所有的事。        ——《不必交談時刻》

一、概述

  上一篇文章RecyclerView(二)中實現瞭如何添加分割線、增刪的動畫、添加頭尾佈局、拖拽和側滑刪除效果,但是在使用的Adapter是沒有封裝過的,每次都要重寫相關方法,下面我們會對Adapter進行簡單的封裝。(源碼在文章最後給出)

二、自定義點擊事件

  RecyclerView並沒有像ListView那樣暴露出item的點擊事件和長按事件處理得API,也就是說在使用RecyclerView的時候需要我們自己來實現item的點擊事件和長按事件的處理。我們通常都是在綁定ViewHolder的時候設置監聽,然後通過Adapter回調出去。

2.1 自定義點擊事件接口

我們需要自定義點擊事件監聽回調的接口,通過接口將點擊事件傳遞出去:

 	//1.定義變量接收接口
    private OnItemClickListener mOnItemClickListener;
    //2.定義接口:點擊事件
    public interface OnItemClickListener {
        void onItemClick(View view, int position);//單擊
        void onItemLongClick(View view, int position);//長按
    }
    //3.設置接口接收的方法
    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        mOnItemClickListener = onItemClickListener;
    }

首先,定義接口Interface OnItemClickListener,並定義接口需要實現的方法;接着定義接收接口的變量OnItemClickListener,當接口實例存在時,能直接實用該接口; 最後定義接收接口的方法setOnItemClickListener(),給定義的接口賦值。

2.2 onBindViewHolder設置點擊事件監聽

onBindViewHolder()中方法中對item設置單擊和長按點擊事件,這裏需要判斷一下mOnItemClickListener是否爲null。

  	@Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, final int position) {
        ViewHolder viewHolder = (ViewHolder) holder;
        //點擊事件
        viewHolder.mCl_root.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mOnItemClickListener != null) mOnItemClickListener.onItemClick(v, position);
            }
        });
        //長按事件
        viewHolder.mCl_root.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if (mOnItemClickListener != null) mOnItemClickListener.onItemLongClick(v, position);
                //true表示此事件已消費,不會觸發單擊事件,false表示兩個回調都會觸發
                return false;
            }
        });
    }

2.3 Adapter設置點擊事件回調

在Activity中,adapter實例設置事件點擊回調,回調點擊事件和長按事件,在回調方法中實現相關邏輯:

 //設置點擊事件
adapter.setOnItemClickListener(new ItemClickAdapter.OnItemClickListener() {
    @Override
    public void onItemClick(View view, int position) {
        Toast.makeText(ItemClickActivity.this, adapter.getData().get(position) + "點擊事件", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onItemLongClick(View view, int position) {
        Toast.makeText(ItemClickActivity.this, adapter.getData().get(position) + "長按事件", Toast.LENGTH_SHORT).show();
    }
});

效果如下:

三、萬能ViewHolder

  ViewHolder的作用就是用來保存控件常量和查找控件(findViewById()),因爲RecyclerView的item是複用了,在item複用的時候也要每次都去查找控件,這樣比較浪費性能,項目中一般都是使用SparseArray來保存已經查找的控件,如果有則直接取出來使用,沒有再去findViewById()查找,保存在SparseArray中,方便下次使用。

public class BaseViewHolder extends RecyclerView.ViewHolder {
    // SparseArray 比 HashMap 更省內存,在某些條件下性能更好,只能存儲 key 爲 int 類型的數據,
    // 用來存放 View 以減少 findViewById 的次數
    private SparseArray<View> viewSparseArray;
    
    //這個是item的對象
    private View mItemView;

    public BaseViewHolder(View itemView) {
        super(itemView);
        this.mItemView = itemView;
        viewSparseArray = new SparseArray<>();
    }
    
    /**
     * 根據 ID 來獲取 View
     * @param viewId viewID
     * @param <T>    泛型
     * @return 將結果強轉爲 View 或 View 的子類型
     */
    public <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;
    }
    
   //獲取item的對象
    public View getItemView() {
        return mItemView;
    }
}

四、萬能適配器

  因爲每次創建Adapter繼承RecyclerView.Adapter的話比較都要實現onCreateViewHolder(),onBindViewHolder(),getItemCount()等相關方法,這裏面有部分代碼每次都要重複寫,我們可以寫一個基類,將重複的代碼寫在裏面,不重複不一樣的通過抽象方法來實現,定義萬能適配器需要繼承RecyclerView.Adapter,並實現相關方法,再通過抽象方法getLayoutId()獲取佈局文件,onBindItemHolder()綁定item數據,以後Adapter繼承這個類並實現這兩個方法即可。

public abstract class BaseListAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> {
    protected Context mContext;
    protected LayoutInflater mInflater;
    protected int mLastPosition = -1;

    protected List<T> mDataList = new ArrayList<>();

    public BaseListAdapter(Context context) {
        mContext = context;
        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }
    @NonNull
    @Override
    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new BaseViewHolder(mInflater.inflate(getLayoutId(), parent, false));
    }
    @Override
    public void onBindViewHolder(@NonNull BaseViewHolder holder, final int position) {
        onBindItemHolder(holder, position);
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (onItemClickListener != null)
                    onItemClickListener.onItemClick(v, position);
            }
        });
    }
    //局部刷新關鍵:帶payload的這個onBindViewHolder方法必須實現
    @Override
    public void onBindViewHolder(@NonNull BaseViewHolder holder, int position, List<Object> payloads) {
        if (payloads.isEmpty()) {
            onBindViewHolder(holder, position);
        } else {
            onBindItemHolder(holder, position, payloads);
        }
    }
    //item佈局文件
    public abstract int getLayoutId();
    //綁定item數據
    public abstract void onBindItemHolder(BaseViewHolder holder, int position);
    public void onBindItemHolder(BaseViewHolder holder, int position, List<Object> payloads) {
    }
    @Override
    public int getItemCount() {
        return mDataList.size();
    }
    /**
     * 獲取List列表數據
     * @return
     */
    public List<T> getDataList() {
        return mDataList;
    }
    /**
     * 設置數據
     * @param list
     */
    public void setDataList(Collection<T> list) {
        mLastPosition = -1;
        clear();
        mDataList.addAll(list);
        notifyDataSetChanged();
    }
    /**
     * 添加數據
     * @param list
     */
    public void addAll(Collection<T> list) {
        int lastIndex = this.mDataList.size();
        if (this.mDataList.addAll(list)) {
            notifyItemRangeInserted(lastIndex, list.size());
        }
    }
    /**
     * 移除某條數據
     * @param position
     */
    public void remove(int position) {
        this.mDataList.remove(position);
        notifyItemRemoved(position);

        if (position != (getDataList().size())) { // 如果移除的是最後一個,忽略
            notifyItemRangeChanged(position, this.mDataList.size() - position);
        }
    }
    /**
     * 清空列表
     */
    public void clear() {
        mDataList.clear();
        notifyDataSetChanged();
    }
    public OnItemClickListener onItemClickListener;
    public void setOnItemClickListener(OnItemClickListener listener) {
        this.onItemClickListener = listener;
    }
}

五、綜合使用

使用上面的BaseAdapter基類,只需要實現getLayoutId()onBindItemHolder()方法即可,將ViewHolder替換爲BaseViewHolder;

public class ItemClickAdapter extends BaseListAdapter<Goods> {
    //item子類點擊事件
    private OnItemChildClickListener mOnItemChildClickListener;
    public void setOnItemChildClickListener(OnItemChildClickListener onItemChildClickListener) {
        mOnItemChildClickListener = onItemChildClickListener;
    }
    
    public ItemClickAdapter(Context context) {
        super(context);
    }

    @Override
    public int getLayoutId() {
        return R.layout.item_normal;
    }

    @Override
    public void onBindItemHolder(BaseViewHolder holder, final int position) {
        List<Goods> dataList = getDataList();
        Goods goods = dataList.get(position);

        TextView tv_name = holder.getView(R.id.tv_name);
        tv_name.setText(goods.getName());

        holder.setText(R.id.tv_price, "$:" + goods.getPrice())
                .setText(R.id.tv_des, goods.getDes())
                .setOnClickListener(R.id.iv_head, new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (mOnItemChildClickListener != null)
                            mOnItemChildClickListener.onItemChildClick(v, position);
                    }
                });
    }
}

Activity的代碼如下:

public class ItemClickActivity extends AppCompatActivity implements OnItemClickListener, OnItemChildClickListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recyclerview);
        RecyclerView recyclerView = findViewById(R.id.recyclerView);

        List<Goods> goodsList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Goods goods = new Goods("第 " + i + " 個item", i * 100, "商品描述");
            goodsList.add(goods);
        }

        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        ItemClickAdapter adapter = new ItemClickAdapter(this);
        recyclerView.setAdapter(adapter);
        adapter.setDataList(goodsList);

        //設置item點擊事件
        adapter.setOnItemClickListener(this);
        adapter.setOnItemChildClickListener(this);
    }

    @Override
    public void onItemClick(View view, int position) {
        Toast.makeText(this, "item點擊事件", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onItemChildClick(View view, int position) {
        switch (view.getId()) {
            case R.id.iv_head:
                Toast.makeText(this, "item子類點擊事件: 頭像", Toast.LENGTH_SHORT).show();
                break;
            default:
                break;
        }
    }
}

點擊事件接口已經分別抽取爲一個單獨的類,方便複用:

//item點擊事件
public interface OnItemClickListener {
    void onItemClick(View view, int position);
}
//item子類點擊事件
public interface OnItemChildClickListener {
    void onItemChildClick(View view, int position);
}

效果如下:
在這裏插入圖片描述

六、LayoutManager解析

  最開始就提到,RecyclerView支持各種各樣的佈局效果,這是ListView所不具備的,而且RecyclerView不需要像ListView那樣if(contentView==null) {}else{}處理複用的邏輯,其核心關鍵在於RecyclerView.LayoutManager中,使用時我們是需要setLayoutManager()設置佈局管理器的。LayoutManager負責RecyclerView的佈局,其中包含itemView的獲取和回收。

RecyclerView提供了三中佈局管理器:

  • LinearLayoutManager      以列表的方式展示item,有水平方向RecyclerView.HORIZONTAL和垂直方向RecyclerView.VERTICAL;
  • GridLayoutManager       以網格的方式展示item,有水平方向和垂直方向;
  • StaggeredGridLayoutManager   以瀑布流的方式展示item,有水平方向和垂直方向。

這裏就不一一分析了,第一篇文章RecyclerView(一)已經做了詳細的介紹,不瞭解的同學可以回頭看一下。

6.1 LayoutManager 常見API

	//能否橫向滾動
    canScrollHorizontally();
    //能否縱向滾動
    canScrollVertically();
    //滾動到指定位置
    scrollToPosition(int position);

	//設置滾動的方向
    setOrientation(int orientation);
    //獲取滾動方向
    getOrientation();

	//獲取指定位置的Item View
    findViewByPosition(int position);
    //獲取第一個完全可見的Item位置
    findFirstCompletelyVisibleItemPosition();
    //獲取第一個可見Item的位置
    findFirstVisibleItemPosition();
    //獲取最後一個完全可見的Item位置
    findLastCompletelyVisibleItemPosition();
    //獲取最後一個可見Item的位置
    findLastVisibleItemPosition();

這裏列出了LayoutManager常見的API,平常都比較需要使用到,特別是在設置能否滾動,滾動位置,滾動方向,以及下拉刷新和上拉加載更多都需要用到這些API。

6.2 LinearLayoutManager源碼分析

  RecyclerView已經將一部分功能抽離出來,另外處理,也方便開發者自行拓展。LayoutManager負責RecyclerView的測量和佈局以及itemView的獲取和回收,那麼它們是怎麼實現的呢?這裏以LinearLayoutManager爲例,我們來觀察一下源碼:

 recyclerView.setLayoutManager(manager);

從設置佈局管理器方法入手,setLayoutManager()設置佈局管理器給RecyclerView使用:

   public void setLayoutManager(@Nullable LayoutManager layout) {
        if (layout == mLayout) {
            return;
        }
        stopScroll();
        if (mLayout != null) {//每次設置layoutManager都重新設置recyclerView的初始參數,動畫回收view等
           ·······
            mLayout.setRecyclerView(null);
            mLayout = null;
        } else {
            mRecycler.clear();
        }
     	·······
        mRecycler.updateViewCacheSize();
        requestLayout();
    }

在最後一行我們發現requestLayout(),那麼說明每次設置layoutManager都會執行View樹的繪製,那麼就會重走onMeasure()、onLayout()、onDraw()這個三個方法。

  public void requestLayout() {
            if (mRecyclerView != null) {
                mRecyclerView.requestLayout();
            }
        }

我們來看看RecyclerView的onMeasure()方法:

   @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {//如果mLayout爲空則採用默認測量
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
       	······
            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }
          
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            dispatchLayoutStep2();

            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
          	······
        } else {
            if (mHasFixedSize) {
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                return;
            }
          	······
            if (mAdapter != null) {//爲狀態添加子View的數量
                mState.mItemCount = mAdapter.getItemCount();
            } else {
                mState.mItemCount = 0;
            }
            startInterceptRequestLayout();
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);//用佈局文件測量
            stopInterceptRequestLayout(false);
            mState.mInPreLayout = false; // clear
        }
    }

如果mLayout爲空則採用默認測量方法,否則採用mLayout.onMeasure()方法進行測量。接下來看看RecyclerView的onLayout()方法:

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
        dispatchLayout();
        TraceCompat.endSection();
        mFirstLayoutComplete = true;
    }

接着看下dispatchLayout()方法:

  void dispatchLayout() {
  		//沒有adapter和layout直接返回
        if (mAdapter == null) {
            Log.e(TAG, "No adapter attached; skipping layout");
            // leave the state in START
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "No layout manager attached; skipping layout");
            // leave the state in START
            return;
        }
        mState.mIsMeasuring = false;
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();//佈局第一步:適配器更新、動畫運行、保存當前視圖的信息、運行預測佈局
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            // First 2 steps are done in onMeasure but looks like we have to run again due to
            // changed size.
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();//佈局第二步:最終實際的佈局視圖,如果有必要會多次運行
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();//佈局第三步:最後一步的佈局,保存視圖動畫、觸發動畫和不必要的清理。
    }

我發現dispatchLayoutStep1()dispatchLayoutStep2()dispatchLayoutStep3()等多個步驟,點進去發現,這是佈局的步驟。當mState.mLayoutStep == State.STEP_START的時候,即開始時,會執行第二個步驟:

 private void dispatchLayoutStep2() {
        startInterceptRequestLayout();
        onEnterLayoutOrScroll();
        mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
        mAdapterHelper.consumeUpdatesInOnePass();
        mState.mItemCount = mAdapter.getItemCount();
        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

        // Step 2: Run layout
        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);
		······
    }

這個是最終的實際佈局視圖步驟,如果有必要會多次運行。我們發現這裏調用了mLayout.onLayoutChildren(mRecycler, mState)方法,進行了子View的位置排版,由於onMeasure()時子View還不存在,所以測量和佈局都交給onLayoutChildren來處理。

  /**
     * {@inheritDoc}
     */
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // layout algorithm:
        // 1) 檢查子類和其他變量找到描點座標和描點位置
        // 2) 從開始填補,從底部堆積
        // 3) 從底部填補,從頂部堆積
        // 4) 從底部堆積來滿足需求
        // 創建佈局狀態
        if (DEBUG) {
            Log.d(TAG, "is pre layout:" + state.isPreLayout());
        }
        if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
            if (state.getItemCount() == 0) {
                removeAndRecycleAllViews(recycler);//移除所有子View
                return;
            }
        }
        if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
            mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
        }

        ensureLayoutState();
        mLayoutState.mRecycle = false;//禁止回收
        //顛倒繪製佈局
        resolveShouldLayoutReverse();

        final View focused = getFocusedChild();//獲取目前持有焦點的child
        if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
                || mPendingSavedState != null) {
            mAnchorInfo.reset();//重置錨點信息
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // 計算描點座標
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true;
        } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
                        >= mOrientationHelper.getEndAfterPadding()
                || mOrientationHelper.getDecoratedEnd(focused)
                <= mOrientationHelper.getStartAfterPadding())) {//如果當前持有焦點不在RecyclerView顯示範圍內,
            mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));//重新以此view爲描點更新信息保持可見範圍不變
        }
        if (DEBUG) {
            Log.d(TAG, "Anchor info:" + mAnchorInfo);
        }
		·······
        //計算第一佈局的方向
        int startOffset;
        int endOffset;
        final int firstLayoutDirection;
        if (mAnchorInfo.mLayoutFromEnd) {
            firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
                    : LayoutState.ITEM_DIRECTION_HEAD;
        } else {
            firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                    : LayoutState.ITEM_DIRECTION_TAIL;
        }

        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
        detachAndScrapAttachedViews(recycler);//將所有child detach並通過Scrap回收,
        mLayoutState.mInfinite = resolveIsInfinite();
        mLayoutState.mIsPreLayout = state.isPreLayout();
        // noRecycleSpace not needed: recycling doesn't happen in below's fill
        // invocations because mScrollingOffset is set to SCROLLING_OFFSET_NaN
        mLayoutState.mNoRecycleSpace = 0;
        if (mAnchorInfo.mLayoutFromEnd) {
            // 從頂部開始填充佈局
            updateLayoutStateToFillStart(mAnchorInfo);//使用描點信息更新layoutState,設置佈局方向爲end
            mLayoutState.mExtraFillSpace = extraForStart;
            fill(recycler, mLayoutState, state, false);////填充所有itemView
            startOffset = mLayoutState.mOffset;
            final int firstElement = mLayoutState.mCurrentPosition;//最後一個item
            if (mLayoutState.mAvailable > 0) {
                extraForEnd += mLayoutState.mAvailable;
            }
            // 從底部開始填充
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForEnd;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);////填充所有itemView
            endOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                // end could not consume all. add more items towards start
                extraForStart = mLayoutState.mAvailable;
                updateLayoutStateToFillStart(firstElement, startOffset);
                mLayoutState.mExtraFillSpace = extraForStart;
                fill(recycler, mLayoutState, state, false);////填充所有itemView
                startOffset = mLayoutState.mOffset;
            }
        } else {
            // 填充到最後
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForEnd;
            fill(recycler, mLayoutState, state, false);//填充子View
            endOffset = mLayoutState.mOffset;
            final int lastElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForStart += mLayoutState.mAvailable;
            }
            // 填充到最開始
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForStart;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);//填充所有itemView
            startOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                extraForEnd = mLayoutState.mAvailable;
                // start could not consume all it should. add more items towards end
                updateLayoutStateToFillEnd(lastElement, endOffset);
                mLayoutState.mExtraFillSpace = extraForEnd;
                fill(recycler, mLayoutState, state, false);////填充所有itemView
                endOffset = mLayoutState.mOffset;
            }
        }
		·······
    }

首先是狀態判斷和一些準備工作,對描點信息選擇和更新,最後填充佈局交給了fill()方法,爲什麼要fill兩次呢,我們來看看fill()方法:

  int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // max offset we should set is mFastScroll + available
        final int start = layoutState.mAvailable;
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);//判斷是否需要回收
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {//一直循環,知道沒有數據
            layoutChunkResult.resetInternal();
            layoutChunk(recycler, state, layoutState, layoutChunkResult);//添加一個child
            ······
            if (layoutChunkResult.mFinished) {//佈局結束,退出循環
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;//根據添加的child高度偏移計算
			······
            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);//根據佈局狀態盡心回收
            }
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }
     	······
        return start - layoutState.mAvailable;//返回這次填充的區域大小
    }

首先根據屏幕還有多少剩餘空間remainingSpace,根據這個數值減去子View所佔的空間大小,小於0時佈局子View結束,如果當前所有子VIew還沒有超過remainingSpace時,調用layoutChunk()安排一下View的位置。

   void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);//獲取當前postion需要展示的View
       	······
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        measureChildWithMargins(view, 0, 0);//測量子View
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
       	······
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        layoutDecoratedWithMargins(view, left, top, right, bottom);//調用child.layout進行佈局
        if (DEBUG) {
            Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
                    + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                    + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
        }
        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.hasFocusable();
    }

這個方法通過layoutState.next()獲取下一個view,通過addView添加到RecyclerView中,然後測量view的大小,計算left, top, right, bottom,最後ayoutDecoratedWithMargins()進行佈局。

   View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

獲取下一個view,並且如果mScrapList 中有緩存的View 則使用緩存的view,如果沒有mScrapList 就創建view,並添加到mScrapList 中。接下來getViewForPosition()方法涉及到了RecyclerView 的緩存機制,這裏暫時不講,後續會講解到。
所以如果要重寫LayoutManager的話必須重寫onLayoutChildren()方法佈局子view,最後再看看RecyclerView的onTouchEvent()方法中LayoutManager需要做什麼操作:

  @Override
    public boolean onTouchEvent(MotionEvent e) {
     	·······
        if (mLayout == null) {
            return false;
        }

        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
        final boolean canScrollVertically = mLayout.canScrollVertically();

       	······
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                mScrollPointerId = e.getPointerId(0);
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            } break;

          ······
            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
               ······
                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    boolean startScroll = false;
                    if (canScrollHorizontally) {
                        if (dx > 0) {
                            dx = Math.max(0, dx - mTouchSlop);
                        } else {
                            dx = Math.min(0, dx + mTouchSlop);
                        }
                        if (dx != 0) {
                            startScroll = true;
                        }
                    }
                    if (canScrollVertically) {
                        if (dy > 0) {
                            dy = Math.max(0, dy - mTouchSlop);
                        } else {
                            dy = Math.min(0, dy + mTouchSlop);
                        }
                        if (dy != 0) {
                            startScroll = true;
                        }
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mReusableIntPair[0] = 0;
                    mReusableIntPair[1] = 0;
                    if (dispatchNestedPreScroll(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                        dx -= mReusableIntPair[0];
                        dy -= mReusableIntPair[1];
                        // Updated the nested offsets
                        mNestedOffsets[0] += mScrollOffset[0];
                        mNestedOffsets[1] += mScrollOffset[1];
                        // Scroll has initiated, prevent parents from intercepting
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
					······
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            e)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;
			·······
            case MotionEvent.ACTION_UP: {
                mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally
                        ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
                final float yvel = canScrollVertically
                        ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetScroll();
            } break;
         ······
        }
        return true;
    }

這裏回調了mLayout.canScrollHorizontally()mLayout.canScrollVertically()方法,如果canScrollHorizontally== ture時,能左右滑動,如果canScrollVertically =true時,能上下滑動;如果滿足一系列條件則會交給scrollByInternal()處理:

 boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0;
        int unconsumedY = 0;
        int consumedX = 0;
        int consumedY = 0;

        consumePendingUpdateOperations();
        if (mAdapter != null) {
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            scrollStep(x, y, mReusableIntPair);
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1];
            unconsumedX = x - consumedX;
            unconsumedY = y - consumedY;
        }
        if (!mItemDecorations.isEmpty()) {
            invalidate();
        }
		······
        return consumedNestedScroll || consumedX != 0 || consumedY != 0;
    }

scrollStep(x, y, mReusableIntPair)方法中:如果是水平滑動則調用mLayout.scrollHorizontallyBy(),如果是垂直滑動則調用mLayout.scrollVerticallyBy()

 void scrollStep(int dx, int dy, @Nullable int[] consumed) {
      	·····
        int consumedX = 0;
        int consumedY = 0;
        if (dx != 0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        if (dy != 0) {
            consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
        }
        ······
    }

但是最終滑動的距離有LayoutManager實現,當滾動發生時,會觸發scrollVerticallyBy(),繼續看看scrollVerticallyBy()做了什麼:

   @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
            RecyclerView.State state) {
        if (mOrientation == HORIZONTAL) {
            return 0;
        }
        return scrollBy(dy, recycler, state);
    }

繼續看scrollBy():

 int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || delta == 0) {
            return 0;
        }
        ensureLayoutState();
        mLayoutState.mRecycle = true;
        final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;//佈局方向根據滾動方向來
        final int absDelta = Math.abs(delta);
        updateLayoutState(layoutDirection, absDelta, true, state);
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);//繼續填充
        if (consumed < 0) {
            if (DEBUG) {
                Log.d(TAG, "Don't have any more elements to scroll");
            }
            return 0;
        }
        final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;//計算滾動的距離
        mOrientationHelper.offsetChildren(-scrolled);//移動
        if (DEBUG) {
            Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
        }
        mLayoutState.mLastScrollDelta = scrolled;//記錄本次滾動的距離
        return scrolled;
    }

這兩個方法主要做三件事,通過updateLayoutState()修正了一些狀態,比如描點在哪裏,是否有動畫等,通過fill()先檢查有哪些view超出邊界,進行回收,然後重新填充新的view,通過offsetChildren()來改變view的Top或者Bottom值從而來達到移動的效果。同樣scrollHorizontallyBy()類似scrollVerticallyBy()這裏就不重複講解了。

至此!本文結束。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章