一篡石、手寫RecycleView的重點(diǎn)
1.View的回收池
2.如何設(shè)計(jì)適配器
3.滑動(dòng)的邊界
4.子View如何布局
二燥狰、回收池和適配器
- RecycleView內(nèi)部有一個(gè)回收池負(fù)責(zé)緩存滑出屏幕的View于宙,設(shè)計(jì)緩存是需要考慮到RecycleView的Item可能有多重布局樣式荚坞,如何緩存下這些View并且區(qū)分出他們的種類。
考慮到RecycleView滑出屏幕一個(gè)View就需要一個(gè)新的Item進(jìn)入的情況询张,使用 Stack作為緩存池,并且針對(duì)每一種Item的布局都分配一個(gè)Stack
在構(gòu)造方法中可以看到針對(duì)每一種View都分配了一個(gè)Stack<View>,而這些Stack<View>對(duì)象又由一個(gè)數(shù)組對(duì)象views來管理,當(dāng)然也可以由一個(gè)Map來管理
/**
* 回收池:回收 View 根據(jù) Type 來存放回收的 View 對(duì)象
* 選取集合需要考慮到滑動(dòng)的情況,先進(jìn)先出的特性史辙,因?yàn)?RecycleView 的一個(gè) Item 滑出屏幕后有可能會(huì)被立即取出
*/
public class Recycler {
private Stack<View>[] views;
public Recycler(int typeNumber) {
views = new Stack[typeNumber];
for (int i = 0; i < typeNumber; i++) {
views[i] = new Stack<View>();
}
}
public void put(View view, int type) {
views[type].push(view);
}
public View get(int type) {
try {
return views[type].pop();
} catch (Exception e) {
return null;
}
}
}
- RecycleView 調(diào)用 Adapter的 onCreateViewHolder 創(chuàng)建一個(gè)Item,當(dāng)?shù)谝黄恋膇tem都滿時(shí)佩伤,完成第一屏的加載聊倔。當(dāng)手指滑動(dòng)時(shí),劃出屏幕的item會(huì)進(jìn)入回收池生巡,這時(shí)候屏幕加載新的item時(shí)會(huì)去回收池查看是否有item耙蔑,并且布局和新進(jìn)入的item一致,一致的話從回收池中拿出這個(gè)item進(jìn)行復(fù)用障斋,復(fù)用的方式是將這個(gè)item交給適配器(因?yàn)閿?shù)據(jù)不一致)纵潦,適配器拿到item后進(jìn)行刷新,然后再繪制到屏幕上
三垃环、適配器
RecycleView需要知道
1.一共有多少條數(shù)據(jù)要渲染
2.Item有多少個(gè)種類
3.創(chuàng)建布局
4.使用緩存的View刷新布局
參考以有的適配器模式邀层,接口如下設(shè)計(jì)
interface Adapter {
View onCreateViewHodler(int position, View convertView, ViewGroup parent);
/**
* 刷新 View 的參數(shù)
*
* @param position
* @param convertView
* @param parent
* @return
*/
View onBinderViewHodler(int position, View convertView, ViewGroup parent);
//獲取指定行數(shù)的 View 類型
int getItemViewType(int row);
//Item的類型數(shù)量
int getViewTypeCount();
// 數(shù)據(jù)的數(shù)量
int getCount();
// 每一個(gè) Item 的高度
public int getHeight(int index);
}
四、RecycleView的布局
onMeasure
onMeasure 需要考慮到所有子View遂庄,sumArray就是計(jì)算出所有子View的高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int h = 0;
if (adapter != null) {
// 獲取到有幾條數(shù)據(jù)
this.rowCount = adapter.getCount();
// 獲取到所有數(shù)據(jù)的高
heights = new int[rowCount];
for (int i = 0; i < heights.length; i++) {
heights[i] = adapter.getHeight(i);
}
}
// 取布局設(shè)置的高以及數(shù)據(jù)總長度的高最小的一個(gè)
int tmpH = sumArray(heights, 0, heights.length);
// 取最小的高度
h = Math.min(heightSize, tmpH);
setMeasuredDimension(widthSize, h);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
onLayout中需要計(jì)算每一個(gè)子View的位置,這里上一個(gè)Item底部的位置就是下一個(gè)Item的Top
......
for (int i = 0; i < rowCount && top < height; i++) {
right = width;
bottom = top + heights[i];
// 生成一個(gè)View
View view = makeAndStep(i, 0, top, right, bottom);
viewList.add(view);
// 下一個(gè) view 的 top 是上一個(gè) View 的 bottom
top = bottom;//循環(huán)擺放
}
....
private View makeAndStep(int row, int left, int top, int right, int bottom) {
View view = obtainView(row, right - left, bottom - top);
view.layout(left, top, right, bottom);
return view;
}
五寥院、如何處理滑動(dòng)事件
需要監(jiān)聽手指按下的事件和移動(dòng)的事件,當(dāng)移動(dòng)的距離大于滑動(dòng)最小距離時(shí)認(rèn)為是一次滑動(dòng)事件
1.通過ViewConfiguration獲取系統(tǒng)設(shè)定的最小滑動(dòng)距離
2.onIntercept用來判斷是否攔截事件,處理滑動(dòng)事件是在onTouchEvent中涛目。
ViewConfiguration configuration = ViewConfiguration.get(context);
this.touchSlop = configuration.getScaledTouchSlop();
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercept = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
currentY = (int) event.getRawY();
break;
}
case MotionEvent.ACTION_MOVE: {
// 當(dāng)手指按下的位值 比在 Y 方向移動(dòng)的距離大于最小滑動(dòng)的距離秸谢,我們攔截這個(gè)事件
int y2 = Math.abs(currentY - (int) event.getRawY());
if (y2 > touchSlop) {
intercept = true;
}
}
}
return intercept;
}
onTouchEvent 中去計(jì)算滑動(dòng)的距離,并執(zhí)行滑動(dòng)
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE: {
// 移動(dòng)的距離 y方向
int y2 = (int) event.getRawY();
// // 上滑正 下滑負(fù)
int diffY = currentY - y2;
// 畫布移動(dòng) 并不影響子控件的位置
scrollBy(0, diffY);
}
}
return super.onTouchEvent(event);
}
六、從換從中獲取View并且刷新布局
Item可以直接創(chuàng)建霹肝,也可以從緩存中獲取估蹄,直接創(chuàng)建的話調(diào)用onCreateViewHolder創(chuàng)建View并加入到緩存中,從緩存中獲取到的View通過onBinderViewHolder刷新數(shù)據(jù)沫换,給View設(shè)置一個(gè)Tag可以通過這個(gè)Tag來區(qū)分Item的布局類型
private View obtainView(int row, int width, int height) {
// 獲取到這一行 View 的類型
int itemType = adapter.getItemViewType(row);
// 根據(jù)類型去 緩存池中獲取
View reclyView = recycler.get(itemType);
View view = null;
// 如果回收池里沒有 View 使用 onCreateViewHolder 創(chuàng)建一個(gè)
if (reclyView == null) {
view = adapter.onCreateViewHodler(row, reclyView, this);
if (view == null) {
throw new RuntimeException("onCreateViewHodler 必須填充布局");
}
} else {
// 否則使用onBinderView
view = adapter.onBinderViewHodler(row, reclyView, this);
}
// 給View 一個(gè) Tag
view.setTag(R.id.tag_type_view, itemType);
view.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
addView(view, 0);
return view;
}
七臭蚁、處理滑動(dòng)事件
1.首先要判斷是否超出了滑動(dòng)的邊界 即滑動(dòng)到最后一個(gè)View仍然上滑或者第一個(gè)VIew仍然下滑
2.如果上滑的高度已經(jīng)大于當(dāng)前可見的第一個(gè)Item的距離,就移除這個(gè)Item,使用while循環(huán)是因?yàn)榉乐褂脩粢淮涡曰龆鄠€(gè)View 所以使用
3.加入一個(gè)Item的條件是新加入Item后所有顯示出的Item是否會(huì)超出高度
@Override
public void scrollBy(int x, int y) {
// scrollY表示 第一個(gè)可見Item的左上頂點(diǎn) 距離屏幕的左上頂點(diǎn)的距離
scrollY += y;
// 判斷是否達(dá)到了極限條件 數(shù)據(jù)的最頂端和數(shù)據(jù)的最低端
scrollY = scrollBounds(scrollY);
// scrolly
if (scrollY > 0) {
/**
* 當(dāng)用戶滑動(dòng)特別快的時(shí)候 可能一下子滑出去3,4個(gè) View 所以要不斷去判斷 scrollY 是否比當(dāng)前
* 第一個(gè) View 的 heights 只內(nèi)讯赏,如果不在繼續(xù)移除垮兑,知道 scrollY 在 當(dāng)前 第一個(gè) item的高度范圍內(nèi)
*
*/
// 上滑正 下滑負(fù) 邊界值
while (scrollY > heights[firstRow]) {
// 1 上滑移除 2 上劃加載 3下滑移除 4 下滑加載
removeView(viewList.remove(0));
// 因?yàn)橛脩艨赡芑瑒?dòng)的很快,可能一次性滑出了好幾個(gè)View漱挎,所以用這個(gè)方式來
// 計(jì)算一次性滑出了幾個(gè) View
scrollY -= heights[firstRow];
firstRow++;
}
// 是否添加一個(gè) View: 數(shù)據(jù)高度減去-scrollY的值
while (getFillHeight() < height) {
int addLast = firstRow + viewList.size();
View view = obtainView(addLast, width, heights[addLast]);
viewList.add(viewList.size(), view);
}
// 下滑添加
} else if (scrollY < 0) {
// 4 下滑加載
while (scrollY < 0) {
int firstAddRow = firstRow - 1;
View view = obtainView(firstAddRow, width, heights[firstAddRow]);
// 因?yàn)槭窍禄虞d系枪,緩存永遠(yuǎn)在第一個(gè)位置
viewList.add(0, view);
firstRow--;
scrollY += heights[firstRow + 1];
}
// 總和高度 - 滑出屏幕的高度 scrollY - 最后一個(gè) item 的高度 就等于 View 的高度
while (sumArray(heights, firstRow, viewList.size()) - scrollY - heights[firstRow + viewList.size() - 1]
>= height) {
removeView(viewList.remove(viewList.size() - 1));
}
} else {
}
// 重新擺放子View的位置
repositionViews();
}