自定义LayoutManager,在path上布局

基础知识

重写2个方法

    override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams
    
    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State)

第一个都差不多,去系统提供的几个里边复制下即可
第二个,主要就是把child添加进来
完事就是重写scroll方法,处理垂直或者水平滚动事件,移动child的位置,另外进行child的回收以及添加
基本步骤就是上边的了。
下边说下添加child的几个方法,基本就是固定的,主要 还是计算child的4个顶点坐标

    val child=recycler.getViewForPosition(index)
            addView(child)
            measureChildWithMargins(child,0,0)
    layoutDecoratedWithMargins(child, left,top,right,bottom)

①获取child
②添加child
③对child进行测量
④布局child,根据实际情况计算left,top,right,bottom的大小
基本就完事了。
下边说下几个获取child相关属性的方法,首先下边的添加间隔的大家都知道:

    addItemDecoration(object :RecyclerView.ItemDecoration(){
                    override fun getItemOffsets(outRect: Rect, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
                        outRect.apply {
                            top=20
                            bottom=20
                        }
                    }
                })

getTopDecorationHeight(child):这个返回的就是Decoration里的top,下边几个同理:

    getLeftDecorationWidth(child)
    getRightDecorationWidth(child)
    getBottomDecorationHeight(child)

瞅下源码就知道了:

        public int getTopDecorationHeight(View child) {
            return ((LayoutParams) child.getLayoutParams()).mDecorInsets.top;
        }

其他方法也可以,如下
calculateItemDecorationsForChild(View, Rect),rect里就有left,right,top,bottom的值
getDecoratedMeasuredHeight(child):child自身的高度,加上上边的top和bottom
getDecoratedMeasuredWidth(child):child的自身的宽,加上上边的 left和right

        public int getDecoratedMeasuredHeight(View child) {
            final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
            return child.getMeasuredHeight() + insets.top + insets.bottom;
        }

其他一些方法,下边就是child在parent中的top位置,算上decoration的top偏移量的。

其他3个方向也一个道理

       /**
             * Returns the top edge of the given child view within its parent, offset by any applied
             * {@link ItemDecoration ItemDecorations}.
             *
             * @param child Child to query
             * @return Child top edge with offsets applied
             * @see #getTopDecorationHeight(View)
             */
            public int getDecoratedTop(View child) {
                return child.getTop() - getTopDecorationHeight(child);
            }

实现的效果

随便弄个简单的path,2个圆弧

在这里插入图片描述

