自定義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個圓弧


簡單分析下流程,最後給出完整的代碼

弄個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
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章