手寫RecyclerView--實現item回收池、支持千萬級item

完成的自定義RecyclerView整體效果如下:可以支持item的滑動,慣性滑動,以及緩衝池添加回收item的功能,項目完整地址https://github.com/buder-cp/CustomView/tree/master/buder_DN_view/buderdn1920

首先我們手寫RecyclerView需要繼承ViewGroup,我們需要重寫ViewGroup的一些方法完成RecyclerVIew的測量、擺放、以及事件分發、移動等主要方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

protected void onLayout(boolean changed, int l, int t, int r, int b)

public boolean onInterceptTouchEvent(MotionEvent event)

public boolean onTouchEvent(MotionEvent event)
 
public void scrollBy(int x, int y) 

public void removeView(View view)

重寫以上方法主要是爲了完成:擺放、手寫RecyclerView 的高度 內容 測量、事件分發、移動、邊界判斷的功能。

步驟一:測量工作:

首先我們來完成RecyclerView的測量以及擺放,主要是重寫onMeasure、onLayout方法,

    @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);
    }

這裏我們需要計算出recyclerview列表的內容高度,從adapter中去獲取item的數目adapter.getCount(),然後獲取到每個item的高度,將每個item高度相加即可完成列表內容高的測量,tmpH是RecyclerView的整體內容高度,例如你有1000條數據的item的總高度,heightSize爲我們XML中設置的RecyclerView的高度,這裏我們取兩者中的小值setMeasuredDimension,即可完成RecyclerView的測量工作。

步驟二:擺放:

我們在onLayout中進行完成擺放工作,onLayout會調用兩次,我們在第一次初始化onLayout時進行初始化擺放即可。因此這裏使用變量needRelayout進行記錄。

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.i(TAG, "onLayout: ");
        if (needRelayout || changed) {
            needRelayout = false;
            viewList.clear();
            removeAllViews();
            if (adapter != null) {
                //擺放子控件
                width = r - l;
                height = b - t;
                int left, top = 0, right, bottom;
                //第一行不是從0開始
                top = -scrollY;
                //rowCount內容的item數量1000,height當前控件的高度
                for (int i = 0; i < rowCount && top < height; i++) {
                    bottom = top + heights[i];
                    //生成View
                    View view = makeAndStep(i, 0, top, width, bottom);
                    //計算View l  r   t   b
                    viewList.add(view);
                    top = bottom;
                }
            }
        }

    }

其中rawCount爲RecyclerVIew裏面item的總數量,需要注意的是我們看到的第一個item的top值不是從0開始算的,例如已經滑動到第100個item了,此時我們肉眼看到的第一行,已經是RecyclerView整體內容的第100個,如圖,左側是我們的RecyclerView看到的第一項item,但是對應整體內容的第100項,那他的高度就是已經滑動的距離-scrollY的值。至此,onLayout的初始化工作已經完成,我們將每個item實例化出來、addView添加到視圖中即可。

    private View makeAndStep(int row, int left, int top, int right, int bottom) {
        //實例化一個有寬度  高度的View
        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);
        //根據這個類型 返回相應View  (佈局)
        //初始化的時候 取不到
        View reclyView = recycler.getRecyclerView(itemType);
        View view = adapter.getView(row, reclyView, this);
        if (view == null) {
            throw new RuntimeException("convertView 不能爲空");
        }
        //View不可能爲空
        view.setTag(R.id.tag_type_view, itemType);
        view.setTag(R.id.tag_row, row);
        //測量
        //VIEW 打tag   row    type
        view.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
                , MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
        addView(view, 0);
        return view;
    }

在進行每個item初始化設置時,需要從緩衝池中獲取各個類型type的item,緩衝池就是一個二維數組,裝載着每種類型type的item:

import java.util.Stack;

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 addRecycledView(View view, int type) {
        views[type].push(view);
    }

    public View getRecyclerView(int type) {
        //只關心取到的View是對應的類型
        try {
            return views[type].pop();
        } catch (Exception e) {
            return null;
        }
    }
}

從緩衝池中獲取到viewType後,就可以從adapter.getView中獲取到view,