免费获取安卓开发架构的资料(包括Fultter、高级UI、性能优化、架构师课程、 NDK、Kotlin、混合式开发(ReactNative+Weex)和一线互联网公司关于android面试的题目汇总可以加:936332305 / 链接:点击链接加入【安卓开发架构】:https://jq.qq.com/?_wv=1027&k=515xp64

在这里插入图片描述

简单分析下流程,最后给出完整的代码

弄个path,然后计算下总长度

      path.reset()//简单添加2个圆弧测试下
            path.apply {
                moveTo(width/2f,20f)
                quadTo(width-1f,height/4f,width/2f,height/2f)
                quadTo(1f,height*3f/4,width/2f,height-10f)
    //            addCircle(width/2f,height/2f,Math.min(width,height)/2f-50,Path.Direction.CW)
            }
            pathMeasure.setPath(path,false)
            pathLength=pathMeasure.length

首先处理下最简单的,也就是不滑动,刚开始添加child,如下,
我们根据distance来计算child在path上的位置,方向。
对pathMeasure不熟悉的随便百度下即可,也不复杂。

    if(childCount==0){
                var index=0
                distance=0
                while (distance<pathLength&&index<itemCount){
                    val addViewDistance=addViewAtPosition(index,distance,recycler)
                    if(addViewDistance==0){
                        break;
                    }
                    distance+=addViewDistance
                    index++
                }
            }

先画个草图,好理解下边distance都是啥,线条就是从A到F
B,D,F就是child的中心点,也就是我们要拿到和A的距离来计算坐标,
AB就是第一个的distanceCurrent,AC+CD就是的哥child的distanceCurrent
在这里插入图片描述

具体方法如下,最开始说过了基本就4个方法

    private fun addViewAtPosition(index:Int,distance:Int,recycler: RecyclerView.Recycler):Int{
            val child=recycler.getViewForPosition(index)
            addView(child)
            measureChildWithMargins(child,0,0)
            val distanceCurrent=distance+child.measuredHeight/2f+getTopDecorationHeight(child)
            if(distanceCurrent>pathLength){
                //跑到路径外边去了,不做处理
                removeView(child)
                return  0
            }else{
                updateChildLocation(child,distanceCurrent)
                arrayRects.put(index,ChildRect(getDecoratedMeasuredHeight(child),getTopDecorationHeight(child),child.measuredHeight))
                return  getDecoratedMeasuredHeight(child)
            }
        }

这里对child的处理,根据distance获取位置,角度,完事计算它的4个顶点应该在的坐标,然后进行旋转即可,如下

    private fun updateChildLocation(child:View,distanceCurrent:Float){
            val childWidthHalf=child.measuredWidth/2
            val childHeightHalf=child.measuredHeight/2
            pathMeasure.getPosTan(distanceCurrent,pos,tan)
            layoutDecoratedWithMargins(child, (pos[0]-childWidthHalf).toInt()-getLeftDecorationWidth(child),
                    pos[1].toInt()-childHeightHalf-getTopDecorationHeight(child),
                    (pos[0]+childWidthHalf).toInt()+getRightDecorationWidth(child),
                    (pos[1]+childHeightHalf).toInt()+getBottomDecorationHeight(child))
            var degree=Math.toDegrees(Math.atan((tan[0]/tan[1]).toDouble())).toFloat()
            child.pivotX=child.width/2f
            child.pivotY=child.height/2f
            child.rotation=-degree
        }

添加不移动的view比较简单了,处理滑动的时候view的回收,新加比较麻烦,得首先想好
先简单模拟下。
我们后边都说上下,也就是开始和结尾。也可能是左右。
手指往上滑,那么顶部的view可能跑到屏幕外边,不可见,就得回收,底部可能需要添加新的child到页面上。
手指往下滑,顶部可能需要添加新的child,相反,底部可能有child不可见,需要回收
如下图,黑框是屏幕,可见的view,不咋屏幕外边的我们进行回收
在这里插入图片描述

首先允许处理y轴的滑动事件:

      override fun canScrollVertically(): Boolean {
            return true
        }

然后重写如下方法,处理手指滑动的距离dy,手指往上是正的,往下是负的

    private var moveY=0//记录总的偏移量
    override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State?): Int {
        if(childCount==0||dy==0){
            return 0
        }
        if(dy<0&&moveY-dy>0){
            return moveY
        }
        if(dy>0){
            val last=getChildAt(childCount-1)
            if(last!=null&&getPosition(last)==itemCount-1){
                println("distance:$distance========dy:$dy======$pathLength")
                if(distance<pathLength){
                    return 0
                }else{
                    return (distance-pathLength).toInt()
                }
            }
        }
        println("vertical=========$dy")
       val consumed= initView(recycler,dy)
        if(consumed>0){
            moveY-=dy
            initView(recycler,consumed-dy)
            moveY-=consumed-dy
        }else{
            moveY-=dy
        }
        return dy
    }

简单说下为啥里边 initView(recycler,dy)会执行2次。
举个例子,比如当前加载了倒数第一个child,就在屏幕最底部,完事手指滑动很快,也就是dy非常大,远远大于最后一个child的高度,那么我们在计算位置的时候按照dy偏移来算,可能最后一个child就不在屏幕底部,而是跑到上边去了,这不太合理,最后一个child不应该滑到屏幕上边去的,所以我们又把多余的算出,让他往回再移动一定距离。

这个manager和普通的LinearLayoutManager之类的不太一样,那种计算位置的时候并不处理dy了,之后计算完以后直接利用offsetChildrenVertical(dy) 最所有的child进行平移。而我们这里的线条是弯曲的,所以这种不行,这里在计算位置的时候,直接把dy加进去了。所以在判断最后一个child位置不对的时候,需要重新布局

看下滑动的时候重新布局,根据上边的图,我们找到第一个显示的child的所以2,完事先处理0到2之间的child,判断下,加上dy以后,判断它的位置是否在path上,小于0就认为不在。如果偏移dy以后在path上,那么我们就把这个child add进来


           distance=moveY-dy//总的偏移量
 
             val childTop=getChildAt(0)
             val first=getPosition(childTop)//第一个child的索引
             var add=0 //额外添加了几个view,手指往下滑的时候顶部可能需要添加view
             (0 until first).forEach {
                 val childRect=arrayRects.get(it)
                 childRect?.apply {
                     val distanceCurrent=distance+this.positionDistance()
 //                    println("顶部添加与否$it=========$distanceCurrent")
                     if(distanceCurrent<0){
 //                        if(dy<0)
 //                        println("顶部不添加$it=========$distanceCurrent")
                     }else{
                         val child=recycler.getViewForPosition(it)
                         addView(child,add)
                         measureChildWithMargins(child,0,0)
                         updateChildLocation(child,distanceCurrent.toFloat())
 //                        println("顶部添加$it===$distanceCurrent===${pos[0]}/${pos[1]}=======${child}====top:${child.top}")
                         arrayRects.put(it,ChildRect(getDecoratedMeasuredHeight(child),getTopDecorationHeight(child),child.measuredHeight))
                         add++
                     }
                     distance+=this.totalDistance()
                 }
             }

