自定义LayoutManager

效果图看这里,我是根据这里的代码继续往下写的
https://www.jianshu.com/p/a4b78f4cabd0

image.png

简单实现了view的复用,另外加载更多数据以后也能继续往上滚了。我也不知道到底咋判断属于加载更多数据,就简单按照滚动的距离大于0来判断的

import android.content.res.Resources;
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView;
import android.util.SparseArray;
import android.view.View;

public class CustomLayoutManager extends RecyclerView.LayoutManager {
    private int mFirstVisiPos;//屏幕可见的第一个View的Position
    private int mLastVisiPos;//屏幕可见的最后一个View的Position
    private int verticalScrollOffset;
    private int offsetH = 0;//
    private int leftMargin, rightMargin;
    private int smallWidth = 0;//1/3的宽

    private SparseArray mItemRects = new SparseArray<>();//key 是View的position,保存View的bounds 和 显示标志,

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
    }

    @Override
    public boolean isAutoMeasureEnabled() {
        return super.isAutoMeasureEnabled();
    }

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        super.onLayoutChildren(recycler, state);
        if (state.getItemCount() == 0) {
            removeAndRecycleAllViews(recycler);
            mItemRects.clear();
            verticalScrollOffset=0;
            return;
        }
        if (getChildCount() == 0 && state.isPreLayout()) {
            return;
        }
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                recycler.getViewForPosition(0).getLayoutParams();
        leftMargin = params.leftMargin;
        rightMargin = params.rightMargin;
        mFirstVisiPos = 0;
        mLastVisiPos = getItemCount() - 1;
        offsetH = getPaddingTop();
        if(getChildCount()==0){
            mItemRects.clear();
        }
        if(verticalScrollOffset>0){
            addMore=true;//上滑加载更多数据以后,
            offsetH=getDecoratedTop(getChildAt(0));
            mFirstVisiPos=getPosition(getChildAt(0));
        }
        //先把所有item从RecyclerView中detach
        detachAndScrapAttachedViews(recycler);

        layoutItem(recycler);
    }
    private boolean addMore=false;
    private void layoutItem(RecyclerView.Recycler recycler) {
        layoutItem(recycler, 0);
    }

    private void layoutItem(RecyclerView.Recycler recycler, int dy) {

        if (smallWidth == 0) {
            smallWidth = (Resources.getSystem().getDisplayMetrics().widthPixels
                    - getRightDecorationWidth(recycler.getViewForPosition(0))
                    - getLeftDecorationWidth(recycler.getViewForPosition(0))
                    - leftMargin - rightMargin) / 3;
        }
        if (dy >= 0) {
            //往上滑动,底部可能需要添加新的item
            int minPosition = 0;
            mLastVisiPos = getItemCount() - 1;
            if(getChildCount()>0){
                View lastChild = getChildAt(getChildCount() - 1);
                int lastPosition = getPosition(lastChild) ;
                minPosition=lastPosition+1;
                if(lastPosition!=getItemCount()-1&&!addMore){
                    minPosition = lastPosition + 1;
                    offsetH = getDecoratedBottom(lastChild);//测试发现快速滑动可能出现问题,因为最后一个显示的child的index=6,下边要添加7和8的时候,getoffsetH就不对了,所以得判断下
                    if(minPosition%3!=0){
                        offsetH=getDecoratedTop(lastChild);
                        if(minPosition%6==5){
                            offsetH=getDecoratedTop(lastChild)-lastChild.getHeight();
                        }
                    }
                }
            }

            //移除顶部即将不可见的item,要移除一次就是3个
            View child;
            while (getChildCount() > 2 && getDecoratedBottom(child = getChildAt(2)) - dy < getPaddingTop()-child.getHeight()) {
                System.out.println("remove child=====3个==" + getPosition(child)+"=="+getDecoratedBottom(child)+"/"+child.getHeight());
                removeAndRecycleView(child, recycler);
                removeAndRecycleView(getChildAt(1), recycler);
                removeAndRecycleView(getChildAt(0), recycler);
            }
            if(addMore){
                addMore=false;
                minPosition=mFirstVisiPos;
            }
            System.out.println("dy=== " + dy + " ========" + minPosition + "/" + mLastVisiPos + "=========" + offsetH+ "/" + getHeight());
            for (int i = minPosition; i <= mLastVisiPos; i++) {

                if (offsetH - dy > getHeight() - getPaddingBottom()) {
                    mLastVisiPos = i;
                    System.out.println("dy>0 break========" + mLastVisiPos + "====" + offsetH + "/" + dy + "/" + getHeight());
                    break;
                }
                addViewBottom(recycler, i);
            }

        } else {
            //顶部可能需要添加新的item
            int maxPosition = getItemCount() - 1;
            mFirstVisiPos = 0;
            if (getChildCount() > 0) {
                View firstChild = getChildAt(0);
                maxPosition = getPosition(firstChild) - 1;
            }
            //移除底部不可见的,
            View child;
            while (true) {
                if(getChildCount()==0){
                    break;
                }
               int topPre= getDecoratedTop(child = getChildAt(getChildCount() - 1))-dy;
               int removeTop=getHeight() - getPaddingBottom()+child.getHeight();
                System.out.println("remove child=======" + getPosition(child)+"==="+topPre+"==="+getHeight()+"=="+child.getHeight());
                if(topPre>removeTop){
                    removeAndRecycleView(child, recycler);
                }else{
                    break;
                }
            }
            for (int i = maxPosition; i >= mFirstVisiPos; i = i - 3) {//如果顶部要添加,那么一次至少3个
                Rect rect = mItemRects.get(i);
                if (rect.bottom - verticalScrollOffset - dy < getPaddingTop()) {
                    mFirstVisiPos = i + 1;
                    return;
                }
                addViewTop(recycler, i);
                addViewTop(recycler, i - 1);
                addViewTop(recycler, i - 2);
            }

        }

    }

    private void addViewTop(RecyclerView.Recycler recycler, int i) {
        if (i < 0) {
            return;
        }
        View child = recycler.getViewForPosition(i);
        addView(child, 0);//将View添加至RecyclerView中,childIndex为1,但是View的位置还是由layout的位置决定
        measureChildWithMargins(child, 0, 0);
        Rect rect = mItemRects.get(i);
        layoutDecoratedWithMargins(child, rect.left, rect.top - verticalScrollOffset, rect.right, rect.bottom - verticalScrollOffset);
    }

    private void addViewBottom(RecyclerView.Recycler recycler, int i) {
        if (i > getItemCount() - 1) {
            return;
        }
        View view = recycler.getViewForPosition(i);
        addView(view);
        measureChildWithMargins(view, 0, 0);
        int height = getDecoratedMeasuredHeight(view) + getBottomDecorationHeight(view);
        System.out.println("addViewBottom=======" + i + "/verticalScrollOffset=" + verticalScrollOffset + "======" + offsetH + "====" + height + "====" + getHeight());
        Rect rect = new Rect();
        switch (i % 6) {
            case 0:
                rect.set(0, offsetH, 2 * smallWidth, offsetH + height);
                layoutDecoratedWithMargins(view, 0, offsetH, 2 * smallWidth, offsetH + height);
                break;
            case 1:
                rect.set(2 * smallWidth, offsetH,
                        3 * smallWidth, offsetH + height / 2);
                layoutDecoratedWithMargins(view, 2 * smallWidth, offsetH,
                        3 * smallWidth, offsetH + height / 2);
                break;
            case 2:
                rect.set(2 * smallWidth, offsetH + height / 2,
                        3 * smallWidth, offsetH + height);
                layoutDecoratedWithMargins(view, 2 * smallWidth, offsetH + height / 2,
                        3 * smallWidth, offsetH + height);
                this.offsetH = this.offsetH + height;
                break;
            case 3:
                rect.set(0, offsetH,
                        smallWidth, offsetH + height / 2);
                layoutDecoratedWithMargins(view, 0, offsetH,
                        smallWidth, offsetH + height / 2);
                break;
            case 4:
                rect.set(0, offsetH + height / 2,
                        smallWidth, offsetH + height);
                layoutDecoratedWithMargins(view, 0, offsetH + height / 2,
                        smallWidth, offsetH + height);
                break;
            case 5:
                rect.set(smallWidth, offsetH,
                        3 * smallWidth, offsetH + height);
                layoutDecoratedWithMargins(view, smallWidth, offsetH,
                        3 * smallWidth, offsetH + height);
                this.offsetH = this.offsetH + height;
                break;
        }
        rect.offset(0, verticalScrollOffset);
        mItemRects.put(i, rect);
    }

    @Override
    public boolean canScrollVertically() {
        return true;
    }

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || dy == 0) {
            return 0;
        }
        //手指往上滑dy为正,手指往下滑dy为负
        layoutItem(recycler, dy);//先添加布局,再位移

        if (dy<0) {
            View firstChild = getChildAt(0);
            if (getPosition(firstChild) == 0 && getDecoratedTop(firstChild) - dy > 0) {
               //第一个child的position是0,并且top即将大于0,那就不能移动
                dy=getDecoratedTop(firstChild);
            }
        }else { //判断最后一个child的底部是否即将跑到屏幕底部的上边
            int bottom=getDecoratedBottom(getChildAt(getChildCount()-1));
            if(bottom- dy < getHeight() - getPaddingBottom()){
                dy=bottom-getHeight()+getPaddingBottom();
            }
        }

        //将竖直方向的偏移量+travel
        verticalScrollOffset += dy;
        // 调用该方法通知view在y方向上移动指定距离
        offsetChildrenVertical(-dy);
        System.out.println("scrollvertical===========" + dy + "=====" + verticalScrollOffset+"==offsetH===="+offsetH);
        return dy;
    }
}

