人不需要活太多樣子,你認真做一件事,就會解釋所有的事。 ——《不必交談時刻》
一、概述
上一篇文章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()
這裏就不重複講解了。
至此!本文結束。