然后处理中间已经在屏幕上的child,因为有些可能需要移除
add就是上边刚新加的child个数,新加的就不处理了,要不distance就加了2次。移除的条件也简单,不在path的长度范围内的。

    var move=0//记录移除了几个view,移除以后child的位置会变化的,
                repeat(childCount-add){
                    var child=getChildAt(it-move+add)
                    val distanceCurrent=distance+child.measuredHeight/2f+getTopDecorationHeight(child)
                    distance+=getDecoratedMeasuredHeight(child)
    //                println("$it=${getPosition(child)}=====$distanceCurrent/$distance======height/top:${child.measuredHeight}/${getTopDecorationHeight(child)}===$move/${it}/${childCount}=====${first}")
                    if(distanceCurrent>=0&&distanceCurrent<=pathLength){
                        updateChildLocation(child,distanceCurrent)
                    }else{
                        detachAndScrapView(child,recycler)
                        move++
                    }
    
                }

然后处理dy大于0,底部可能需要添加新的child的情况

     

           if(dy>0){//手指往上,底部可能需要添加新的item
                   var index=getPosition(getChildAt(childCount-1))+1
    //                println("add new child from ======$index")
                    var totalAdd=0//记录添加的child的总高度
                    while (distance<pathLength&&index<itemCount){
                        val addViewDistance=addViewAtPosition(index,distance,recycler)
                        if(addViewDistance==0){
                            break;
                        }
                        distance+=addViewDistance
                        index++
                        totalAdd+=addViewDistance
                    }
                    if(totalAdd<dy){
    //                    println("happened=========$totalAdd/$dy")
                        //说明往上滑动的距离太大,高于添加的child的总高度,这时候就需要手动模拟往回移动一点距离。保证最后一个child不偏离底部太多
                        return totalAdd
                    }
                }

