效果图看这里,我是根据这里的代码继续往下写的
https://www.jianshu.com/p/a4b78f4cabd0
简单实现了view的复用,另外加载更多数据以后也能继续往上滚了。我也不知道到底咋判断属于加载更多数据,就简单按照滚动的距离大于0来判断的
import android.content.res.Resources;
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView;
import android.util.SparseArray;
import android.view.View;
public class CustomLayoutManager extends RecyclerView.LayoutManager {
private int mFirstVisiPos;//屏幕可见的第一个View的Position
private int mLastVisiPos;//屏幕可见的最后一个View的Position
private int verticalScrollOffset;
private int offsetH = 0;//
private int leftMargin, rightMargin;
private int smallWidth = 0;//1/3的宽
private SparseArray mItemRects = new SparseArray<>();//key 是View的position,保存View的bounds 和 显示标志,
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
}
@Override
public boolean isAutoMeasureEnabled() {
return super.isAutoMeasureEnabled();
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
mItemRects.clear();
verticalScrollOffset=0;
return;
}
if (getChildCount() == 0 && state.isPreLayout()) {
return;
}
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
recycler.getViewForPosition(0).getLayoutParams();
leftMargin = params.leftMargin;
rightMargin = params.rightMargin;
mFirstVisiPos = 0;
mLastVisiPos = getItemCount() - 1;
offsetH = getPaddingTop();
if(getChildCount()==0){
mItemRects.clear();
}
if(verticalScrollOffset>0){
addMore=true;//上滑加载更多数据以后,
offsetH=getDecoratedTop(getChildAt(0));
mFirstVisiPos=getPosition(getChildAt(0));
}
//先把所有item从RecyclerView中detach
detachAndScrapAttachedViews(recycler);
layoutItem(recycler);
}
private boolean addMore=false;
private void layoutItem(RecyclerView.Recycler recycler) {
layoutItem(recycler, 0);
}
private void layoutItem(RecyclerView.Recycler recycler, int dy) {
if (smallWidth == 0) {
smallWidth = (Resources.getSystem().getDisplayMetrics().widthPixels
- getRightDecorationWidth(recycler.getViewForPosition(0))
- getLeftDecorationWidth(recycler.getViewForPosition(0))
- leftMargin - rightMargin) / 3;
}
if (dy >= 0) {
//往上滑动,底部可能需要添加新的item
int minPosition = 0;
mLastVisiPos = getItemCount() - 1;
if(getChildCount()>0){
View lastChild = getChildAt(getChildCount() - 1);
int lastPosition = getPosition(lastChild) ;
minPosition=lastPosition+1;
if(lastPosition!=getItemCount()-1&&!addMore){
minPosition = lastPosition + 1;
offsetH = getDecoratedBottom(lastChild);//测试发现快速滑动可能出现问题,因为最后一个显示的child的index=6,下边要添加7和8的时候,getoffsetH就不对了,所以得判断下
if(minPosition%3!=0){
offsetH=getDecoratedTop(lastChild);
if(minPosition%6==5){
offsetH=getDecoratedTop(lastChild)-lastChild.getHeight();
}
}
}
}
//移除顶部即将不可见的item,要移除一次就是3个
View child;
while (getChildCount() > 2 && getDecoratedBottom(child = getChildAt(2)) - dy < getPaddingTop()-child.getHeight()) {
System.out.println("remove child=====3个==" + getPosition(child)+"=="+getDecoratedBottom(child)+"/"+child.getHeight());
removeAndRecycleView(child, recycler);
removeAndRecycleView(getChildAt(1), recycler);
removeAndRecycleView(getChildAt(0), recycler);
}
if(addMore){
addMore=false;
minPosition=mFirstVisiPos;
}
System.out.println("dy=== " + dy + " ========" + minPosition + "/" + mLastVisiPos + "=========" + offsetH+ "/" + getHeight());
for (int i = minPosition; i <= mLastVisiPos; i++) {
if (offsetH - dy > getHeight() - getPaddingBottom()) {
mLastVisiPos = i;
System.out.println("dy>0 break========" + mLastVisiPos + "====" + offsetH + "/" + dy + "/" + getHeight());
break;
}
addViewBottom(recycler, i);
}
} else {
//顶部可能需要添加新的item
int maxPosition = getItemCount() - 1;
mFirstVisiPos = 0;
if (getChildCount() > 0) {
View firstChild = getChildAt(0);
maxPosition = getPosition(firstChild) - 1;
}
//移除底部不可见的,
View child;
while (true) {
if(getChildCount()==0){
break;
}
int topPre= getDecoratedTop(child = getChildAt(getChildCount() - 1))-dy;
int removeTop=getHeight() - getPaddingBottom()+child.getHeight();
System.out.println("remove child=======" + getPosition(child)+"==="+topPre+"==="+getHeight()+"=="+child.getHeight());
if(topPre>removeTop){
removeAndRecycleView(child, recycler);
}else{
break;
}
}
for (int i = maxPosition; i >= mFirstVisiPos; i = i - 3) {//如果顶部要添加,那么一次至少3个
Rect rect = mItemRects.get(i);
if (rect.bottom - verticalScrollOffset - dy < getPaddingTop()) {
mFirstVisiPos = i + 1;
return;
}
addViewTop(recycler, i);
addViewTop(recycler, i - 1);
addViewTop(recycler, i - 2);
}
}
}
private void addViewTop(RecyclerView.Recycler recycler, int i) {
if (i < 0) {
return;
}
View child = recycler.getViewForPosition(i);
addView(child, 0);//将View添加至RecyclerView中,childIndex为1,但是View的位置还是由layout的位置决定
measureChildWithMargins(child, 0, 0);
Rect rect = mItemRects.get(i);
layoutDecoratedWithMargins(child, rect.left, rect.top - verticalScrollOffset, rect.right, rect.bottom - verticalScrollOffset);
}
private void addViewBottom(RecyclerView.Recycler recycler, int i) {
if (i > getItemCount() - 1) {
return;
}
View view = recycler.getViewForPosition(i);
addView(view);
measureChildWithMargins(view, 0, 0);
int height = getDecoratedMeasuredHeight(view) + getBottomDecorationHeight(view);
System.out.println("addViewBottom=======" + i + "/verticalScrollOffset=" + verticalScrollOffset + "======" + offsetH + "====" + height + "====" + getHeight());
Rect rect = new Rect();
switch (i % 6) {
case 0:
rect.set(0, offsetH, 2 * smallWidth, offsetH + height);
layoutDecoratedWithMargins(view, 0, offsetH, 2 * smallWidth, offsetH + height);
break;
case 1:
rect.set(2 * smallWidth, offsetH,
3 * smallWidth, offsetH + height / 2);
layoutDecoratedWithMargins(view, 2 * smallWidth, offsetH,
3 * smallWidth, offsetH + height / 2);
break;
case 2:
rect.set(2 * smallWidth, offsetH + height / 2,
3 * smallWidth, offsetH + height);
layoutDecoratedWithMargins(view, 2 * smallWidth, offsetH + height / 2,
3 * smallWidth, offsetH + height);
this.offsetH = this.offsetH + height;
break;
case 3:
rect.set(0, offsetH,
smallWidth, offsetH + height / 2);
layoutDecoratedWithMargins(view, 0, offsetH,
smallWidth, offsetH + height / 2);
break;
case 4:
rect.set(0, offsetH + height / 2,
smallWidth, offsetH + height);
layoutDecoratedWithMargins(view, 0, offsetH + height / 2,
smallWidth, offsetH + height);
break;
case 5:
rect.set(smallWidth, offsetH,
3 * smallWidth, offsetH + height);
layoutDecoratedWithMargins(view, smallWidth, offsetH,
3 * smallWidth, offsetH + height);
this.offsetH = this.offsetH + height;
break;
}
rect.offset(0, verticalScrollOffset);
mItemRects.put(i, rect);
}
@Override
public boolean canScrollVertically() {
return true;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
//手指往上滑dy为正,手指往下滑dy为负
layoutItem(recycler, dy);//先添加布局,再位移
if (dy<0) {
View firstChild = getChildAt(0);
if (getPosition(firstChild) == 0 && getDecoratedTop(firstChild) - dy > 0) {
//第一个child的position是0,并且top即将大于0,那就不能移动
dy=getDecoratedTop(firstChild);
}
}else { //判断最后一个child的底部是否即将跑到屏幕底部的上边
int bottom=getDecoratedBottom(getChildAt(getChildCount()-1));
if(bottom- dy < getHeight() - getPaddingBottom()){
dy=bottom-getHeight()+getPaddingBottom();
}
}
//将竖直方向的偏移量+travel
verticalScrollOffset += dy;
// 调用该方法通知view在y方向上移动指定距离
offsetChildrenVertical(-dy);
System.out.println("scrollvertical===========" + dy + "=====" + verticalScrollOffset+"==offsetH===="+offsetH);
return dy;
}
}
自定义LayoutManager的步骤
写了几个简单的,现在来简单记录下流程
这里为写一个简单的线性的垂直布局为例
1.继承,实现抽象方法
class CustomLayoutManager:RecyclerView.LayoutManager(){
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams(-2,-2)
}
- 重写onLayoutChildren方法
基本流程就是先判断是否有item,没有就return
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
super.onLayoutChildren(recycler, state)
if (state.itemCount == 0) {
removeAndRecycleAllViews(recycler);//这个view是被回收了。
return;
}
if (getChildCount() == 0 && state.isPreLayout()) {
return;
}
detachAndScrapAttachedViews(recycler);//这个是把view从页面拿走,单并没有回收view
reset()
layoutView(recycler,state,0)//这个就是用来布局的
}
- addView到布局里
这里分两种,一种往后边加view,一种往前边加view
像平时见到的那种卡片一层一层的,也是这样写的,主要就是layout那些child的位置
//top 根据index不同代表不同的属性。index=-1,这里表示的是最后一个child的bottom位置,如果是0,代表的是第一个child的top位置
//position 新添加的view的adapter的position
//index =0表示添加到上边,index=-1表示添加到下边,也就是末尾
private fun addView(recycler: RecyclerView.Recycler, nearY:Int,position:Int,index:Int){
val child=recycler.getViewForPosition(position)//获取item,系统内部处理的,有可以复用的就复用,没有就新create一个。
addView(child,index)//添加到布局里
measureChildWithMargins(child,0,0)//对child进行测量
val heightChild=getDecoratedMeasuredHeight(child)//获取child的高度,包含itemDecoration的高度
if(index==-1){
layoutDecoratedWithMargins(child,left,nearY,right,nearY+heightChild)//对child的位置进行layout
}else{
layoutDecoratedWithMargins(child,left,nearY-heightChild,right,nearY)
}
}
4.啥时候addview?
//第一种情况,初始化的时候,child count==0,这时候肯定要add拉
if(childCount==0){
var top=0
for(it in 0 ..maxPostion){
val child=recycler.getViewForPosition(it)
addView(child)
measureChildWithMargins(child,0,0)
val heightChild=getDecoratedMeasuredHeight(child)
layoutDecoratedWithMargins(child,left,top,right,top+heightChild)
top+=heightChild
if(top>height-paddingBottom){//跑到屏幕外边去了,就不继续添加了。
maxPostion=it
break;
}
}
return
}
//第二种,滑动的时候添加新的,这时候childCount就不是0了
也分两种,手指上滑,底部可能需要添加view,手指下滑,顶部可能添加view
同时,移除view,也分两种,上滑,顶部可能需要移除view,下滑,底部可能需要移除view
判断条件也很简单,顶部移除的话,判断item的bottom是否在控件外边,也就是小于paddingTop
顶部添加,看item的top是否大于paddingTop。
底部道理一样,移除的话,判断item的top是否大于控件height-paddingBottom
添加的话,判断item的bottom是否小于height-paddingBottom
下边的dy就是手指移动的距离,dy大于0是手指往上滑,dy小于0是手指往下滑,
手指上滑的处理
if(childCount>0){
//remove the top views which are invisible
var child=getChildAt(0)
while(getDecoratedBottom(child)-dy
手指下滑的处理
if(childCount>0){
//remove views at the bottom that will be invisible
var child=getChildAt(childCount-1)
while (getDecoratedTop(child)-dy>height-paddingBottom){
removeAndRecycleView(child,recycler)
maxPostion=getPosition(child)-1
if(childCount==0){
break;
}
child=getChildAt(childCount-1)
}
//add views at the top
val childTop=getChildAt(0)
val top=getDecoratedTop(childTop)
if(top-dy>paddingTop&&minPosition>0){
minPosition--
addTopView(recycler,top)
println("add top view=========${minPosition}")
}
}
- 处理滑动事件
这里的例子是垂直方向滑动,所以下边的方法返回true即可,然后处理对应方向的滑动事件
override fun canScrollVertically(): Boolean {
return true
}
override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
if(itemCount==0||childCount==0){
return dy
}
layoutView(recycler,state,dy)//先布局,再处理view的位移
var move=dy
val child0=getChildAt(0)//最顶部的view
val childTop=getDecoratedTop(child0)
val childLast=getChildAt(childCount-1)//最底部的view
val bottomChild=getDecoratedBottom(childLast)
if(childTop-move>0){//防止第一个item往下移动,上边成了空白
move=childTop
}else if(bottomChild-move
另一种处理滑动事件的方法
我们需要写个callback来处理每个item的触摸事件,卡片式的用这种也很方便,如下图
首先自定义一个layoutmanager,比较简单了,layout child如下的位置即可,都是居中的,然后依次缩小,平移一定距离就可以了
完事我们添加item的触摸事件,用下边的helper
public ItemTouchHelper(Callback callback)
//使用
ItemTouchHelper(SwipCardCallBack(0,ItemTouchHelper.UP or ItemTouchHelper.DOWN
or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT,dps)).attachToRecyclerView(this)
简单的callback如下
class SwipCardCallBack:ItemTouchHelper.SimpleCallback{
var datas= arrayListOf()
constructor(dragDirs: Int, swipeDirs: Int,data:ArrayList) : super(dragDirs, swipeDirs){
datas=data
}
override fun onMove(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?, target: RecyclerView.ViewHolder?): Boolean {
return false
}
var rv:RecyclerView?=null;
//拖动结束以后会走这里
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val data=datas.removeAt(0)
datas.add(data)//这里数据不删除,就是把滑动的数据再放到最后。有具体需求再改
rv?.adapter?.notifyDataSetChanged()
println("========onSwiped===$direction====${datas[0]}")
}
//拖动的时候不停的刷新child的大小位置等。
override fun onChildDraw(c: Canvas?, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
rv=recyclerView;
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
val z=Math.hypot(dX.toDouble(),dY.toDouble()).toFloat()
var factor=z/(recyclerView.width/2f);//移动的距离和宽度的一半 做比较
if(factor>1){
factor= 1f
}
for(i in 0 until recyclerView.childCount-1){
val child=recyclerView.getChildAt(i)
val realPosition=recyclerView.getChildAdapterPosition(child)
child.translationY=child.width/15*(realPosition-factor)
child.scaleY=1f-(realPosition-factor)*0.05f
child.scaleX=1f-(realPosition-factor)*0.05f
}
}
}
需要注意的问题
添加view的使用有addView(child,0)或者addView(child)
写的时候注意下,你的child到底应该添加到哪里,因为后边你可能用到getChildAt(index)
我写另外一个自定义的LayoutManager的时候,不是用的这篇文章的方法
整体流程是【主要处理控件的复用】
1.找到第一个child的索引first
2.完事根据总体移动的距离moveY,以及即将移动的距离dy,
开始计算index从0到first之间的child,是否在屏幕上,不在的话啥也不干,把控件占的距离加上即可,在的话,把这个child add进来
3.修正已经在屏幕上的child,计算位置是否在屏幕,不在移除,在的话重新修正位置
4.判断dy大于0的情况,也就是手指往上滑动,底部可能需要添加新的item。
如下这种,是根据给定的path来布局的,这个path比较简单,如箭头所示