基础知识
重写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
先画个草图,好理解下边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(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
}
}