最后是完整的代码

    import android.graphics.Path
    import android.graphics.PathMeasure
    import android.support.v7.widget.RecyclerView
    import android.view.View
    import android.view.ViewGroup
    
    class PathLayoutManager:RecyclerView.LayoutManager(){
        var arrayRects= hashMapOf<Int,ChildRect>()//每次添加child的时候,记录下child的大小信息,方便回收以后计算距离
        var pathLength=1f//path的总长度
        var path= Path()//path
        val pathMeasure=PathMeasure()
        val pos=FloatArray(2)//某点的位置
        val tan=FloatArray(2)//某点的正切x,y
        override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
            return RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT)
        }
    
        inner  class ChildRect(var totalDecorHeight:Int,var decorationTop:Int ,var measureHeight: Int){
            fun totalDistance():Int{
                return totalDecorHeight
            }
            fun positionDistance():Int{
                return  measureHeight/2+decorationTop
            }
        }
    
        override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
            if (getItemCount() == 0) {//没有Item,界面空着吧
                detachAndScrapAttachedViews(recycler);
                return;
            }
            if (getChildCount() == 0 && state.isPreLayout()) {//state.isPreLayout()是支持动画的
                return;
            }
            arrayRects.clear()
            //onLayoutChildren方法在RecyclerView 初始化时 会执行两遍
            detachAndScrapAttachedViews(recycler);
            path.reset()//简单添加2个圆弧测试下
            path.apply {
                moveTo(width/2f,20f)
                quadTo(width-1f,height/4f,width/2f,height/2f)
                quadTo(1f,height*3f/4,width/2f,height-10f)
    //            addCircle(width/2f,height/2f,Math.min(width,height)/2f-50,Path.Direction.CW)
            }
            pathMeasure.setPath(path,false)
            pathLength=pathMeasure.length
            initView(recycler,0)
        }
        private fun addViewAtPosition(index:Int,distance:Int,recycler: RecyclerView.Recycler):Int{
            val child=recycler.getViewForPosition(index)
            addView(child)
            measureChildWithMargins(child,0,0)
            val distanceCurrent=distance+child.measuredHeight/2f+getTopDecorationHeight(child)
            if(distanceCurrent>pathLength){
                //跑到路径外边去了,不做处理
                removeView(child)
                return  0
            }else{
                updateChildLocation(child,distanceCurrent)
                arrayRects.put(index,ChildRect(getDecoratedMeasuredHeight(child),getTopDecorationHeight(child),child.measuredHeight))
                return  getDecoratedMeasuredHeight(child)
            }
        }
        var distance=0
        private fun initView(recycler: RecyclerView.Recycler,dy: Int):Int{
    
            println("dy==${dy}=====moveY=${moveY}========${childCount}")
    
            if(childCount==0){
                var index=0
                distance=0
                while (distance<pathLength&&index<itemCount){
                    val addViewDistance=addViewAtPosition(index,distance,recycler)
                    if(addViewDistance==0){
                        break;
                    }
                    distance+=addViewDistance
                    index++
                }
            }else{
                 distance=moveY-dy//总的偏移量
    
                val childTop=getChildAt(0)
                val first=getPosition(childTop)//第一个child的索引
                var add=0 //额外添加了几个view,手指往下滑的时候顶部可能需要添加view
                (0 until first).forEach {
                    val childRect=arrayRects.get(it)
                    childRect?.apply {
                        val distanceCurrent=distance+this.positionDistance()
    //                    println("顶部添加与否$it=========$distanceCurrent")
                        if(distanceCurrent<0){
    //                        if(dy<0)
    //                        println("顶部不添加$it=========$distanceCurrent")
                        }else{
                            val child=recycler.getViewForPosition(it)
                            addView(child,add)
                            measureChildWithMargins(child,0,0)
                            updateChildLocation(child,distanceCurrent.toFloat())
    //                        println("顶部添加$it===$distanceCurrent===${pos[0]}/${pos[1]}=======${child}====top:${child.top}")
                            arrayRects.put(it,ChildRect(getDecoratedMeasuredHeight(child),getTopDecorationHeight(child),child.measuredHeight))
                            add++
                        }
                        distance+=this.totalDistance()
                    }
                }
    //            println("处理已添加的child========count${childCount}  add:$add")
                var move=0//记录移除了几个view,移除以后child的位置会变化的,
                repeat(childCount-add){
                    var child=getChildAt(it-move+add)
                    val distanceCurrent=distance+child.measuredHeight/2f+getTopDecorationHeight(child)
                    distance+=getDecoratedMeasuredHeight(child)
    //                println("$it=${getPosition(child)}=====$distanceCurrent/$distance======height/top:${child.measuredHeight}/${getTopDecorationHeight(child)}===$move/${it}/${childCount}=====${first}")
                    if(distanceCurrent>=0&&distanceCurrent<=pathLength){
                        updateChildLocation(child,distanceCurrent)
                    }else{
                        detachAndScrapView(child,recycler)
                        move++
                    }
    
                }
                if(dy>0){//手指往上,底部可能需要添加新的item
                   var index=getPosition(getChildAt(childCount-1))+1
    //                println("add new child from ======$index")
                    var totalAdd=0//记录添加的child的总高度
                    while (distance<pathLength&&index<itemCount){
                        val addViewDistance=addViewAtPosition(index,distance,recycler)
                        if(addViewDistance==0){
                            break;
                        }
                        distance+=addViewDistance
                        index++
                        totalAdd+=addViewDistance
                    }
                    if(totalAdd<dy){
    //                    println("happened=========$totalAdd/$dy")
                        //说明往上滑动的距离太大,高于添加的child的总高度,这时候就需要手动模拟往回移动一点距离。保证最后一个child不偏离底部太多
                        return totalAdd
                    }
                }
            }
            return 0
        }
    
        private fun updateChildLocation(child:View,distanceCurrent:Float){
            val childWidthHalf=child.measuredWidth/2
            val childHeightHalf=child.measuredHeight/2
            pathMeasure.getPosTan(distanceCurrent,pos,tan)
            layoutDecoratedWithMargins(child, (pos[0]-childWidthHalf).toInt()-getLeftDecorationWidth(child),
                    pos[1].toInt()-childHeightHalf-getTopDecorationHeight(child),
                    (pos[0]+childWidthHalf).toInt()+getRightDecorationWidth(child),
                    (pos[1]+childHeightHalf).toInt()+getBottomDecorationHeight(child))
            var degree=Math.toDegrees(Math.atan((tan[0]/tan[1]).toDouble())).toFloat()
            child.pivotX=child.width/2f
            child.pivotY=child.height/2f
            child.rotation=-degree
        }
        override fun canScrollVertically(): Boolean {
            return true
        }
        private var moveY=0//记录总的偏移量
        override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State?): Int {
            if(childCount==0||dy==0){
                return 0
            }
            if(dy<0&&moveY-dy>0){
                return moveY
            }
    
            if(dy>0){
                val last=getChildAt(childCount-1)
                if(last!=null&&getPosition(last)==itemCount-1){
                    println("distance:$distance========dy:$dy======$pathLength")
                    if(distance<pathLength){
                        return 0
                    }else{
                        return (distance-pathLength).toInt()
                    }
                }
            }
            println("vertical=========$dy")
           val consumed= initView(recycler,dy)
            if(consumed>0){
                moveY-=dy
                initView(recycler,consumed-dy)
                moveY-=consumed-dy
            }else{
                moveY-=dy
            }
    
            return dy
        }

效果图:

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值