转载请注明出处:
http://blog.csdn.net/user11223344abc/article/details/78080671
出自【蛟-blog】
本文分为俩个Step来研究如何自定义一个合格的LinearlayoutMnager。
内容涉及到部分原理,更多是代码层面的讲解,就是说,代码为什么这样写
Ps:第1主要是描述的一些基础,在1.3内有关于回收机制的叙述,若有基础的同学不想看预备知识点,而只想看实现细节,则可以直接跳到第2,3步,看实现细节的分析。
看看系统给我们提供的3个LayoutManager:
LinearLayoutManager
public class LinearLayoutManager extends RecyclerView.LayoutManager implements
ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
.......
}
StaggeredGridLayoutManager
public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager implements
RecyclerView.SmoothScroller.ScrollVectorProvider {
.......
}
GridLayoutManager,这个是LinearLayoutManager子类,本质上还是extends RecyclerView.LayoutManager。
public class GridLayoutManager extends LinearLayoutManager {
.......
}
所以,我们写出了如下代码:
public class CustomerLayoutManger extends RecyclerView.LayoutManager{
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT);
}
/**
*
* @param recycler
* @param state
*/
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
}
}
这个方法就是RecyclerView Item的布局参数,换种说法,就是RecyclerView 子 item 的 LayoutParameters,若是想修改子Item的布局参数(比如:宽/高/margin/padding等等),那么可以在该方法内进行设置。
一般来说,没什么特殊需求的话,则可以直接让子item自己决定自己的宽高即可(wrap_content)。
public abstract LayoutParams generateDefaultLayoutParams();
你可以看到这里多了个方法onLayoutChildren,这个方法就类似于自定义ViewGroup的onLayout方法,这也是自定义LayoutOutManager的主要入口(重要)。后面会详细的描述如何定义该方法。
public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
上面说了实际上自定义layoutManager的过程也就是自定义onLayoutChildren()的过程,其中分为多个步骤,其中一个重要的步骤就是处理回收这个步骤。需要一定的理论知识,即在一定程度上的去理解recyclerView的缓存机制。
先来一张图:(摘自RV缓存机制详解-腾讯Bugly的专栏)
这张图讲的是Rv和Lv的缓存机制对比,作者视图结合用Lv的2级缓存来让我去理解Rv的4级缓存机制。
关于这里出现的几个RecyclerView相关概念:
当我们去获取一个新的View时,RecyclerView首先去检查Scrap缓存是否有对应的position的View,如果有,则直接拿出来可以直接用,不用去重新绑定数据;如果没有,则从Recycle缓存中取,并且会回调Adapter的onBindViewHolder方法(如果Recycle缓存为空,还会调用onCreateViewHolder方法),最后再将绑定好新数据的View返回。
滑动主要涉及4个方法:
由本例是模拟一个LinearlayoutManager,所以我们就关心vertical俩个方法就好了。看上面的注释,2个can方法都好理解,就是返回一个boolean值来告诉手机当前列表可否横竖滑动,true代表可以滑动,false反之,另外,相对于滑动而言,咱们主要来分析2个scrollBy方法,其中的难点也在这里,后面写代码的时候再细说。
终于到了本文的主菜,开局说了分为俩大步,那么到了细节,我们再来拆分这俩大步所包含的细节。
Step 1:视觉上定义一个具备上下边界的RecyclerView.layoutMnager
将各个item.addView 【addView】
测量每个item 【measure】
放置各个item 【layout】
处理滚动 【scoll】
Step 2:item回收,以及性能的验证
条目的回收 【recycler/scrap】
4步:
addView(itemView);
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
......添加view
for (int i = 0; i < getItemCount(); i++) {
View scrap = recycler.getViewForPosition(i);
addView(itemView);
}
......
}
核心api:
layoutDecorated(itemView, 0, 0);
......放置
View scrap = recycler.getViewForPosition(i);
int width = getDecoratedMeasuredWidth(scrap);
int height = getDecoratedMeasuredHeight(scrap);
layoutDecorated(scrap, offsetX, offsetY, offsetX + width, offsetY + height);
offsetY += height;
......
到这一步理论上来说,屏幕上应该能看见一个vertical的列表了
在此汇总一下之前的代码:
/**
* @param recycler
* @param state
*/
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
int offsetY = 0;
for (int i = 0; i < getItemCount(); i++) {
View scrap = recycler.getViewForPosition(i);
addView(scrap);
measureChildWithMargins(scrap, 0, 0);
int perItemWidth = getDecoratedMeasuredWidth(scrap);
int perItemHeight = getDecoratedMeasuredHeight(scrap);
layoutDecorated(scrap, 0, offsetY, perItemWidth, offsetY + perItemHeight);
offsetY += perItemHeight;
}
mTotalHeight = offsetY;
}
but,现在还不能滚动。
预备知识内已经讲解了滑动相关的回调方法,这里主要讲api和实现。
首先,需要明确,滑动的核心API
也就是说,要滑动,这api是必调的(一个方向对应一个方法)。
/**
* Offset all child views attached to the parent RecyclerView by dy pixels along
* the vertical axis.
*
* @param dy Pixels to offset by
*/
public void offsetChildrenVertical(int dy) {
if (mRecyclerView != null) {
mRecyclerView.offsetChildrenVertical(dy);
}
}
/**
* Offset all child views attached to the parent RecyclerView by dx pixels along
* the horizontal axis.
*
* @param dx Pixels to offset by
*/
public void offsetChildrenHorizontal(int dx) {
if (mRecyclerView != null) {
mRecyclerView.offsetChildrenHorizontal(dx);
}
}
根据上面的分析,我们现在来加上这俩个方法的代码:
@Override
public boolean canScrollVertically() {
return true;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
offsetChildrenVertical(dy);
return super.scrollVerticallyBy(dy,recycler,state);
}
呵呵,必须不正常,毕竟只写了一行代码,目前存在4个问题:
总结下:
问题1:方向是反的。
问题2:头,底,边界设置。
问题3:滑动惯性。
问题4:关于dy的修正。
那么接下来解决这4个问题。
scrollVerticallyBy()方法:
这个方法的回调参数内有个dy。他代表手指在屏幕上每次滑动的位移。
从日志观察:
我的理解方式是:看源码注释
@param dy
distance to scroll in pixels. Y increases as scroll position approaches the bottom.
滑动的距离(像素为单位),Y随着滚动位置靠近底部而增加。
也就是说,我可以理解为,手指滑动方向往下,Dy会变大(正),手指方向往上,Dy会变小(负)。
2个问题:
换种方式来思考:
一个点,做垂直移动,每次移动的起点是上一次的终点,并且会给出每一次移动的距离值,当一次移动的终点在起点之上时,这个距离值的符号为正,当终点在本次移动的起点之下时,移动距离的符号为负,问,如何判断该点抵达了上边界或下边界。
我先给出代码,然后再分析这个代码为何这样写:
@Override
public boolean canScrollVertically() {
return true;
}
//手指 从上往下move是 下拉 dy是负
//手指 从下往上move是 上拉 dy是正
int mTheMoveDistance = 0;
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
int theRvVisibleHeight = getVerticalVisibleHeight();
int theMoreHeight = mTotalHeight - theRvVisibleHeight;
Log.e("zj", "mRealMoveDistance == " + mTheMoveDistance);
if (mTheMoveDistance + dy < 0) { //抵达上边界
...
} else if (mTotalHeight > theRvVisibleHeight && mTheMoveDistance + dy > theMoreHeight) {//抵达下边界
...
} else {
}
....
mTheMoveDistance += dy;
return dy;
}
public int getVerticalVisibleHeight() {
return getHeight() - getPaddingTop() - getPaddingBottom();
}
上边界问题:
if (mTheMoveDistance + dy < 0) { //抵达上边界
这个没什么可讲的,可以试着想象一下mTheMoveDistance初始化为0,而dy每次移动都是一个从0开始偏移的变量,这么计算则是将每次偏移的距离进行一个记录,这样有助于后续用这个距离来进行边界的判断。
mTheMoveDistance为滑动的距离,这里之所以要先+dy是因为它是一个预判的动作,就是说在滑动距离增加之前,我先判断它究竟是正常的+=计算增加,还是需要修正之后再进行赋值。若不进行预判,则有可能出现列表上拉到边界时出现列表闪烁的问题,但闪烁之后会回复正常,有兴趣的同学可以自己进行试验,这里我就不贴图上来了。
下边界问题:
else if (mTotalHeight > theRvVisibleHeight && mTheMoveDistance + dy > theMoreHeight) {//抵达下边界
mTheMoveDistance + dy 若大于隐藏的部分的高度,则视为抵达底部边界。
这里所牵涉到的变量请参考下面的图进行理解。
—>mTotalHeight
mTotalHeight是在layout的时候就进行了一个计算了,它是一个全局变量
—>theRvVisibleHeight
这个是获取Rv在屏幕内显示的可见高度
它的赋值方法是这个:
int theRvVisibleHeight = getVerticalVisibleHeight();
public int getVerticalVisibleHeight() {
return getHeight() - getPaddingTop() - getPaddingBottom();
}
—>theMoreHeight
这个值就是Rv所隐藏的高度,就是这个列表总高度减去可见高度
理解theMoreHeight请看这张图:
总高度 - 可见高度 == 被隐藏的多余部分(也就是蓝色那部分)
惯性的计算(flings),是由该方法的返回值决定的,当返回值和dy不一致时则会失去惯性效果,并且边界会产生发亮的效果。也就是说,正确的对dy修正并让其返回是fling惯性正常的一个重要前提条件。
这是一个衍生的问题,就是说我们光判断了边界,但不对返回值dy进行修正的话,就会导致moveDistance计算失误,计算失误产生的直接后果就是判断条件错误,因为移动距离moveDistance是作为我们的一个判断条件而存在的。那么我们该如何修正边界呢?
关于边界修复的思路就是,在特定的边界,对moveDistance计算出特定的值,而又因为这个边界的赋值是动态的 moveDistance+=dy ,且因返回值为dy的因素(返回值的影响惯性的效果在上面已经说过了),所以,我们真正需要修正的,实际是dy。
上边界修正:
dy = -mTheMoveDistance;
上边界时,我们认定滑动距离为0。
则,moveDistance+=dy 需要等于 0
得出:dy = -mTheMoveDistance
下边界修正:
dy = theMoreHeight - mTheMoveDistance;
当滑动距离超过底部距离时,将滑动距离修正为底部距离。
因为:底部距离为:mTheMoveDistance = mTheMoveDistance + dy
且,需要修正成为的距离为:mTheMoveDistance = theMoreHeight;
得出表达式:mTheMoveDistance + dy = theMoreHeight;
转换后的结果则是:dy = theMoreHeight - mTheMoveDistance;
配上一张示意图:不明白的同学把图上的值带入情景计算一下便明白了。
上图,蓝色部分是屏幕隐藏的部分(也就是说当蓝色部分全部显示时,表明已经抵达列表底部边界),绿色部分是可见部分,橙黄色部分是表示多滑动的距离,也就是需要被修正的部分。
至此,视觉上看着已经没问题了,其实有效代码也就50行左右。
但我们的条目还没有进行缓存和回收。接下来进行缓存的回收及利用。
目前阶段的代码:
http://download.csdn.net/download/user11223344abc/9993385
条目回收和复用准备放到下一篇博客内进行讲解。
传送门:https://blog.csdn.net/user11223344abc/article/details/79168157