基礎知識
重寫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
}
}