自定义LayoutManager的步骤

写了几个简单的,现在来简单记录下流程
这里为写一个简单的线性的垂直布局为例
1.继承,实现抽象方法

class CustomLayoutManager:RecyclerView.LayoutManager(){

    override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
        return RecyclerView.LayoutParams(-2,-2)
    }
  1. 重写onLayoutChildren方法
    基本流程就是先判断是否有item,没有就return
    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
        super.onLayoutChildren(recycler, state)
        if (state.itemCount == 0) {
            removeAndRecycleAllViews(recycler);//这个view是被回收了。
            return;
        }
        if (getChildCount() == 0 && state.isPreLayout()) {
            return;
        }
        detachAndScrapAttachedViews(recycler);//这个是把view从页面拿走,单并没有回收view
        reset()
        layoutView(recycler,state,0)//这个就是用来布局的
    }
  1. addView到布局里
    这里分两种,一种往后边加view,一种往前边加view
    像平时见到的那种卡片一层一层的,也是这样写的,主要就是layout那些child的位置
    //top 根据index不同代表不同的属性。index=-1,这里表示的是最后一个child的bottom位置,如果是0,代表的是第一个child的top位置
    //position 新添加的view的adapter的position
    //index =0表示添加到上边,index=-1表示添加到下边,也就是末尾
    private fun addView(recycler: RecyclerView.Recycler, nearY:Int,position:Int,index:Int){
        val child=recycler.getViewForPosition(position)//获取item,系统内部处理的,有可以复用的就复用,没有就新create一个。
        addView(child,index)//添加到布局里
        measureChildWithMargins(child,0,0)//对child进行测量
        val heightChild=getDecoratedMeasuredHeight(child)//获取child的高度,包含itemDecoration的高度
        if(index==-1){
            layoutDecoratedWithMargins(child,left,nearY,right,nearY+heightChild)//对child的位置进行layout
        }else{
            layoutDecoratedWithMargins(child,left,nearY-heightChild,right,nearY)
        }
    }

