两年Android菜鸟手写RecyclerView布局——是时候
2020-09-28 本文已影响0人
码农的书柜
RecyclerView实现原理解析(自己手写RecycleView加深对RecycleView理解
)
涉及知识点
回收池
、scrollTo
、scrollBy
、onInterceptTouchEvent
、onTouchEvent
、onMeasure
、onLayout
、Stack
、getScaledTouchSlop
-
回收池
:复用同类型的Item,保证滑动展示大量数据Item时,内存占用不至于过大。永远都只使用回收池里几种类型Item -
onInterceptTouchEvent
、onTouchEvent
:
监听手势滑动,RecycleView(自己实现的
)继承ViewGroup,当手势滑动距离大于最小滑动距离时,ViewGroup本身自己处理滑动事件 -
Stack
:回收池采用栈的方式管理,这样可以保证滑动RecycleView时,以最快的方式复用顶部滑出去的View为底部刚添加的View所用 -
getScaledTouchSlop
:系统定义的一个区分手势点击或滑动的距离界限值
上述阐明了自己认为手写RecycleView需要关注的几个知识点,后续可能在继续补充
贴上RecycleView实现中心思想的代码
RecycleView.java
public class RecyclerView extends ViewGroup {
private static final String TAG = "RecyclerView";
// 最小滑动距离(通过此值判断滑动事件是否传递到子View)
private int touchSlop;
// 当前显示的View集合(无论后续上划,下划,最终展示使用的几个View集合)
private List<View> viewList;
// 初始化 第一屏最慢
private boolean needRelayout;
// y偏移量
private int scrollY;
// item 高度之和
private int[] heights;
private int width;
private int height;
// 行数
private int rowCount;
// view的第一行 是占内容的几行
private int firstRow;
private Adapter adapter;
Recycler recycler;
public RecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
ViewConfiguration configuration = ViewConfiguration.get(context);
this.touchSlop = configuration.getScaledTouchSlop();
this.viewList = new ArrayList<>();
this.needRelayout = true;
}
/**
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
Log.i(TAG, "onMeasure widthSize:" + widthSize);
Log.i(TAG, "onMeasure heightSize:" + heightSize);
int h;
if (adapter != null) {
this.rowCount = adapter.getCount();
heights = new int[rowCount];
for (int i = 0; i < heights.length; i++) {
heights[i] = adapter.getHeight(i);
}
}
// 数据的高度(假设所有数据都滑动展示时,所有子ItemView高度之和)
int tmpH = sumArray(heights, 0, heights.length);
h = Math.min(heightSize, tmpH);
// 设置RecycleView的最终高度
// 在onMeasure方法中最终调用 setMeasuredDimension 方法来确定控件的大小
setMeasuredDimension(widthSize, h);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public void setAdapter(Adapter adapter) {
this.adapter = adapter;
if (adapter != null) {
// 保存复用RecycleView的Item回收池
recycler = new Recycler(adapter.getViewTypeCount());
scrollY = 0;
firstRow = 0;
needRelayout = true;
requestLayout(); // 1 onMeasure 2 onLayout
}
}
public Adapter getAdapter() {
return adapter;
}
// 初始化
@Override
protected void onLayout(boolean changed, int mLeft, int mTop, int mRight, int mBottom) {
Log.i(TAG, "onLayout changed:" + changed);
Log.i(TAG, "onLayout needRelayout:" + needRelayout);
if (needRelayout || changed) {
needRelayout = false;
viewList.clear();
removeAllViews();
if (adapter != null) {
// 摆放所有RecycleView的Item的实际高度和宽度
width = mRight - mLeft;
height = mBottom - mTop;
Log.i(TAG, "onLayout width:" + width);
Log.i(TAG, "onLayout height:" + height);
int top = 0, bottom;
for (int i = 0; i < rowCount && top < height; i++) {
bottom = top + heights[i];
// 生成一个View
View view = makeAndStep(i, 0, top, width, bottom);
viewList.add(view);
top = bottom; // 循环摆放需要展示的几个Item View
}
}
}
}
/**
* @param row
* @param left
* @param top
* @param right
* @param bottom
* @return
*/
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;
}
private View obtainView(int row, int width, int height) {
int itemType = adapter.getItemViewType(row);
// 通过指定 position 位置的View类型去回收池查找View,存在则复用,不存在则调用 onCreateViewHolder 创建
View recycleView = recycler.get(itemType);
View view;
if (recycleView == null) {
view = adapter.onCreateViewHolder(row, recycleView, this);
if (view == null) {
throw new RuntimeException("onCreateViewHolder 必须填充布局");
}
} else {
view = adapter.onBinderViewHolder(row, recycleView, this);
}
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;
}
// -------------------------------------------------------------------------
// 当前滑动的y值
private int currentY;
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
Log.i(TAG, "onInterceptTouchEvent");
boolean intercept = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
currentY = (int) event.getRawY();
break;
}
case MotionEvent.ACTION_MOVE: {
// 当手指在屏幕上滑动距离大于 touchSlop 最小滑动距离时,则继承ViewGroup的 RecyclerView 拦截事件
int y2 = Math.abs(currentY - (int) event.getRawY());
if (y2 > touchSlop) {
intercept = true;
}
}
}
return intercept;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG, "onTouchEvent");
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE: {
// 移动的距离 y方向
int y2 = (int) event.getRawY();
// 大于0表示 上滑
// 小于0表示 下滑
int diffY = currentY - y2;
// 画布移动 并不影响子控件的位置
scrollBy(0, diffY);
}
}
return super.onTouchEvent(event);
}
@Override
public void scrollBy(int x, int y) {
// scrollY表示 第一个可见Item的左上顶点 距离屏幕的左上顶点的距离
scrollY += y;
scrollY = scrollBounds(scrollY);
// scrollY
if (scrollY > 0) {
// 上滑正 下滑负 边界值
while (scrollY > heights[firstRow]) {
// 1 上滑移除 2 上划加载 3下滑移除 4 下滑加载
removeView(viewList.remove(0));
scrollY -= heights[firstRow];
firstRow++;
}
// 上滑添加
while (getFillHeight() < height) {
int addLast = firstRow + viewList.size();
View view = obtainView(addLast, width, heights[addLast]);
viewList.add(viewList.size(), view);
}
} else if (scrollY < 0) {
// 下滑加载
while (scrollY < 0) {
int firstAddRow = firstRow - 1;
View view = obtainView(firstAddRow, width, heights[firstAddRow]);
viewList.add(0, view);
firstRow--;
scrollY += heights[firstRow + 1];
}
// 下滑移除
while (sumArray(heights, firstRow, viewList.size()) - scrollY - heights[firstRow + viewList.size() - 1] >= height) {
removeView(viewList.remove(viewList.size() - 1));
}
}
repositionViews();
}
private int scrollBounds(int scrollY) {
// 上滑
if (scrollY > 0) {
} else {
// 极限值 会取零 非极限值的情况下 scroll
scrollY = Math.max(scrollY, -sumArray(heights, 0, firstRow));
}
return scrollY;
}
/**
* @return 数据的高度 -scrollY
*/
private int getFillHeight() {
return sumArray(heights, firstRow, viewList.size()) - scrollY;
}
/**
* @param array
* @param firstIndex
* @param count
* @return 计算所有组合Item的总高度 与 RecycleView高度对比
*/
private int sumArray(int array[], int firstIndex, int count) {
int sum = 0;
count += firstIndex;
for (int i = firstIndex; i < count; i++) {
sum += array[i];
}
return sum;
}
private void repositionViews() {
int left, top, right, bottom, i;
top = -scrollY;
i = firstRow;
for (View view : viewList) {
bottom = top + heights[i++];
view.layout(0, top, width, bottom);
top = bottom;
}
}
@Override
public void removeView(View view) {
super.removeView(view);
int key = (int) view.getTag(R.id.tag_type_view);
recycler.put(view, key);
}
// -------------------------------------------------------------------------
interface Adapter {
View onCreateViewHolder(int position, View convertView, ViewGroup parent);
View onBinderViewHolder(int position, View convertView, ViewGroup parent);
// Item的类型
int getItemViewType(int row);
// Item的类型数量
int getViewTypeCount();
int getCount();
int getHeight(int index);
}
}
Recycler.java(回收池代码)
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;
}
}
}
MainAct.java(同学们用这个代码测试一下)
public class MainAct extends Activity {
private static final String TAG = "MainAct";
private RecyclerView recyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_act);
recyclerView = findViewById(R.id.table);
recyclerView.setAdapter(new RecyclerView.Adapter() {
@Override
public View onCreateViewHolder(int position, View convertView, ViewGroup parent) {
convertView = MainAct.this.getLayoutInflater()
.inflate(R.layout.item_table, parent, false);
TextView textView = convertView.findViewById(R.id.text1);
textView.setText("RecycleView Item " + position);
Log.i(TAG, "onCreateViewHolder: " + convertView.hashCode());
return convertView;
}
@Override
public View onBinderViewHolder(int position, View convertView, ViewGroup parent) {
TextView textView = convertView.findViewById(R.id.text1);
textView.setText("RecycleView Item " + position);
Log.i(TAG, "onBinderViewHolder: " + convertView.hashCode());
return convertView;
}
@Override
public int getItemViewType(int row) {
return 0;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public int getCount() {
// 通过改动这里的数字来模拟实际使用中RecycleView展示Item总数
return 100;
}
@Override
public int getHeight(int index) {
return 100;
}
});
}
}
item_table.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical" >
<TextView
android:id="@+id/text1"
android:background="@color/colorAccent"
android:textColor="@color/colorPrimaryDark"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="100dp" />
</LinearLayout>
ids.xml(相关的几个调试代码文件我都贴下吧)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="tag_type_view" type="id"/>
</resources>
main_act.xml
我就不贴了,就是一个手写RecycleView的布局,如果这个你也不想写,这种惰性
是学不好知识的,哈哈
PS:
手写RecycleView
滑动到最后一个Item程序会Crash
,这里实现只帮助大家加深对RecycleView实现原理了解
看完点赞,养成习惯,微信搜一搜「 程序猿养成中心 」关注这个喜欢写干货的程序员。
另外更有Android一线大厂面试完整考点、资料更新在我的Gitee,有面试需要的朋友们可以去参考参考,如果对你有帮助,可以点个Star哦!
Gitee地址:【老陈的Gitee】