前言
RecyclerView 是我们用的最多的控件,也是一个较为复杂的控件,本文的目的在于疏通RecyclerView的基本用法以及原理,不对源码以及内部组件进行过于深入的讨论(当然,有兴趣的话也会说一说),结合一些自己在开发中的经验,谈谈如何设计一个流畅的列表。
带着问题来思考
1.RecyclerView.Adapter的几个函数的作用(getItemCount,)
2.Recycler的复用机制
3.如何编写多Item,如何添加footer与header
4.RecyclerView滑动卡顿的优化,RecyclerView资源是如何释放的,哪些情况容易出现OOM,有没有可能出现内存泄漏
5.数据的刷新机制,如何做到最优化刷新
6.LayoutManager
7.如何实现瀑布流
8.复杂动画的实现,以及滑动冲突的解决
ViewHolder
首先,我们需要了解一下什么是ViewHolder;ViewHolder最初是出现在ListView的BaseAdapter中,作为一种优化手段,我们来看一下普通的ListView的getView方法就知道了
1 |
|
2 | public View getView(int position, View convertView, ViewGroup parent) { |
3 | View v = null; |
4 | v = li.inflate(R.layout.list_text, parent, false); |
5 | |
6 | String text = (String)getItem(position); |
7 | |
8 | TextView tv = (TextView) convertView.findViewById(R.id.tv); |
9 | tv.setText(text); |
10 | |
11 | Log.i("-getView-", String.valueOf(counter)); |
12 | return v; |
13 | } |
getView的作用有以下两点:
1.引入布局来构造一个新的ItemView
2.为该ItemView绑定数据(前提是实例化ItemView的内部控件)
可能你就会问,convertView为什么没有用?它是什么?对于ListView而言,它的内部维护了一个ItemView的缓存队列,划出屏幕的View不会立即被回收,而是会进入一个缓存队列,当这个队列满的时候,最早进入的View才会被回收。这里的convertView实际上就是缓存队列中的View。
我们注意到,getView这样一个过程实际上可以被优化,首先,我们不用每次都inflate新的布局出来,而是判断有没有缓存,也就是判断convertView==null?,如果converView!=null,则可以直接view = convertView。另一个可以优化的点,则是控件的实例化,findViewById也是需要不小的开销的,我们可不可以在第一次实例化控件的时候,就把它绑定到ItemView上面,这样,下次接收到convertView的时候,就可以直接获取到相关的实例。以上就是关于ListView的一些优化
反映到代码中:
1 |
|
2 | public View getView(int position, View convertView, ViewGroup parent) { |
3 | if(convertView == null){ |
4 | converView = li.inflate(R.layout.list_text, parent, false); |
5 | } |
6 | |
7 | String text = (String)getItem(position); |
8 | |
9 | TextView tv = (TextView) convertView.findViewById(R.id.tv); |
10 | ViewHolder vh = new ViewHolder(tv); |
11 | tv.setText(text); |
12 | |
13 | convertView.setTag(vh); |
14 | |
15 | return convertView; |
16 | } |
17 | |
18 | class ViewHolder{ |
19 | TextView tv; |
20 | public ViewHolder(TextView tv){ |
21 | this.tv = tv; |
22 | } |
23 | } |
关于多ItemType的处理:
多ItemType是如何实现的,事实上,ListView他不会关心你几个type,它只需要Adapter在恰当的位置提供一个恰当的ItemView给他就 OK 了,这一切都是Adapter来操心的。那么Adapter是怎么处理多ItemType的呢,关键的地方在于 convertView 这个参数,当你调用 getView 的时候,你可以通过 data.get(position) 来找到对应的数据,那么 View 呢,我们怎么知道传递进来的 convertView 是不是我需要的 ViewType, 这点设计者也想到了,事实上,Adapter 内部缓存了好几条View队列,数量等于 TypeCount,在调用 getView 之前,会预先调用 getItemViewType 这个函数,来得到你需要的视图类型,再将其传递给 getView 函数,如果 convertView 为 null,并不表示缓存队列为空,而仅仅代表该 Type 的 缓存队列为空。这时候我们inflate一个新的View就可以了。
RecyclerView.Adapter
RecyclerView.Adapter里面有很多的函数,我选取几个常用的来说明一下它们各自的作用
1 | public abstract static class Adapter<VH extends RecyclerView.ViewHolder> { |
2 | private final RecyclerView.AdapterDataObservable mObservable = |
3 | new RecyclerView.AdapterDataObservable(); |
4 | private boolean mHasStableIds = false; |
5 | |
6 | public Adapter() { |
7 | } |
8 | |
9 | |
10 | public abstract VH onCreateViewHolder(@NonNull ViewGroup var1, int var2); |
11 | |
12 | public void onBindViewHolder(@NonNull VH holder, int position, @NonNull |
13 | List<Object> payloads) { |
14 | } |
15 | |
16 | |
17 | public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) { |
18 | ..... |
19 | } |
20 | |
21 | public int getItemViewType(int position) { |
22 | return 0; |
23 | } |
24 | |
25 | public abstract int getItemCount(); |
下面的分析过程,是按照实际的业务流程来走的,也便于你了解到 RecyclerView 到底是如何工作的
1.getItemCount
首先,对于一个RecyclerView来说,它在不断向下滑动的过程中,需要知道什么时候滑动到了底部,这个可以通过 getItemCount来得到,当超过了ItemCount的时候,列表不再滑动
2.getItemType(int position)
实现机制和BaseAdapter一致,参考ListView的分析
3.onCreateViewHolder( ViewGroup parent , int viewType )
注意,返回的不仅是控件的实例哈,我们写的ViewHolder必须继承自 RecyclerView.ViewHolder,并在构造方法中调用super,这样,RecyclerView.ViewHolder内部有一个ItemView会将布局实例保存下来,所以,你不需要再单独地将ViewHolder和ItemView绑定,因为RecyclerView已经帮你绑定好了。
4.onBindViewHolder( ViewHolder vh, int position, List
这里就是将数据绑定到vh里面啦,很简单,就不作过多的说明了,唯一一点需要注意的是,有时候会出现Item内容错乱的情况,很大概率是绑定数据的时候,没有清空原来的数据,当然,这种一般是发生在条件绑定的情况,就是某些条件下才绑定某些数据,因为如果每次都绑定所有控件的话,相当于也是把原来的数据清空了。
一个简单的多ItemType的Adapter的实现
多ItemType的情况有很多种,处理方式也不太一样,但是关键的点,无非就在于,在 getItemType 中获取到 type,然后分别在 onCreateViewHolder 和 onBindViewHolder 中进行分支操作。这里以 一个添加了 header 和 footer 的 Rv 的Adapter 伪码作为实例
1 | // List<Course> courses; |
2 | public MAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> |
3 | { |
4 | public final int TYPE_HEADER = 1; |
5 | public final int TYPE_FOOTER = 2; |
6 | public final int TYPE_COURSE = 3; |
7 | |
8 | // 也可能是list里面的数据有不同的 type,可以根据具体的情况来判断 type |
9 | public int getItemType(int position){ |
10 | int count = getItemCount(); |
11 | if(position == 0) |
12 | { |
13 | return TYPE_HEADER; |
14 | }else if(position < count) |
15 | { |
16 | return TYPE_COURSE; |
17 | }else |
18 | { |
19 | return TYPE_FOOTER; |
20 | } |
21 | } |
22 | |
23 | public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int |
24 | viewType) |
25 | { |
26 | switch(viewType){ |
27 | case TYPE_HEADER: |
28 | return new HeadHolder(layoutInflater.inflate(.....)); |
29 | break; |
30 | case TYPE_COURSE: |
31 | return new CourseHolder(...); |
32 | break; |
33 | case TYPE_FOOTER: |
34 | return new FooterHolder(....) |
35 | break; |
36 | } |
37 | } |
38 | |
39 | public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { |
40 | switch(holder.getItemViewType()){ |
41 | case TYPE_HEADER: |
42 | bindHeader(holder,position); |
43 | break; |
44 | case .. |
45 | .... |
46 | } |
47 | } |
48 | |
49 | public void bindHeader(RecyclerView.ViewHolder vh,int position){ |
50 | .... |
51 | } |
52 | |
53 | public void bindCourse() |
54 | public void bindFooter() |
55 | .... |
56 | } |
多ItemType关于数据源的处理
这算是比较简单的情况了,有时候我们会碰到有两个数据源的情况,这时我们该如何处理呢?
1 | List<A> listA; // 对应布局 item_a |
2 | List<B> listB; // 对应布局 item_b |
这种情况涉及到以下几个问题
1.listA和listB都是通过构造函数传进Adapter么?
2.界面的刷新是等数据全部加载完,还是每加载完一个刷新一下?
3.刷新的方式?如何才是比较优雅的刷新?
采用原始泛型的思想
1 | ----- MainActivity ----------- |
2 | List<Object> data; |
3 | data.addAll(listA); |
4 | data.addAll(listB); |
5 | adapter = new Adapter(data) |
6 | |
7 | ----- Adapter --------------- |
8 | public int getItemType(int position){ |
9 | Object object = data.get(position); |
10 | if(object.instanceOf(A)){ |
11 | return TYPE_A; |
12 | }else{ |
13 | return TYPE_B; |
14 | } |
15 | } |
16 | |
17 | // 在绑定的适合进行强转 |
18 | public void onBindViewHolder(....., List<Object> data){ |
19 | if(holder.getType==TYPE_A){ |
20 | bindA(holder,(A)data.get(position)); |
21 | }else{ |
22 | bindB(holder,(B)data.get(position)); |
23 | } |
24 | } |
实现一个Model接口
1 | public interface Model{ |
2 | // 空实现 |
3 | } |
4 | |
5 | -------- 数据源 --------------- |
6 | public class A implements Model{ |
7 | .... |
8 | public static final int TYPE_A = 1; |
9 | } |
10 | B同上 |
11 | |
12 | -------- MainActivity ---------- |
13 | List<Model> data; |
14 | List<A> listA; |
15 | List<B> listB; |
16 | |
17 | data.addAll(listA); |
18 | data.addAll(listB); |
19 | adapter = new Adapter(.... , data) |
20 | |
21 | -------- Adapter ---------------- |
22 | public int getItemType(int position){ |
23 | Model model = data.get(position); |
24 | if(model.instanceOf(A)){ |
25 | return TYPE_A; |
26 | }else{ |
27 | return TYPE_B; |
28 | } |
29 | } |
关于 Model接口,一开始我写了个 getType 方法在里面,这样就不用在判断type的时候根据类名来判断了,不过后来非猿哥告诉我,这样不太好,这不该是Model该做的工作,而是Adapter的任务,所以,我就把这个任务改成了空实现。
官方的 Rv就是这样,现在有很多不错的开源的封装库,能让我们更好地使用 RecyclerView,这里强烈推荐以下库:
我会专门开一篇文章来介绍 Flap 的使用以及实现原理,以及我们为什么称它为当前最强大易用的 RecyclerView 使用框架。而且我继承了FlapAdapter增加添加Header和footer的功能。
关于Header与Footer
现在有很多的图片上拉刷新,下拉加载框架,所以,个人觉得把这个交给这些框架来做就好,不要用header或者footer去做,很麻烦 = =,当然,大佬请无视我的话哈
Header与Footer解决什么
有很多的页面现在都是由一个 Banner 和下面的列表项组成,而且,通常的设计是 Banner 可以和 列表项一起滑动,
Header需要刷新数据(不难,在List
如何添加Header与Footer
主要两种方式:
1.将header与footer加入数据源 data
2.不加入data,直接在Adapter里面做一些适配(不太适合需要刷新数据的Header与Footer)
ListView的几个需要注意的地方
OnIntemSelectedListener
这个Listener仅在按键模式下有用(待考证,讲的比较笼统),在触摸模式下,这个一般不会回调,onItemSelected指的就是选中了当前Item(TV模式下背景会变成已选中),但此时还不会执行 onClick。
网上有一些办法可以解决ListView初始化的时候自动调用 onItemClick 的问题。
多 ItemType 的Type定义的问题
之前在实现一个多 ItemType的ListView的时候,addScrpaView 方法遇到了数组溢出问题.后来查阅一些资料,发现问题出在Type的定义上
1 | public static final int TYPE_1 = 1024; |
2 | public static final int TYPE_2 = 2048; |
在定义 Type 的时候,最好是从0定义,目前没有仔细阅读源码,不知道为什么会出现这样的问题,不过记住这个先
ListView源码分析
Adapter的作用
将数据适配工作与View结构解耦。Adapter是数据源,AdapterView是展示数据源的UI控件,Adapter是给AdapterView使用的,通过调用AdapterView的setAdapter方法就可以让一个AdapterView绑定Adapter对象,从而AdapterView会将Adapter中的数据展示出来。
Adapter的作用除了适配数据之外,还有一个非常重要的作用,就是 getView (包含了数据绑定的步骤),这也是为了解耦,这样ListView才能完全地与数据解耦(不必绑定数据)
需要关注一个方法
Adapter # getItemViewType:ListView 如果想要实现多 ItemType,则必须实现这个方法,不实现当然也可以,但是你就无法重用 ConvertView 了,因为当无法取到对应的 Type 的缓存队列的话,convertView 默认就会返回默认的Type,这样在绑定数据的时候就会出现错误
RecycleBin 机制
RecycleBin 是 AbsListView 的一个内部类,其主要作用就是提供了 ItemView 的缓存机制,即重用划出屏幕的 View。
AbsListView -> RecycleBin
1 | /** |
2 | * The RecycleBin facilitates reuse of views across layouts. The RecycleBin |
3 | * has two levels of storage: ActiveViews and ScrapViews. ActiveViews are |
4 | * those views which were onscreen at the start of a layout. By |
5 | * construction, they are displaying current information. At the end of |
6 | * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews |
7 | * are old views that could potentially be used by the adapter to avoid |
8 | * allocating views unnecessarily. |
9 | * RecycleBin维护两个Level的存储:ActiveViews和ScrapViews |
10 | * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) |
11 | * @see android.widget.AbsListView.RecyclerListener |
12 | */ |
13 | class RecycleBin { |
14 | private RecyclerListener mRecyclerListener; |
15 | |
16 | /** |
17 | * The position of the first view stored in mActiveViews. |
18 | */ |
19 | private int mFirstActivePosition; |
20 | |
21 | /** |
22 | * Views that were on screen at the start of layout. This array is |
23 | * populated at the start of layout, and at the end of layout all view |
24 | * in mActiveViews are moved to mScrapViews. Views in mActiveViews |
25 | * represent a contiguous range of Views, with position of the first |
26 | * view store in mFirstActivePosition. |
27 | */ |
28 | private View[] mActiveViews = new View[0]; |
29 | |
30 | /** |
31 | * Unsorted views that can be used by the adapter as a convert view. |
32 | */ |
33 | private ArrayList<View>[] mScrapViews; |
34 | |
35 | private int mViewTypeCount; |
36 | |
37 | private ArrayList<View> mCurrentScrap; |
38 | |
39 | /** |
40 | * Fill ActiveViews with all of the children of the AbsListView. |
41 | * @param childCount The minimum number of views mActiveViews should hold |
42 | * @param firstActiPosition The position of the first view that will be stored in mActiveViews |
43 | */ |
44 | ----------- |
45 | // 这个方法在 RecycleBin 里有点异类。它的作用是将 AbsListView的所有子View添加到 mActiveViews 数组里 |
46 | // 它主要是为了防止第二次 layout,要重新 inflate 新的 view。这也是它闪光的唯一时刻 |
47 | void fillActiveViews(int childCount, int firstActivePosition) { |
48 | if (mActiveViews.length < childCount) { |
49 | mActiveViews = new View[childCount]; |
50 | } |
51 | mFirstActivePosition = firstActivePosition; |
52 | final View[] activeViews = mActiveViews; |
53 | for (int i = 0; i < childCount; i++) { |
54 | View child = getChildAt(i); |
55 | AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); |
56 | // Don't put header or footer views into the scrap heap |
57 | if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { |
58 | // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in |
59 | // active views. |
60 | // However, we will NOT place them into scrap views. |
61 | activeViews[i] = child; |
62 | } |
63 | } |
64 | } |
65 | |
66 | /** |
67 | * Get the view corresponding to the specified position. The view will |
68 | * be removed from mActiveViews if it is found. |
69 | * @param position The position to look up in mActiveViews |
70 | * @return The view if it is found, null otherwise |
71 | */ |
72 | View getActiveView(int position) { |
73 | int index = position - mFirstActivePosition; |
74 | final View[] activeViews = mActiveViews; |
75 | if (index >= 0 && index < activeViews.length) { |
76 | final View match = activeViews[index]; |
77 | activeViews[index] = null; |
78 | return match; |
79 | } |
80 | return null; |
81 | } |
82 | |
83 | //Move all views remaining in mActiveViews to mScrapViews. |
84 | void scrapActiveViews() { |
85 | //.... |
86 | } |
87 | |
88 | /** |
89 | * Put a view into the ScapViews list. These views are unordered. |
90 | * @param scrap The view to add |
91 | */ |
92 | void addScrapView(View scrap) { |
93 | AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); |
94 | if (lp == null) { |
95 | return; |
96 | } |
97 | // Don't put header or footer views or views that should be ignored |
98 | // into the scrap heap |
99 | int viewType = lp.viewType; |
100 | if (!shouldRecycleViewType(viewType)) { |
101 | if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { |
102 | removeDetachedView(scrap, false); |
103 | } |
104 | return; |
105 | } |
106 | if (mViewTypeCount == 1) { |
107 | dispatchFinishTemporaryDetach(scrap); |
108 | mCurrentScrap.add(scrap); |
109 | } else { |
110 | dispatchFinishTemporaryDetach(scrap); |
111 | mScrapViews[viewType].add(scrap); |
112 | } |
113 | |
114 | if (mRecyclerListener != null) { |
115 | mRecyclerListener.onMovedToScrapHeap(scrap); |
116 | } |
117 | } |
118 | |
119 | /** |
120 | * @return A view from the ScrapViews collection. These are unordered. |
121 | */ |
122 | View getScrapView(int position) { |
123 | ArrayList<View> scrapViews; |
124 | if (mViewTypeCount == 1) { |
125 | scrapViews = mCurrentScrap; |
126 | int size = scrapViews.size(); |
127 | if (size > 0) { |
128 | return scrapViews.remove(size - 1); |
129 | } else { |
130 | return null; |
131 | } |
132 | } else { |
133 | int whichScrap = mAdapter.getItemViewType(position); |
134 | if (whichScrap >= 0 && whichScrap < mScrapViews.length) { |
135 | scrapViews = mScrapViews[whichScrap]; |
136 | int size = scrapViews.size(); |
137 | if (size > 0) { |
138 | return scrapViews.remove(size - 1); |
139 | } |
140 | } |
141 | } |
142 | return null; |
143 | } |
144 | |
145 | public void setViewTypeCount(int viewTypeCount) { |
146 | if (viewTypeCount < 1) { |
147 | throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); |
148 | } |
149 | // noinspection unchecked |
150 | ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; |
151 | for (int i = 0; i < viewTypeCount; i++) { |
152 | scrapViews[i] = new ArrayList<View>(); |
153 | } |
154 | mViewTypeCount = viewTypeCount; |
155 | mCurrentScrap = scrapViews[0]; |
156 | mScrapViews = scrapViews; |
157 | } |
158 | |
159 | } |
这个方法在 RecycleBin 里有点异类。它的作用是将 AbsListView的所有子View添加到 mActiveViews 数组里
// 它主要是为了防止第二次 layout,要重新 inflate 新的 view。这也是它闪光的唯一时刻
mActiveViews[] vs mScrapVies[]
RecycleBin 中维护了两个很重要的数组
- mActiveViews[] : 当前在页面(onScreen)的 View .这个数组可以说是在不断变化的,最初是空的,即便是在滑动过程中,也会在 n+1和n+2之间波动 。 特别要注意的是,如果一个 ItemView 滑动出了屏幕,它就会被 mActiveVies 移除出数组,并且新进入屏幕的 View,如果他不是从 mActivieViews 中直接重用的,它就会被加入 mActiveViews . 关于直接重用。关于直接重用这个概念有一点需要清楚,我们的ListView不是说只有在新的 Item 进来了之后才重绘,实际上,只要是视图改变了(滑动),它都要重绘,重绘之前,他的子View们都被Dettah了,之前的子View 仍然要重新布局,重新attach,这时候,不过我们的 数据还在,mActivitViews 还储存着之前的 onScreen 的Views,这时候就可以重用了。
- mScrapViews[] : 当前被回收的 View,也就是 offScreen 的View,当然这个最初也是0,在华东过程中它的实际大小也会波动。
FillActiveViews
这个方法在 RecycleBin 里有点异类。它的作用是将 AbsListView的所有子View添加到 mActiveViews 数组里
。它主要是为了防止第二次 layout,要重新 inflate 新的 view。这也是它闪光的唯一时刻。
RecycleBin 的基本原理
当ListView加载一个子View的时候(onScreen),它会做如下的判断:
上面是 RecycleBin 的核心思想,RecycleBin 提供的相应的API 也是点到为止,比如数组内元素的添加和删除等工作,机制的实现还得靠 ListView 相关的方法(主要就是 ListView 的 onLayout 方法)。下面,我们深入一下源代码,看看 ListView 是如何实现这些的。
ListView # onLayout
写在前面:
- View是一帧一帧绘制的,每一帧绘制都经历了measure->layout->draw这三个阶段,绘制完一帧之后,如果UI需要更新,比如用户滚动了ListView,那么又会绘制下一帧,再次经历measure->layout->draw方法。
- ListView # childCount : childCount 并不是 Adapter 中数据数目,而是显示在 ListView 中的 Item 数目(onScreen)。其实也很好理解,虽然ListView里面可能缓存了很多的 View ,但是真正 attach 到 ListView 的数量其实是有限但不一定的(会在 n+1~n+2 的范围内波动)
- ListView # getChildAt( position ):结合上面的概念,也就不难理解了,position也并非指代在数据源(List
)中的位置,而是处于ListView中(onScreen)的位置,比如 getChildAt(0) 返回的就是 firstPosition(这个则是 ListView 记录的对应 Adapter中的位置) 的View。
第一次 Layout:
ListView并没有重写 onLayout ,那我们看看父类 AbsListView 中的 onLayout 方法
1 | /** |
2 | * Subclasses should NOT override this method but {@link #layoutChildren()} |
3 | * instead. |
4 | */ |
5 |
|
6 | protected void onLayout(boolean changed, int l, int t, int r, int b) { |
7 | super.onLayout(changed, l, t, r, b); |
8 | mInLayout = true; |
9 | if (changed) { |
10 | int childCount = getChildCount(); |
11 | for (int i = 0; i < childCount; i++) { |
12 | getChildAt(i).forceLayout(); |
13 | } |
14 | mRecycler.markChildrenDirty(); |
15 | } |
16 | layoutChildren(); |
17 | mInLayout = false; |
18 | } |
可以看到,onLayout()方法中并没有做什么复杂的逻辑操作,主要就是一个判断,如果ListView的大小或者位置发生了变化,那么changed变量就会变成true,此时会要求所有的子布局都强制进行重绘。除此之外倒没有什么难理解的地方了,不过我们注意到,在第16行调用了layoutChildren()这个方法,从方法名上我们就可以猜出这个方法是用来进行子元素布局的,不过进入到这个方法当中你会发现这是个空方法,没有一行代码。这当然是可以理解的了,因为子元素的布局应该是由具体的实现类来负责完成的,而不是由父类完成。那么进入ListView的layoutChildren()方法,代码如下所示:
1 |
|
2 | protected void layoutChildren() { |
3 | final boolean blockLayoutRequests = mBlockLayoutRequests; |
4 | if (!blockLayoutRequests) { |
5 | mBlockLayoutRequests = true; |
6 | } else { |
7 | return; |
8 | } |
9 | try { |
10 | super.layoutChildren(); |
11 | invalidate(); |
12 | if (mAdapter == null) { |
13 | resetList(); |
14 | invokeOnItemScrollListener(); |
15 | return; |
16 | } |
17 | int childrenTop = mListPadding.top; |
18 | int childrenBottom = getBottom() - getTop() - mListPadding.bottom; |
19 | int childCount = getChildCount(); |
20 | int index = 0; |
21 | int delta = 0; |
22 | View sel; |
23 | View oldSel = null; |
24 | View oldFirst = null; |
25 | View newSel = null; |
26 | View focusLayoutRestoreView = null; |
27 | |
28 | // Remember stuff we will need down below |
29 | // 好像是为了记录之前的状态(比如选中的是哪个 Position),这段代码可以先关注 |
30 | switch (mLayoutMode) { |
31 | case LAYOUT_SET_SELECTION: |
32 | index = mNextSelectedPosition - mFirstPosition; |
33 | if (index >= 0 && index < childCount) { |
34 | newSel = getChildAt(index); |
35 | } |
36 | break; |
37 | case LAYOUT_FORCE_TOP: |
38 | case LAYOUT_FORCE_BOTTOM: |
39 | case LAYOUT_SPECIFIC: |
40 | case LAYOUT_SYNC: |
41 | break; |
42 | case LAYOUT_MOVE_SELECTION: |
43 | default: |
44 | // Remember the previously selected view |
45 | index = mSelectedPosition - mFirstPosition; |
46 | if (index >= 0 && index < childCount) { |
47 | oldSel = getChildAt(index); |
48 | } |
49 | // Remember the previous first child |
50 | oldFirst = getChildAt(0); |
51 | if (mNextSelectedPosition >= 0) { |
52 | delta = mNextSelectedPosition - mSelectedPosition; |
53 | } |
54 | // Caution: newSel might be null |
55 | newSel = getChildAt(index + delta); |
56 | } |
57 | |
58 | boolean dataChanged = mDataChanged; |
59 | if (dataChanged) { |
60 | handleDataChanged(); |
61 | } |
62 | // Handle the empty set by removing all views that are visible |
63 | // and calling it a day |
64 | if (mItemCount == 0) { |
65 | resetList(); |
66 | invokeOnItemScrollListener(); |
67 | return; |
68 | } else if (mItemCount != mAdapter.getCount()) { |
69 | throw new IllegalStateException("The content of the adapter has changed but " |
70 | + "ListView did not receive a notification. Make sure the content of " |
71 | + "your adapter is not modified from a background thread, but only " |
72 | + "from the UI thread. [in ListView(" + getId() + ", " + getClass() |
73 | + ") with Adapter(" + mAdapter.getClass() + ")]"); |
74 | } |
75 | setSelectedPositionInt(mNextSelectedPosition); |
76 | // Pull all children into the RecycleBin. |
77 | // These views will be reused if possible |
78 | final int firstPosition = mFirstPosition; |
79 | final RecycleBin recycleBin = mRecycler; |
80 | // reset the focus restoration |
81 | View focusLayoutRestoreDirectChild = null; |
82 | // Don't put header or footer views into the Recycler. Those are |
83 | // already cached in mHeaderViews; |
84 | if (dataChanged) { |
85 | for (int i = 0; i < childCount; i++) { |
86 | recycleBin.addScrapView(getChildAt(i)); |
87 | if (ViewDebug.TRACE_RECYCLER) { |
88 | ViewDebug.trace(getChildAt(i), |
89 | ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i); |
90 | } |
91 | } |
92 | } else { |
93 | //----------------------- |
94 | //这句代码很关键,我们将缓存所有的 ActiveViews。由此可以看出好像并不是每添加一个View,就保存一个,而是批量保存 |
95 | recycleBin.fillActiveViews(childCount, firstPosition); |
96 | } |
97 | // take focus back to us temporarily to avoid the eventual |
98 | // call to clear focus when removing the focused child below |
99 | // from messing things up when ViewRoot assigns focus back |
100 | // to someone else |
101 | final View focusedChild = getFocusedChild(); |
102 | if (focusedChild != null) { |
103 | // TODO: in some cases focusedChild.getParent() == null |
104 | // we can remember the focused view to restore after relayout if the |
105 | // data hasn't changed, or if the focused position is a header or footer |
106 | if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) { |
107 | focusLayoutRestoreDirectChild = focusedChild; |
108 | // remember the specific view that had focus |
109 | focusLayoutRestoreView = findFocus(); |
110 | if (focusLayoutRestoreView != null) { |
111 | // tell it we are going to mess with it |
112 | focusLayoutRestoreView.onStartTemporaryDetach(); |
113 | } |
114 | } |
115 | requestFocus(); |
116 | } |
117 | // Clear out old views |
118 | detachAllViewsFromParent(); |
119 | switch (mLayoutMode) { |
120 | case LAYOUT_SET_SELECTION: |
121 | if (newSel != null) { |
122 | sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); |
123 | } else { |
124 | sel = fillFromMiddle(childrenTop, childrenBottom); |
125 | } |
126 | break; |
127 | case LAYOUT_SYNC: |
128 | sel = fillSpecific(mSyncPosition, mSpecificTop); |
129 | break; |
130 | case LAYOUT_FORCE_BOTTOM: |
131 | sel = fillUp(mItemCount - 1, childrenBottom); |
132 | adjustViewsUpOrDown(); |
133 | break; |
134 | case LAYOUT_FORCE_TOP: |
135 | mFirstPosition = 0; |
136 | sel = fillFromTop(childrenTop); |
137 | adjustViewsUpOrDown(); |
138 | break; |
139 | case LAYOUT_SPECIFIC: |
140 | sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop); |
141 | break; |
142 | case LAYOUT_MOVE_SELECTION: |
143 | sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom); |
144 | break; |
145 | //------- |
146 | // 核心代码 |
147 | default: |
148 | // 第一次 onLayout 会走这里 |
149 | if (childCount == 0) { |
150 | // 默认填充方式:从上往下 |
151 | if (!mStackFromBottom) { |
152 | final int position = lookForSelectablePosition(0, true); |
153 | setSelectedPositionInt(position); |
154 | // 从上开始往下填充 |
155 | sel = fillFromTop(childrenTop); |
156 | } else { |
157 | final int position = lookForSelectablePosition(mItemCount - 1, false); |
158 | setSelectedPositionInt(position); |
159 | sel = fillUp(mItemCount - 1, childrenBottom); |
160 | } |
161 | } else { |
162 | if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { |
163 | sel = fillSpecific(mSelectedPosition, |
164 | oldSel == null ? childrenTop : oldSel.getTop()); |
165 | } else if (mFirstPosition < mItemCount) { |
166 | sel = fillSpecific(mFirstPosition, |
167 | oldFirst == null ? childrenTop : oldFirst.getTop()); |
168 | } else { |
169 | sel = fillSpecific(0, childrenTop); |
170 | } |
171 | } |
172 | break; |
173 | } |
174 | //--------------------------- |
175 | // Flush any cached views that did not get reused above |
176 | recycleBin.scrapActiveViews(); |
177 | if (sel != null) { |
178 | // the current selected item should get focus if items |
179 | // are focusable |
180 | if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) { |
181 | final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild && |
182 | focusLayoutRestoreView.requestFocus()) || sel.requestFocus(); |
183 | if (!focusWasTaken) { |
184 | // selected item didn't take focus, fine, but still want |
185 | // to make sure something else outside of the selected view |
186 | // has focus |
187 | final View focused = getFocusedChild(); |
188 | if (focused != null) { |
189 | focused.clearFocus(); |
190 | } |
191 | positionSelector(sel); |
192 | } else { |
193 | sel.setSelected(false); |
194 | mSelectorRect.setEmpty(); |
195 | } |
196 | } else { |
197 | positionSelector(sel); |
198 | } |
199 | mSelectedTop = sel.getTop(); |
200 | } else { |
201 | if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) { |
202 | View child = getChildAt(mMotionPosition - mFirstPosition); |
203 | if (child != null) positionSelector(child); |
204 | } else { |
205 | mSelectedTop = 0; |
206 | mSelectorRect.setEmpty(); |
207 | } |
208 | // even if there is not selected position, we may need to restore |
209 | // focus (i.e. something focusable in touch mode) |
210 | if (hasFocus() && focusLayoutRestoreView != null) { |
211 | focusLayoutRestoreView.requestFocus(); |
212 | } |
213 | } |
214 | // tell focus view we are done mucking with it, if it is still in |
215 | // our view hierarchy. |
216 | if (focusLayoutRestoreView != null |
217 | && focusLayoutRestoreView.getWindowToken() != null) { |
218 | focusLayoutRestoreView.onFinishTemporaryDetach(); |
219 | } |
220 | mLayoutMode = LAYOUT_NORMAL; |
221 | mDataChanged = false; |
222 | mNeedSync = false; |
223 | setNextSelectedPositionInt(mSelectedPosition); |
224 | updateScrollIndicators(); |
225 | if (mItemCount > 0) { |
226 | checkSelectionChanged(); |
227 | } |
228 | invokeOnItemScrollListener(); |
229 | } finally { |
230 | if (!blockLayoutRequests) { |
231 | mBlockLayoutRequests = false; |
232 | } |
233 | } |
234 | } |
再来看看 fillFromTop 方法:
1 | /** |
2 | * Fills the list from top to bottom, starting with mFirstPosition |
3 | * @param nextTop The location where the top of the first item should be drawn |
4 | * @return The view that is currently selected |
5 | */ |
6 | private View fillFromTop(int nextTop) { |
7 | mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); |
8 | mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); |
9 | if (mFirstPosition < 0) { |
10 | mFirstPosition = 0; |
11 | } |
12 | // 我们看到,最终还是调用 fillDown 这个通用的函数(通用的意思是作为一个经常调用的方法,不依赖具体的情况) |
13 | return fillDown(mFirstPosition, nextTop); |
14 | } |
fillDown
1 | /** |
2 | * Fills the list from pos down to the end of the list view. |
3 | * @param pos The first position to put in the list |
4 | * @param nextTop The location where the top of the item associated with posshould be drawn |
5 | * @return The view that is currently selected, if it happens to be in the range that we draw. |
6 | */ |
7 | private View fillDown(int pos, int nextTop) { |
8 | View selectedView = null; |
9 | int end = (getBottom() - getTop()) - mListPadding.bottom; |
10 | |
11 | //不断地 makeAndAddView,直到下一个子View的Top不在屏幕中(也就是它不该被显示出来)或者数据已经到头了 |
12 | while (nextTop < end && pos < mItemCount) { |
13 | // is this the selected item? |
14 | boolean selected = pos == mSelectedPosition; |
15 | |
16 | // 这个 makeAndAddView就是下一步要执行的核心方法了 |
17 | View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); |
18 | //根据返回的child决定它的 nextTop |
19 | nextTop = child.getBottom() + mDividerHeight; |
20 | if (selected) { |
21 | selectedView = child; |
22 | } |
23 | pos++; |
24 | } |
25 | return selectedView; |
26 | } |
makeAndAddView
1 | /** |
2 | * Obtain the view and add it to our list of children. The view can be made |
3 | * fresh, converted from an unused view, or used as is if it was in the |
4 | * recycle bin. |
5 | */ |
6 | private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, |
7 | boolean selected) { |
8 | View child; |
9 | if (!mDataChanged) { |
10 | // Try to use an exsiting view for this position |
11 | child = mRecycler.getActiveView(position); |
12 | if (child != null) { |
13 | // Found it -- we're using an existing child |
14 | // This just needs to be positioned |
15 | ----- |
16 | setupChild(child, position, y, flow, childrenLeft, selected, true); |
17 | return child; |
18 | } |
19 | } |
20 | // Make a new view for this position, or convert an unused view if possible |
21 | child = obtainView(position, mIsScrap); |
22 | // This needs to be positioned and measured |
23 | ----- |
24 | setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); |
25 | return child; |
26 | } |
这个方法主要就是获得一个View(有一个优先顺序,也就是先判断 mActiveVies 里面有没有 ; 没有就走 obtainView 方法,这个方法是一定能获取到View 的,它会先判断 mScrapView 是否可以返回一个View,没有的话就会调用 adapter.getView 并传入 convertView = null 以 inflate 一个新的 View) ,并且 将其传入 setupChild() (setupChild 方法的作用就是将这个获取到的子 View 加入到 ListView 中),由于我们前面的 fillDown 方法的循环,最终会不断地加入子View直到填满。
第二次 Layout
第二次的Layout仍然会继续从layoutChildern()开始。但相比较于第一次childCount不为0,那么相应的下面的逻辑也就发生变化了。我们在第一次layout的时候,调用过一个 fillActiveViews 方法,已经将 ActivieViews 都保存了,所以不用担心第二次 Layouyt的时候 View 不能重用的问题了。
我们可以做些什么: [RecyclerListener]
1 | /** |
2 | * A RecyclerListener is used to receive a notification whenever a View is placed |
3 | * inside the RecycleBin's scrap heap. This listener is used to free resources |
4 | * associated to Views placed in the RecycleBin. |
5 | * android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) |
6 | */ |
7 | public static interface RecyclerListener { |
8 | /** |
9 | * Indicates that the specified View was moved into the recycler's scrap heap. |
10 | * The view is not displayed on screen any more and any expensive resource |
11 | * associated with the view should be discarded. |
12 | * @param view |
13 | */ |
14 | void onMovedToScrapHeap(View view); |
15 | } |
我们可以在 onMoveToScrapHeap 中做一些资源回收的操作。