转载请注明出处:
http://blog.csdn.net/user11223344abc/article/details/78080671
出自【蛟-blog】
0.前言
本文分为俩个Step来研究如何自定义一个合格的LinearlayoutMnager。
- Step 1:视觉上定义一个具备上下边界的RecyclerView.layoutMnager
这里边又分为几个小步,后面会细说。 - Step 2:item回收,以及性能的验证
当然我们不能满足于视觉上,条目的离屏回收和复用是一个合格Rv的基本标准。
内容涉及到部分原理,更多是代码层面的讲解,就是说,代码为什么这样写
Ps:第1主要是描述的一些基础,在1.3内有关于回收机制的叙述,若有基础的同学不想看预备知识点,而只想看实现细节,则可以直接跳到第2,3步,看实现细节的分析。
1.准备知识
1.0.自定义的第一步:extends RecyclerView.LayoutManager
看看系统给我们提供的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);
}
}
1.1.关于generateDefaultLayoutParams()
这个方法就是RecyclerView Item的布局参数,换种说法,就是RecyclerView 子 item 的 LayoutParameters,若是想修改子Item的布局参数(比如:宽/高/margin/padding等等),那么可以在该方法内进行设置。
一般来说,没什么特殊需求的话,则可以直接让子item自己决定自己的宽高即可(wrap_content)。
public abstract LayoutParams generateDefaultLayoutParams();
1.2.关于onLayoutChildren()
你可以看到这里多了个方法onLayoutChildren,这个方法就类似于自定义ViewGroup的onLayout方法,这也是自定义LayoutOutManager的主要入口(重要)。后面会详细的描述如何定义该方法。
public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
1.3.关于回收和缓存(重要):
上面说了实际上自定义layoutManager的过程也就是自定义onLayoutChildren()的过程,其中分为多个步骤,其中一个重要的步骤就是处理回收这个步骤。需要一定的理论知识,即在一定程度上的去理解recyclerView的缓存机制。
1.3.1.相关概念:
先来一张图:(摘自RV缓存机制详解-腾讯Bugly的专栏)
这张图讲的是Rv和Lv的缓存机制对比,作者视图结合用Lv的2级缓存来让我去理解Rv的4级缓存机制。
关于这里出现的几个RecyclerView相关概念:
- scrap:
里面缓存的View是接下来需要用到的,即里面的绑定的数据无需更改,可以直接拿来用的,是一个轻量级的缓存集合; - Recycle:
Recycle的缓存的View为里面的数据需要重新绑定,即需要通过Adapter重新绑定数据(onBindViewHolder/onCreateViewHolder)。
1.3.2.取缓存的流程:
当我们去获取一个新的View时,RecyclerView首先去检查Scrap缓存是否有对应的position的View,如果有,则直接拿出来可以直接用,不用去重新绑定数据;如果没有,则从Recycle缓存中取,并且会回调Adapter的onBindViewHolder方法(如果Recycle缓存为空,还会调用onCreateViewHolder方法),最后再将绑定好新数据的View返回。
1.3.3.缓存的手段:
- scrap >> detach:
detachAndScrapView()
场景:当我们对View进行重新排序的时候,可以选择Detach,因为屏幕上显示的就是这些position对应的View,我们并不需要重新去绑定数据,这明显可以提高效率。 - Recycle >> remove
removeAndRecycleView()
场景:是当View不在屏幕中有任何显示的时候,你需要将它Remove掉,以备后面循环利用。
1.4 滑动处理:
滑动主要涉及4个方法:
- canScrollVertically //是否能垂直滑动
- scrollVerticallyBy //处理垂直滑动
- canScrollHorizontally //是否能水平华东
- scrollHorizontallyBy //处理水平滑动
由本例是模拟一个LinearlayoutManager,所以我们就关心vertical俩个方法就好了。看上面的注释,2个can方法都好理解,就是返回一个boolean值来告诉手机当前列表可否横竖滑动,true代表可以滑动,false反之,另外,相对于滑动而言,咱们主要来分析2个scrollBy方法,其中的难点也在这里,后面写代码的时候再细说。
2:实践
终于到了本文的主菜,开局说了分为俩大步,那么到了细节,我们再来拆分这俩大步所包含的细节。
-
Step 1:视觉上定义一个具备上下边界的RecyclerView.layoutMnager
将各个item.addView 【addView】
测量每个item 【measure】
放置各个item 【layout】
处理滚动 【scoll】 -
Step 2:item回收,以及性能的验证
条目的回收 【recycler/scrap】
2.1:视觉上定义一个具备上下边界的RecyclerView.layoutMnager(模拟一个LinearLayout LayoutManager):
4步:
- 将各个item.addView 【addView】
- 测量每个item 【measure】
- 放置各个item 【layout】
- 处理滚动 【scoll】
2.1.1: 将各个item.addView【addView】:
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);
}
......
}
2.1.2: 测量每个item【measure】:
核心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,现在还不能滚动。
2.1.3. 处理滚动【scoll】:
预备知识内已经讲解了滑动相关的回调方法,这里主要讲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个问题。
2.1.3.1.问题1:方向是反的:
scrollVerticallyBy()方法:
这个方法的回调参数内有个dy。他代表手指在屏幕上每次滑动的位移。
从日志观察:
- 只有在列表可滚动的时候,该值才具有意义。(canScrollVertically 返回true)。
- 手指由下往上滑时,dy值为 >0 的。
- 手指由上往下滑时,dy值为 <0 的。
- 当手指滑动的幅度,速率越大,dy的绝对值越大。
- offsetChildrenVertical(dy),这个方法传入的dy需要乘以-1,才能让列表滑动符合我们的的生活习惯,否则列表是反向滑动的。
我的理解方式是:看源码注释
@param dy
distance to scroll in pixels. Y increases as scroll position approaches the bottom.
滑动的距离(像素为单位),Y随着滚动位置靠近底部而增加。
也就是说,我可以理解为,手指滑动方向往下,Dy会变大(正),手指方向往上,Dy会变小(负)。
给张示意图:
2.1.3.2.问题2:头,底,边界设置,以及对滑动位置的修正:
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();
}
理解theRvVisibleHeight请看这张图:
—>theMoreHeight
这个值就是Rv所隐藏的高度,就是这个列表总高度减去可见高度
理解theMoreHeight请看这张图:
总高度 - 可见高度 == 被隐藏的多余部分(也就是蓝色那部分)
2.1.3.3.问题3:滑动惯性问题:
惯性的计算(flings),是由该方法的返回值决定的,当返回值和dy不一致时则会失去惯性效果,并且边界会产生发亮的效果。也就是说,正确的对dy修正并让其返回是fling惯性正常的一个重要前提条件。
2.1.3.4.问题4:边界修正问题:
这是一个衍生的问题,就是说我们光判断了边界,但不对返回值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