4.啥时候addview?
//第一种情况,初始化的时候,child count==0,这时候肯定要add拉


        if(childCount==0){
            var top=0
            for(it in 0 ..maxPostion){
                val child=recycler.getViewForPosition(it)
                addView(child)
                measureChildWithMargins(child,0,0)
                val heightChild=getDecoratedMeasuredHeight(child)
                layoutDecoratedWithMargins(child,left,top,right,top+heightChild)
                top+=heightChild
                if(top>height-paddingBottom){//跑到屏幕外边去了,就不继续添加了。
                    maxPostion=it
                    break;
                }
            }
            return
        }


//第二种,滑动的时候添加新的,这时候childCount就不是0了
也分两种,手指上滑,底部可能需要添加view,手指下滑,顶部可能添加view
同时,移除view,也分两种,上滑,顶部可能需要移除view,下滑,底部可能需要移除view
判断条件也很简单,顶部移除的话,判断item的bottom是否在控件外边,也就是小于paddingTop
顶部添加,看item的top是否大于paddingTop。
底部道理一样,移除的话,判断item的top是否大于控件height-paddingBottom
添加的话,判断item的bottom是否小于height-paddingBottom
下边的dy就是手指移动的距离,dy大于0是手指往上滑,dy小于0是手指往下滑,
手指上滑的处理

            if(childCount>0){
                //remove the top views which are invisible
                var child=getChildAt(0)
                while(getDecoratedBottom(child)-dy

手指下滑的处理

            if(childCount>0){
                //remove views at the bottom that will be invisible
                var child=getChildAt(childCount-1)
                while (getDecoratedTop(child)-dy>height-paddingBottom){
                    removeAndRecycleView(child,recycler)
                    maxPostion=getPosition(child)-1
                    if(childCount==0){
                        break;
                    }
                    child=getChildAt(childCount-1)
                }
                //add  views at the top
                val childTop=getChildAt(0)
                val top=getDecoratedTop(childTop)
                if(top-dy>paddingTop&&minPosition>0){
                    minPosition--
                    addTopView(recycler,top)
                    println("add top view=========${minPosition}")
                }
            }
  1. 处理滑动事件
    这里的例子是垂直方向滑动,所以下边的方法返回true即可,然后处理对应方向的滑动事件
    override fun canScrollVertically(): Boolean {
        return true
    }

    override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
        if(itemCount==0||childCount==0){
            return  dy
        }
        layoutView(recycler,state,dy)//先布局,再处理view的位移
        var move=dy
        val child0=getChildAt(0)//最顶部的view
        val childTop=getDecoratedTop(child0)

        val childLast=getChildAt(childCount-1)//最底部的view
        val bottomChild=getDecoratedBottom(childLast)

        if(childTop-move>0){//防止第一个item往下移动,上边成了空白
            move=childTop
        }else if(bottomChild-move

另一种处理滑动事件的方法

我们需要写个callback来处理每个item的触摸事件,卡片式的用这种也很方便,如下图
首先自定义一个layoutmanager,比较简单了,layout child如下的位置即可,都是居中的,然后依次缩小,平移一定距离就可以了


image.png

完事我们添加item的触摸事件,用下边的helper

public ItemTouchHelper(Callback callback)

//使用
ItemTouchHelper(SwipCardCallBack(0,ItemTouchHelper.UP or ItemTouchHelper.DOWN
                    or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT,dps)).attachToRecyclerView(this)

简单的callback如下

class SwipCardCallBack:ItemTouchHelper.SimpleCallback{
    var datas= arrayListOf()
    constructor(dragDirs: Int, swipeDirs: Int,data:ArrayList) : super(dragDirs, swipeDirs){
        datas=data
    }

    override fun onMove(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?, target: RecyclerView.ViewHolder?): Boolean {
        return false
    }
    var rv:RecyclerView?=null;
    //拖动结束以后会走这里
    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        val data=datas.removeAt(0)
        datas.add(data)//这里数据不删除,就是把滑动的数据再放到最后。有具体需求再改
        rv?.adapter?.notifyDataSetChanged()
        println("========onSwiped===$direction====${datas[0]}")
    }

//拖动的时候不停的刷新child的大小位置等。
    override fun onChildDraw(c: Canvas?, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
        rv=recyclerView;
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
        val z=Math.hypot(dX.toDouble(),dY.toDouble()).toFloat()
        var factor=z/(recyclerView.width/2f);//移动的距离和宽度的一半 做比较
        if(factor>1){
            factor= 1f
        }
        for(i in 0 until  recyclerView.childCount-1){
            val child=recyclerView.getChildAt(i)
            val realPosition=recyclerView.getChildAdapterPosition(child)
            child.translationY=child.width/15*(realPosition-factor)
            child.scaleY=1f-(realPosition-factor)*0.05f
            child.scaleX=1f-(realPosition-factor)*0.05f
        }
    }
}

需要注意的问题

添加view的使用有addView(child,0)或者addView(child)
写的时候注意下,你的child到底应该添加到哪里,因为后边你可能用到getChildAt(index)
我写另外一个自定义的LayoutManager的时候,不是用的这篇文章的方法
整体流程是【主要处理控件的复用】
1.找到第一个child的索引first
2.完事根据总体移动的距离moveY,以及即将移动的距离dy,
开始计算index从0到first之间的child,是否在屏幕上,不在的话啥也不干,把控件占的距离加上即可,在的话,把这个child add进来
3.修正已经在屏幕上的child,计算位置是否在屏幕,不在移除,在的话重新修正位置
4.判断dy大于0的情况,也就是手指往上滑动,底部可能需要添加新的item。
如下这种,是根据给定的path来布局的,这个path比较简单,如箭头所示


image.png

你可能感兴趣的:(自定义LayoutManager)