一、手写RecycleView的重点
1.View的回收池
2.如何设计适配器
3.滑动的边界
4.子View如何布局
二、回收池和适配器
- RecycleView内部有一个回收池负责缓存滑出屏幕的View,设计缓存是需要考虑到RecycleView的Item可能有多重布局样式,如何缓存下这些View并且区分出他们的种类。
考虑到RecycleView滑出屏幕一个View就需要一个新的Item进入的情况,使用 Stack作为缓存池,并且针对每一种Item的布局都分配一个Stack
在构造方法中可以看到针对每一种View都分配了一个Stack<View>,而这些Stack<View>对象又由一个数组对象views来管理,当然也可以由一个Map来管理
/**
* 回收池:回收 View 根据 Type 来存放回收的 View 对象
* 选取集合需要考虑到滑动的情况,先进先出的特性,因为 RecycleView 的一个 Item 滑出屏幕后有可能会被立即取出
*/
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 调用 Adapter的 onCreateViewHolder 创建一个Item,当第一屏的item都满时,完成第一屏的加载。当手指滑动时,划出屏幕的item会进入回收池,这时候屏幕加载新的item时会去回收池查看是否有item,并且布局和新进入的item一致,一致的话从回收池中拿出这个item进行复用,复用的方式是将这个item交给适配器(因为数据不一致),适配器拿到item后进行刷新,然后再绘制到屏幕上
三、适配器
RecycleView需要知道
1.一共有多少条数据要渲染
2.Item有多少个种类
3.创建布局
4.使用缓存的View刷新布局
参考以有的适配器模式,接口如下设计
interface Adapter {
View onCreateViewHodler(int position, View convertView, ViewGroup parent);
/**
* 刷新 View 的参数
*
* @param position
* @param convertView
* @param parent
* @return
*/
View onBinderViewHodler(int position, View convertView, ViewGroup parent);
//获取指定行数的 View 类型
int getItemViewType(int row);
//Item的类型数量
int getViewTypeCount();
// 数据的数量
int getCount();
// 每一个 Item 的高度
public int getHeight(int index);
}
四、RecycleView的布局
onMeasure
onMeasure 需要考虑到所有子View,sumArray就是计算出所有子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) {
// 获取到有几条数据
this.rowCount = adapter.getCount();
// 获取到所有数据的高
heights = new int[rowCount];
for (int i = 0; i < heights.length; i++) {
heights[i] = adapter.getHeight(i);
}
}
// 取布局设置的高以及数据总长度的高最小的一个
int tmpH = sumArray(heights, 0, heights.length);
// 取最小的高度
h = Math.min(heightSize, tmpH);
setMeasuredDimension(widthSize, h);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
onLayout中需要计算每一个子View的位置,这里上一个Item底部的位置就是下一个Item的Top
......
for (int i = 0; i < rowCount && top < height; i++) {
right = width;
bottom = top + heights[i];
// 生成一个View
View view = makeAndStep(i, 0, top, right, bottom);
viewList.add(view);
// 下一个 view 的 top 是上一个 View 的 bottom
top = bottom;//循环摆放
}
....
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;
}
五、如何处理滑动事件
需要监听手指按下的事件和移动的事件,当移动的距离大于滑动最小距离时认为是一次滑动事件
1.通过ViewConfiguration获取系统设定的最小滑动距离
2.onIntercept用来判断是否拦截事件,处理滑动事件是在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: {
// 当手指按下的位值 比在 Y 方向移动的距离大于最小滑动的距离,我们拦截这个事件
int y2 = Math.abs(currentY - (int) event.getRawY());
if (y2 > touchSlop) {
intercept = true;
}
}
}
return intercept;
}
onTouchEvent 中去计算滑动的距离,并执行滑动
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE: {
// 移动的距离 y方向
int y2 = (int) event.getRawY();
// // 上滑正 下滑负
int diffY = currentY - y2;
// 画布移动 并不影响子控件的位置
scrollBy(0, diffY);
}
}
return super.onTouchEvent(event);
}
六、从换从中获取View并且刷新布局
Item可以直接创建,也可以从缓存中获取,直接创建的话调用onCreateViewHolder创建View并加入到缓存中,从缓存中获取到的View通过onBinderViewHolder刷新数据,给View设置一个Tag可以通过这个Tag来区分Item的布局类型
private View obtainView(int row, int width, int height) {
// 获取到这一行 View 的类型
int itemType = adapter.getItemViewType(row);
// 根据类型去 缓存池中获取
View reclyView = recycler.get(itemType);
View view = null;
// 如果回收池里没有 View 使用 onCreateViewHolder 创建一个
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 一个 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;
}
七、处理滑动事件
1.首先要判断是否超出了滑动的边界 即滑动到最后一个View仍然上滑或者第一个VIew仍然下滑
2.如果上滑的高度已经大于当前可见的第一个Item的距离,就移除这个Item,使用while循环是因为防止用户一次性滑出多个View 所以使用
3.加入一个Item的条件是新加入Item后所有显示出的Item是否会超出高度
@Override
public void scrollBy(int x, int y) {
// scrollY表示 第一个可见Item的左上顶点 距离屏幕的左上顶点的距离
scrollY += y;
// 判断是否达到了极限条件 数据的最顶端和数据的最低端
scrollY = scrollBounds(scrollY);
// scrolly
if (scrollY > 0) {
/**
* 当用户滑动特别快的时候 可能一下子滑出去3,4个 View 所以要不断去判断 scrollY 是否比当前
* 第一个 View 的 heights 只内,如果不在继续移除,知道 scrollY 在 当前 第一个 item的高度范围内
*
*/
// 上滑正 下滑负 边界值
while (scrollY > heights[firstRow]) {
// 1 上滑移除 2 上划加载 3下滑移除 4 下滑加载
removeView(viewList.remove(0));
// 因为用户可能滑动的很快,可能一次性滑出了好几个View,所以用这个方式来
// 计算一次性滑出了几个 View
scrollY -= heights[firstRow];
firstRow++;
}
// 是否添加一个 View: 数据高度减去-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]);
// 因为是下滑加载,缓存永远在第一个位置
viewList.add(0, view);
firstRow--;
scrollY += heights[firstRow + 1];
}
// 总和高度 - 滑出屏幕的高度 scrollY - 最后一个 item 的高度 就等于 View 的高度
while (sumArray(heights, firstRow, viewList.size()) - scrollY - heights[firstRow + viewList.size() - 1]
>= height) {
removeView(viewList.remove(viewList.size() - 1));
}
} else {
}
// 重新摆放子View的位置
repositionViews();
}
网友评论