自定义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个圆弧


image.png

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

弄个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

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


image.png

具体方法如下,最开始说过了基本就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,屏幕外边的我们进行回收


image.png

首先允许处理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(distance0){
            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

最后是完整的代码

刚写完,也许哪里写的不好,等以后发现再改。


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()//每次添加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=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 (distance0){
            return moveY
        }

        if(dy>0){
            val last=getChildAt(childCount-1)
            if(last!=null&&getPosition(last)==itemCount-1){
                println("distance:$distance========dy:$dy======$pathLength")
                if(distance0){
            moveY-=dy
            initView(recycler,consumed-dy)
            moveY-=consumed-dy
        }else{
            moveY-=dy
        }

        return dy
    }
}
image.png

你可能感兴趣的:(自定义LayoutManager,在path上布局)