步驟三:事件攔截分發以及滑動:

    //攔截 滑動事件  預處理 事件的過程
    @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: {
                int y2 = Math.abs(currentY - (int) event.getRawY());
                if (y2 > touchSlop) {
                    intercept = true;
                }
                break;
            }
        }
        return intercept;
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
            }
            case MotionEvent.ACTION_MOVE: {
//                移動的距離   y方向
                int y2 = (int) event.getRawY();
                //   diffX>0    往左劃
                int diffY = currentY - y2;
                scrollBy(0, diffY);
            }
        }
        return super.onTouchEvent(event);
    }

其中最主要的是滑動scrollBy方法,這個是相對滑動,每次都滑動相對的距離diffY

    @Override
    public void scrollBy(int x, int y) {
        scrollY += y;
//     scrollY取值   0 ---- 屏幕 的高度   0---無限大   2
//修正一下  內容的總高度 是他的邊界值
        scrollY = scrollBounds(scrollY, firstRow, heights, height);
        if (scrollY > 0) {
            //            往上滑
            while (heights[firstRow] < scrollY) {
//              remove  item完全移出去了
                if (!viewList.isEmpty()) {
                    removeTop();
                }
                scrollY -= heights[firstRow];
                firstRow++;
            }
            //            scrollY=0

            while (getFilledHeight() < height) {
                addBottom();
            }

        } else if (scrollY < 0) {
            //            往下滑
            while (!viewList.isEmpty() && getFilledHeight() - heights[firstRow + viewList.size()] > height) {
                removeBottom();
            }

            while (0 > scrollY) {
                addTop();
                firstRow--;
                scrollY += heights[firstRow + 1];
            }
        }
//        重新對一個子控件進行重新layout
        repositionViews();

//        重繪
        awakenScrollBars();
    }

方法中不斷修正scrollY的取值,這裏主要是根據滑動的距離去刪除掉滑出的item的view,然後放回到回收池中,並添加新加入的itemview,並且重新對子控件進行佈局。

步驟四:慣性滑動:

慣性滑動使用VelocityTracker去監聽滑動速度,在手指擡起時觸發的event.UP事件中進行慣性滑動的判斷。

        case MotionEvent.ACTION_UP: {
                velocityTracker.computeCurrentVelocity(1000, maximumVelocity);

                int velocityY = (int) velocityTracker.getYVelocity();

                int initY = scrollY + sumArray(heights, 1, firstRow);
                int maxY = Math.max(0, sumArray(heights, 0, heights.length) - height);
//                判斷是否開啓 慣性滑動
                if (Math.abs(velocityY) > minimumVelocity) {
//                        線程  ---> 自己開線程
                    flinger.start(0, initY, 0, velocityY, 0, maxY);
                } else {

                    if (this.velocityTracker != null) {
                        this.velocityTracker.recycle();
                        this.velocityTracker = null;
                    }

                }
                break;
            }

判斷慣性滑動是否觸發,利用系統提供的configuration.getScaledMaximumFlingVelocity();滑動最大和最小速率進行判斷。如果需要進行慣性滑動,則開啓線程完成剩餘的滑動距離。

    class Flinger implements Runnable {
        //        scrollBy   (移動的偏移量)  而不是速度
        private Scroller scroller;
        //
        private int initY;

        void start(int initX, int initY, int initialVelocityX, int initialVelocityY, int maxX, int maxY) {
            scroller.fling(initX, initY, initialVelocityX
                    , initialVelocityY, 0, maxX, 0, maxY);
            this.initY = initY;
            post(this);
        }

        Flinger(Context context) {
            scroller = new Scroller(context);
        }

        @Override
        public void run() {
            if (scroller.isFinished()) {
                return;
            }

            boolean more = scroller.computeScrollOffset();
//
            int y = scroller.getCurrY();
            int diffY = initY - y;
            if (diffY != 0) {
                scrollBy(0, diffY);
                initY = y;
            }
            if (more) {
                post(this);
            }
        }

        boolean isFinished() {
            return scroller.isFinished();
        }
    }

完整項目

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章