家资是何物,积帙列梁梠。这篇文章主要讲述Android深入掌握自定义LayoutManager 系列开篇 常见误区问题注意事项,常用API。相关的知识,希望能为你提供帮助。
转载请标明出处:本系列文章相关代码传送门:
http://blog.csdn.net/zxt0601/article/details/52948009
本文出自:【张旭童的博客】
自定义LayoutManager实现的流式布局
欢迎star, pr, issue。
本系列文章目录:
掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项, 常用API。
掌握自定义LayoutManager(二) 实现流式布局
概述 这篇文章是深入掌握自定义LayoutManager系列的开篇, 是一份总结报告。部分内容不属于引言、过于深入, 用作系列后续文章的参考, 以及浏览完后的复习之用。
本文内容涉及RecyclerView、LayoutManager、RecyclerViewPool、Recycler。
注:
1 以下问题, 初学者如有不理解的, 可以不用太在意, 等学习完自定义LayoutManager相关知识, 写几个Demo再回来看更好理解。
2 在RecyclerView中, ItemView和ViewHolder其实是一一绑定的, 所以提到的View = ViewHolder。
一 常见误区、问题、注意事项: 在自定义LayoutManager文章开始之前, 我总结了一些我在学习以及阅读别人的文章、编码的过程中, 遇到的一些疑惑问题, 并附上我个人的理解与答案。欢迎拍砖讨论。
因网上有大量半吊子写的LayoutManager相关的中文文章。( 包括我也是半吊子) , 所以很多文章看完了, 心中都有N个疑问, 如, 作者好牛逼啊, 但是为什么我独立写还是写不出来。 自定义一个LayoutManager就自动复用了吗? …等等, 下面逐个来讲讲。
Q1 看完了, 但是我独立写还是不知道怎么写。 A1: 自定义LayoutManager是一项颇有难度的工程, 你很难仅仅阅读一两篇文章, 花两三个小时就能学习完。
里面涉及到子View的布局, 坐标的计算, 偏移量的计算, 在滑动时、在合适的时机回收屏幕上不再显示的View, 如何判断这些View是在屏幕上不可见, 以及View究竟是暂时detach掉, 还是recycle回收掉…等大量问题
。老实说, 也许我水平有限, 这是我在学习android过程中, 耗时最久的几个知识点之一。( 十几个小时才写出第一个及格的作品)
但是它值得你学习。所以独立写不出来别灰心, 先仿照一个Demo写一写, 如果用心理解, 第二遍第二遍应该就可以独立完成了。
Q2 学习自定义LayoutManager需要的铺垫知识 一 : 熟练掌握自定义ViewGroup。
( 在自定义LayoutManager过程的第一步,
onLayoutChildren()
方法里,
就类似于自定义ViewGroup的onLayout()方法。)
但与自定义LayoutManager相比, 自定义ViewGroup是一种静态的layout 子View的过程, 因为ViewGroup内部不支持滑动, 所以只需要无脑layout出所有的View, 便不用再操心剩下的事。
而自定义LayoutManager与之不同, 在第一步layout时, 千万不要layout出所有的子View, 这里也是网上一些文章里的错误做法, 他们带着老思想, 在第一步就layout出了所有的childView, 这会导致一个很严重的问题: 你的自定义LayoutManager = 自定义ViewGroup。即, 他们没有View复用机制。
why? 这里简单证明结论, 在Q5的回答里会说明为什么。
在Adapter的onCreateViewHolder()方法里增加打印语句, 如果你的数据源有100000条数据, 那么在RecyclerView第一次显示在屏幕上时, onCreateViewHolder()会执行100000次, 你就可以尽情的欣赏ANR了。
反观使用官方提供的三种LayoutManager, 开始时屏幕上有n少个ItemView, 一般就执行n次onCreateViewHolder(), ( 也有可能多执行1次) , 在后续滑动时, 大部分情况都只是执行onBindViewHolder()方法, 不会再执行onCreateViewHolder()。
二 : 熟练使用RecyclerView。这个不用多说, 毕竟RecyclerView是LayoutManager的宿主。
【Android深入掌握自定义LayoutManager 系列开篇 常见误区问题注意事项,常用API。】其实会以上两点就可以开始我们的学习之旅了, 不过如果能对RecyclerView的Adapter、RecyclerViewPool、ItemDecoration也有一定的了解那是最好。
Q3 自定义LayoutManager的实战场景多吗? A3: 实战场景还是相当有限的。系统自带的三个LayoutManager已经很够用, 满足绝大部分需求。
我个人从学习自定义LayoutManager至今的收获 , 大部分是对RecyclerView机制的理解进一步加深, 也会伴随一定量的源码阅读经验提升。随没有我想象中的提升巨大生产力的赶脚, 因为很多时候, 产品设计要求的布局, 现有方案已经可以很好解决。
但是它值得学习。
Q4 自定义一个LayoutManager就自动复用ItemView了吗? A4: 不是, 实际上这是自定义LayoutManager的重头戏之一, 要做到在合适的时机回收 不可见的旧子View , 复用子View layout 新的子View, 以及Q2提及的在LayoutManager的初始化时合理布局可见数量的子View等, 才算是复用了ItemView。
注意, 这里的回收是recycle, 而不是detach。
如果你只detach了ItemView, 并没有recycle它们, 它们会一直被保存在Recycler的
mAttachedScrap
里,
它是一个ArrayList,
保存了被detach但还没有recycle的ViewHolder。public final class Recycler {
final ArrayList<
ViewHolder>
mAttachedScrap =
new ArrayList<
>
();
( 实际上Recycler内部的缓存机制远不止一个mAttachedScrap 。)
Q5 用RecyclerView就等于ItemView复用? A5: 显然也不是。除了Q4的因素外, 这里还有一个很大的误区: 很多人认为使用了RecyclerView, ItemView就都回收复用了。
这里出个题: 基本上APP都有个TopBanner在, 它放在RecyclerView里作为HeaderView( 通过特殊的ItemViewType实现) , 剩下都是普通的ItemView, 那么列表滚动, 当Banner早已不可见时, 它的View(ViewHolder)会被回收、被其他ItemView复用吗?
如下图:
文章图片
答案: Banner的ViewHolder 会被回收, 但该ViewHolder的内存空间 不会被释放 , 不会被其他的ItemView复用。
回收都好理解, 在屏幕上不可见时, LayoutManager会把它回收至RecyclerViewPool里。
然而却不会给normalItem复用, 因为它们的ItemViewType不同。
所以它的内存空间不会被释放, 将一直被RecyclerViewPool持有着, 等待着需求相同ItemViewType的ViewHolder的请求到来。
即, 当页面滚动回顶部, 显示Banner时, 这个View会被复用。
先说为什么, 再说如何去验证。
为什么?
这涉及到Recycler、RecyclerViewPool的知识, ( 小安利, 我在http://blog.csdn.net/zxt0601/article/details/52267325 这篇文章的第四节里对RecyclerViewPool的源码进行过全解, 不过大家也可以自己去查看, 源码很短。)
在LayoutManager里, 获取childView是通过如下方法得到:
View child =
recycler.getViewForPosition(i);
该方法内部, 先通过position去获取是否有detach掉的scrapView( ViewHolder) ,
holder =
getScrapViewForPosition(position, INVALID_TYPE, dryRun);
如果没有则根据position去获取itemViewType,
final int type =
mAdapter.getItemViewType(offsetPosition);
根据itemViewType获取在RecyclerViewPool里是否有该ViewHolder,
holder =
getRecycledViewPool().getRecycledView(type);
这里由于我们的Banner的viewType和normalItem的viewType不一样, 即使Banner被回收进了RecyclerViewPool, 但是由于itemViewtype和普通的ItemView不同, 它也无法被取出、从而复用, ( 发散一下, 另外一点, 它也无法被释放, 被强引用在内存里, http://blog.csdn.net/zxt0601/article/details/52267325 这篇文章有详细分析) 。
再往下由于holder还是空的, 最终便会调用Adapter的onCreateViewHolder()方法create一个新的ViewHolder。
`
holder =
mAdapter.createViewHolder(RecyclerView.this, type);
`
验证:
感兴趣的人去重写任意Adapter的
getItemViewType()
方法:
@
Override
public int getItemViewType(int position) {
return position;
}
这样每一个ItemViewType都不一样, RecyclerView不会有任何的复用, 因为每一个ItemView在RecyclerViewPool里都找不到可以复用的holder, ItemView有n个, onCreateViewHolder方法会执行n次。
看到这里就能回答Q2一的问题:
因为在初始化时, Recycler(scrapCache)和RecyclerViewPool里的缓存都是空的, 所以此时得到的ViewHolder都是通过onCreateViewHolder(),new 出的ViewHolder。如果此时get了整个itemCount数量的View, 那么也会new出itemCount数量的ViewHolder, 此时这些ViewHolder都存在内存里, 和普通ViewGroup毫无分别, 也更容易OOM。
Q6 RecyclerView的缓存机制简述 A6: 上面BB了这么多, 涉及到Recycler、RecyclerViewPool以及scrap, detach, remove, recycle等概念。
文章图片
这张图摘自( http://kymjs.com/code/2016/07/10/01) , 源头应该是Google官方的视频里。
我理解图上的cache是被detach掉的ViewHolder存放的区域, 即scrapCache区域。
这个区域由
final ArrayList<
ViewHolder>
mAttachedScrap =
new ArrayList<
>
();
ArrayList<
ViewHolder>
mChangedScrap =
null;
final ArrayList<
ViewHolder>
mCachedViews =
new ArrayList<
ViewHolder>
();
这三个ArrayList组成。
而被remove掉的ViewHolder会按照ViewType分组被存放在RecyclerViewPool里, 默认最大缓存每组( ViewType) 5个。
private SparseArray<
ArrayList<
ViewHolder>
>
mScrap =
new SparseArray<
ArrayList<
ViewHolder>
>
();
Q7 detach 和recycle的时机。 一个View只是暂时被清除掉, 稍后立刻就要用到, 使用detach。它会被缓存进scrapCache的区域。
一个View 不再显示在屏幕上, 需要被清除掉, 并且下次再显示它的时机目前未知 , 使用remove。它会被以viewType分组, 缓存进RecyclerViewPool里。
注意: 一个View只被detach, 没有被recycle的话, 不会放进RecyclerViewPool里, 会一直存在recycler的scrap 中。网上有人的Demo就是如此, 因此View也没有被复用, 有多少ItemCount, 就会new出多少个ViewHolder。
Q8 初始化时, onLayoutChildren()为什么会执行两次? 答 : 参看RecyclerView源码, onLayoutChildren 会执行两次, 一次RecyclerView的onMeasure() 一次onLayout()。
李菊福: RecyclerView的onMeasure(),会调用
dispatchLayoutStep2()
方法,
该方法内部会调用 mLayout.onLayoutChildren(mRecycler, mState);
,这是第一次。如下:
@
Override
protected void onMeasure(int widthSpec, int heightSpec) {
......
dispatchLayoutStep2();
......
}
/**
* The second layout step where we do the actual layout of the views for the final state.
* This step might be run multiple times if necessary (e.g. measure).
*/
private void dispatchLayoutStep2() {
.....
mLayout.onLayoutChildren(mRecycler, mState);
.....
}
onLayout()方法会调用
dispatchLayout();
,该方法内部又调用了dispatchLayoutStep2();
,
这是第二次。Q9 基于上个问题, 我们要注意什么? 答: 即使是在写onLayoutChildren()方法时, 也要考虑将屏幕上的View( 如果有) , detach掉, 否则屏幕初始化时, 同一个position的ViewHolder, 也会onCreateViewHolder两次。因此childCount也会翻倍。
最后也是最重要的 LayoutManager API 支持强大且复杂的布局回收, 正因为它API强大, 所以我们需要实现大量的代码才能完成功能。不要过度封装、过度优化你的代码, 只要能完成你的需求即可。( 当然最基本的要求: ViewHolder复用 要满足)
原话如下:
文章图片
文章链接: http://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/
该文章是我见过学习自定义LayoutManager最好的资料。
二 常用API: 布局API:
//找recycler要一个childItemView,我们不管它是从scrap里取,
还是从RecyclerViewPool里取,
亦或是onCreateViewHolder里拿。
View view =
recycler.getViewForPosition(xxx);
//获取postion为xxx的View
addView(view);
//将View添加至RecyclerView中,
addView(child, 0);
//将View添加至RecyclerView中,
childIndex为0,
但是View的位置还是由layout的位置决定,
该方法在逆序layout子View时有大用
measureChildWithMargins(scrap, 0, 0);
//测量View,这个方法会考虑到View的ItemDecoration以及Margin
//将ViewLayout出来,
显示在屏幕上,
内部会自动追加上该View的ItemDecoration和Margin。此时我们的View已经可见了
layoutDecoratedWithMargins(view, leftOffset, topOffset,
leftOffset +
getDecoratedMeasuredWidth(view),
topOffset +
getDecoratedMeasuredHeight(view));
回收API:
detachAndScrapAttachedViews(recycler);
//detach轻量回收所有View
detachAndScrapView(view, recycler);
//detach轻量回收指定View// recycle真的回收一个View ,
该View再次回来需要执行onBindViewHolder方法
removeAndRecycleView(View child, Recycler recycler)
removeAndRecycleAllViews(Recycler recycler);
detachView(view);
//超级轻量回收一个View,马上就要添加回来
attachView(view);
//将上个方法detach的View attach回来
recycler.recycleView(viewCache.valueAt(i));
//detachView 后 没有attachView的话 就要真的回收掉他们
移动子ViewAPI:
offsetChildrenVertical(-dy);
// 竖直平移容器内的item
offsetChildrenHorizontal(-dx);
//水平平移容器内的item
工具API:
public int getPosition(View view)//获取某个view 的 layoutPosition,
很有用的方法,
却鲜(没)有文章提及,
是我翻看源码找到的。
//以下方法会我们考虑ItemDecoration的存在,
但部分函数没有考虑margin的存在
getDecoratedLeft(view)=
view.getLeft()
getDecoratedTop(view)=
view.getTop()
getDecoratedRight(view)=
view.getRight()
getDecoratedBottom(view)=
view.getBottom()
getDecoratedMeasuredHeight(view)=
view.getMeasuredWidth()
getDecoratedMeasuredHeight(view)=
view.getMeasuredHeight()
//由于上述方法没有考虑margin的存在,
所以我参考LinearLayoutManager的源码:
/**
* 获取某个childView在水平方向所占的空间
*
* @
param view
* @
return
*/
public int getDecoratedMeasurementHorizontal(View view) {
final RecyclerView.LayoutParams params =
(RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredWidth(view) +
params.leftMargin
+
params.rightMargin;
}/**
* 获取某个childView在竖直方向所占的空间
*
* @
param view
* @
return
*/
public int getDecoratedMeasurementVertical(View view) {
final RecyclerView.LayoutParams params =
(RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredHeight(view) +
params.topMargin
+
params.bottomMargin;
}
推荐阅读
- Android性能优化之线程池策略和对线程池的了解
- Android屏幕适配
- Android 动画总结
- Eclipse Android开发环境的搭建步骤及注意事项
- Android自定义View实战之仿QQ运动步数圆弧及动画,Dylan计步中的控件StepArcView
- Android service进程保护
- Android自动化测试--Espresso框架使用
- android activity中监听View测量完成的4种方式
- 理解boot.img与逆向分析Android/linux内核