傲视苍穹《Android》VIP专题安卓开发博客

两年Android菜鸟手写RecyclerView布局——是时候

2020-09-28  本文已影响0人  码农的书柜

RecyclerView实现原理解析(自己手写RecycleView加深对RecycleView理解)

涉及知识点

回收池scrollToscrollByonInterceptTouchEventonTouchEventonMeasureonLayoutStackgetScaledTouchSlop

  1. 回收池:复用同类型的Item,保证滑动展示大量数据Item时,内存占用不至于过大。永远都只使用回收池里几种类型Item

  2. onInterceptTouchEventonTouchEvent
    监听手势滑动,RecycleView(自己实现的)继承ViewGroup,当手势滑动距离大于最小滑动距离时,ViewGroup本身自己处理滑动事件

  3. Stack:回收池采用栈的方式管理,这样可以保证滑动RecycleView时,以最快的方式复用顶部滑出去的View为底部刚添加的View所用

  4. 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

上一篇下一篇

猜你喜欢

热点阅读