完成的自定义RecyclerView整体效果如下:可以支持item的滑动,惯性滑动,以及缓冲池添加回收item的功能,项目完整地址https://github.com/buder-cp/CustomView/tree/master/buder_DN_view/buderdn1920

recyclerview 删除item数据发生变化界面未刷新 recyclerview itemanimator_ci

首先我们手写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添加到视图中即可。

recyclerview 删除item数据发生变化界面未刷新 recyclerview itemanimator_手写RecyclerView_02

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

完整项目