├── README.md
├── article
├── LruCache源码解析.md
├── NavigationView源码解析.md
├── ViewDragHelper源码分析.md
├── ViewGroup 源码解析.md
├── ViewStub 源码解析.md
├── images
│ ├── fontmetrics.gif
│ ├── generate_child_measurespec.png
│ ├── viewgroup_lifecycle.png
│ └── viewgroup_touchevent.png
├── imgs-navigationview
│ ├── nv-sample.png
│ ├── nv-sample_02.png
│ └── nv-sample_03.gif
└── textview源碼解析.md
└── images
└── qrcode.jpg
/README.md:
--------------------------------------------------------------------------------
1 | ANDROID SDK 源码解析
2 | ===============================
3 |
4 | 
5 |
6 | #### GitHub小伙伴公众号,欢迎扫码关注!
7 |
8 |
9 |
10 | -----
11 |
12 | ##### 概要说明:
13 |
14 | * 已发布文章 发表已经整理好的文章,读者可以阅读学习!
15 |
16 | * 认领方式 可以在 issues 提你要认领什么内容。
17 |
18 | ~~* 已认领文章 如果你喜欢的文章被认领,你想参与,你还是可以分析认领,我们选择好的发布,也可以作为校对者。认领方式:可以在 Issues 提你要认领什么内容~~
19 |
20 | ~~* 待认领文章 是想参与的的同学可以参与进来,如被认领,也可以做校对者,若想解析的内容不在表格,可以联系我们添加分析的内容,方式:在 Issues 提你要认领什么内容~~
21 |
22 | ##### 校对发布说明:
23 | 分析完成后可直接在对应 issue 下回复,可直接原文回复也可是原文链接,校对通过后会直接进行发布。(这样大家可以更灵活自由的安排,同时也可以更快的发布校对好的文章)
24 |
25 | ##### 转载说明:
26 | 这里每一篇文章我们都或多或少的付出了时间、精力分析校对,第一次搞这种源码解析,可能有很多地方做的不好,但是我们用心做了!所以,如果你想转载,至少文章开头写下来源地址:
27 |
28 | [https://github.com/LittleFriendsGroup/AndroidSdkSourceAnalysis](https://github.com/LittleFriendsGroup/AndroidSdkSourceAnalysis)
29 |
30 | ,还有写下分析者名字!请尊重每一篇文章的劳动成果,谢谢!
31 |
32 | ## 已发布文章
33 |
34 | ### 第三期
35 | Class | 分析者 | 校对者 | 版本 | 发布时间
36 | :------------- | :------------- | :------------- | :------------- | :-------------
37 | [ViewGroup 源码解析](https://github.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/blob/master/article/ViewGroup%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) | [7heaven](https://github.com/7heaven) | [Nukc](https://github.com/nukc) | branch nougat-mr2-release | 2017/4/17
38 | [StaticLayout 源码解析](http://jaeger.itscoder.com/android/2016/08/05/staticlayout-source-analyse.html) | [laobie](https://github.com/laobie) | [7heaven](https://github.com/7heaven) | android api 23 | 2017/4/17
39 | [AtomicFile 源码解析](https://github.com/GcsSloop/AndroidNote/blob/master/SourceAnalysis/AtomicFile.md) | [GcsSloop](https://github.com/GcsSloop) | [Nukc](https://github.com/nukc) | android api 25 | 2017/4/17
40 | [Spannable 源码解析](https://github.com/lber19535/SourceAnalysis/blob/master/Spannable%20%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) | [lber19535](https://github.com/lber19535) | [Nukc](https://github.com/nukc) | android api 24 | 2017/4/17
41 | [Notification 源码解析](http://www.jianshu.com/p/0cb97db7090c) | [huanglongyu](https://github.com/huanglongyu) | [Nukc](https://github.com/nukc) | android api 21 (cm) | 2017/4/17
42 | [SparseArray 源码解析](http://sonaive.me/2016/05/04/sparse-array-analysis/) | [taoliuh](https://github.com/taoliuh) | [Nukc](https://github.com/nukc) | android api 22 | 2017/4/17
43 | [ViewStub 源码解析](https://github.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/blob/master/article/ViewStub%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) | [Nukc](https://github.com/nukc) | [7heaven](https://github.com/7heaven) | android api 25 | 2017/4/17
44 |
45 | ### 第二期
46 | Class | 分析者 | 校对者 | 版本 | 发布时间
47 | :------------- | :------------- | :------------- | :------------- | :-------------
48 | [MediaPlayer源码解析](https://github.com/lber19535/SourceAnalysis/blob/master/Media%20Player%20%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) | [lber19535](https://github.com/lber19535) | [android-cjj](https://github.com/android-cjj) | android api 22 | 2016/7/25
49 | [NavigationView源码解析](https://github.com/hongyangAndroid/AndroidSdkSourceAnalysis/blob/master/article/NavigationView%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) | [hongyangAndroid](https://github.com/hongyangAndroid) | [android-cjj](https://github.com/android-cjj) | support-v7-23.1.0 | 2016/7/25
50 | [Service源码解析](https://github.com/asLody/SourceAnalysis/blob/master/Service%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) | [asLody](https://github.com/asLody) | [liaohuqiu](https://github.com/liaohuqiu) | android api 23 | 2016/7/25
51 | [SharePreferences源码解析](http://blog.csdn.net/yanbober/article/details/47866369) | [yanbober](https://github.com/yanbober) | [android-cjj](https://github.com/android-cjj) | android api 22 | 2016/7/25
52 | [ScrollView源码分析](https://github.com/Skykai521/AndroidSdkSourceAnalysis/blob/master/article/ScrollView%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) | [Skykai521](https://github.com/Skykai521) | [android-cjj](https://github.com/android-cjj) | android api 23 | 2016/7/25
53 | [Handler源码解析](https://github.com/maoruibin/HandlerAnalysis) | [maoruibin](https://github.com/maoruibin) | [android-cjj](https://github.com/android-cjj) | android api 23 | 2016/7/25
54 | [NestedScrollView源码解析](https://github.com/xmuSistone/android-source-analysis/blob/master/NestedScrollView.md) | [xmuSistone](https://github.com/xmuSistone) | [android-cjj](https://github.com/android-cjj) | support-v4-23.1.0 | 2016/7/25
55 | [SQLiteOpenHelper/...源码解析](https://github.com/YZHIWEN/AndroidSdkSourceAnalysis/blob/master/SQLite_Android.md) | [YZHIWEN](https://github.com/YZHIWEN) | [CaMnter](https://github.com/CaMnter) | android api 23 | 2016/7/25
56 | [Bundle源码解析](https://github.com/ASPOOK/BundleAnalysis) | [ASPOOK](https://github.com/ASPOOK) | [CaMnter](https://github.com/CaMnter) | android api 23 | 2016/7/25
57 | [LocalBroadcastManager源码解析](https://github.com/czhzero/AndroidSdkSourceAnalysis/blob/master/article/LocalBroadcastManager%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) | [czhzero](https://github.com/czhzero) | [CaMnter](https://github.com/CaMnter) | support-v4-23.4.0 | 2016/7/25
58 | [Toast源码解析](https://github.com/WuXiaolong/AndroidSdkSourceAnalysis/blob/master/article/Toast%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) | [Wuxiaolong](https://github.com/WuXiaolong) | [Nukc](https://github.com/nukc) | android api 23 | 2016/7/25
59 | [TextInputLayout源码解析](https://github.com/wbersaty/TextInputLayout-24) | [wbersaty](https://github.com/wbersaty) | [android-cjj](https://github.com/android-cjj) | design-24.0.0-alpha2 | 2016/7/25
60 | [LayoutInflater...源码解析](https://github.com/peerless2012/SourceAnalysis/blob/master/Android/FrameWork/LayoutInflater%26LayoutInflaterCompat%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) | [peerless2012](https://github.com/peerless2012) | [android-cjj](https://github.com/android-cjj) | android api 23 | 2016/7/25
61 | [NestedScrolling事件机制源码解析](http://www.jianshu.com/p/6547ec3202bd) | [android-cjj](https://github.com/android-cjj) | [android-cjj](https://github.com/android-cjj/) | design-24.0.0 | 2016/7/25
62 |
63 | ### 第一期
64 |
65 | Class | 分析者 | 校对者 | 版本 | 发布时间
66 | :------------- | :------------- | :------------- | :------------- | :-------------
67 | [Binder源码解析](https://github.com/xdtianyu/SourceAnalysis/blob/master/Binder%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) | [xdtianyu](https://github.com/xdtianyu) |[xdtianyu](https://github.com/xdtianyu) |android api 23| 2016/5/8
68 | [TextView源码解析](https://github.com/7heaven/AndroidSdkSourceAnalysis/blob/master/article/textview%E6%BA%90%E7%A2%BC%E8%A7%A3%E6%9E%90.md) | [7heaven](https://github.com/7heaven) |[7heaven](https://github.com/7heaven) |android api 23| 2016/5/8
69 | [CoordinatorLayout源码解析](https://github.com/desmond1121/AndroidSdkSourceAnalysis/blob/master/article/CoordinatorLayout%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) | [Desmond Yao](https://github.com/desmond1121) |[轻微](https://github.com/zzz40500) | support-v7-23.2.1| 2016/5/8
70 | [Scroller源码解析](https://github.com/Skykai521/AndroidSdkSourceAnalysis/blob/master/article/Scroller%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) | [Skykai521](https://github.com/Skykai521) |[子墨](https://github.com/zimoguo) | android api 22 | 2016/5/8
71 | [SwipeRefreshLayout 源码解析](https://github.com/hanks-zyh/SwipeRefreshLayout/blob/master/README.md) | [hanks-zyh](https://github.com/hanks-zyh) |[android-cjj](https://github.com/android-cjj/) | support-v7-23.2.1| 2016/5/8
72 | [FloatingActionButton源码解析](https://github.com/Rowandjj/my_awesome_blog/blob/master/fab_anlysis/README.md) | [Rowandjj](https://github.com/Rowandjj) |[CaMnter](https://github.com/CaMnter) | support-v7-23.2.1| 2016/5/8
73 | [AsyncTask源码解析](https://github.com/white37/AndroidSdkSourceAnalysis/blob/master/article/AsyncTask%E5%92%8CAsyncTaskCompat%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) | [white37](https://github.com/white37) |[android-cjj](https://github.com/android-cjj/) | android api 23 | 2016/5/8
74 | [TabLayout源码解析](https://github.com/Aspsine/AndroidSdkSourceAnalysis/blob/master/article/TabLayout%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) | [Aspsine](https://github.com/Aspsine) |[android-cjj](https://github.com/android-cjj/) | design-23.2.0 | 2016/5/8
75 | [CompoundButton源码解析](https://github.com/Tikitoo/AndroidSdkSourceAnalysis/blob/master/article/CompoundButton%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) | [Tikitoo](https://github.com/Tikitoo) |[android-cjj](https://github.com/android-cjj/) | android api 23 | 2016/5/8
76 | [LinearLayout源码解析](https://github.com/razerdp/AndroidSourceAnalysis/blob/master/LinearLayout/android_widget_LinearLayout.md) | [razerdp](https://github.com/razerdp) |[android-cjj](https://github.com/android-cjj/) | support-v7-23.2.1 | 2016/5/8
77 | [SearchView源码解析](https://github.com/nukc/SearchViewAnalysis/blob/master/README.md) | [Nukc](https://github.com/nukc) |[android-cjj](https://github.com/android-cjj/) | support-v7-23.2.1 | 2016/5/7
78 | [LruCache源码解析](https://github.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/blob/master/article/LruCache%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) | [CaMnter](https://github.com/CaMnter)| [alafighting](https://github.com/alafighting)|support-v4-23.2.1 | 2016/4/24
79 | [ViewDragHelper源码解析](https://github.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/blob/master/article/ViewDragHelper%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) | [达庆凯](https://github.com/Skykai521)| [android-cjj](https://github.com/android-cjj/)|support-v4-21.0 | 2016/4/21
80 | [BottomSheets源码解析](https://github.com/android-cjj/SourceAnalysis) | [android-cjj](https://github.com/android-cjj/)| [轻微](https://github.com/zzz40500)|design-23.2.0 | 2016/4/20
81 |
82 |
83 |
84 | ## 已认领文章
85 | (写好的童鞋可以加 QQ 群:369144556做校对)
86 |
87 |
88 |
89 | Class |
90 | 认领者 |
91 |
92 |
93 |
94 |
95 | Seekbar源码解析 |
96 | JohnTsaiAndroid |
97 |
98 |
99 | ArrayMap源码解析 |
100 | audiebantzhan |
101 |
102 |
103 | SimpleArrayMap源码解析 |
104 | david-wei |
105 |
106 |
107 | ViewPager源码解析 |
108 | cpoopc |
109 |
110 |
111 | LongSparseArray源码解析 |
112 | taoliuh |
113 |
114 |
115 | Dialog源码解析 |
116 | wingjay |
117 |
118 |
119 | Frame/RelativeLayout源码解析 |
120 | wingjay |
121 |
122 |
123 | Drawable源码解析 |
124 | wingjay |
125 |
126 |
127 | AppBarLayout源码解析 |
128 | desmond1121 |
129 |
130 |
131 | ProgressBar源码解析 |
132 | carozhu |
133 |
134 |
135 | GestureDetector源码分析 |
136 | lishiwei |
137 |
138 |
139 | RecyclerView/ItemTouchHelper源码解析 |
140 | xdtianyu |
141 |
142 |
143 | Toolbar源码解析 |
144 | SeniorZhai |
145 |
146 |
147 | WebView源码解析 |
148 | markzhai |
149 |
150 |
151 | Bitmap源码解析 |
152 | zimoguo |
153 |
154 |
155 | AdapterView源码解析 |
156 | ShenghuGong |
157 |
158 |
159 | Activity源码解析 |
160 | nekocode |
161 |
162 |
163 | Camera源码解析 |
164 | gcgongchao |
165 |
166 |
167 | Volley源码解析 |
168 | THEONE10211024 |
169 |
170 |
171 | AudioPlayer源码解析 |
172 | ayyb1988 |
173 |
174 |
175 | TimePicker源码解析 |
176 | shixinzhang |
177 |
178 |
179 | Log源码解析 |
180 | lypeer |
181 |
182 |
183 | Button源码解析 |
184 | pc859107393 |
185 |
186 |
187 | Animation源码解析 |
188 | binIoter |
189 |
190 |
191 | Parcelable源码解析 |
192 | neuyu |
193 |
194 |
195 | BroadcastReceiver源码解析 |
196 | tiefeng0606 |
197 |
198 |
199 | ImageView源码解析 |
200 | 976014121 |
201 |
202 |
203 | ListView源码解析 |
204 | KingJA |
205 |
206 |
207 | Intent源码解析 |
208 | imdreamrunner |
209 |
210 |
211 | FragmentTabHost源码分析 |
212 | Tikitoo |
213 |
214 |
215 | Canvas源码解析 |
216 | heavenxue |
217 |
218 |
219 | PopupWindow源码解析 |
220 | GJson |
221 |
222 |
223 | AudioRecord源码解析 |
224 | GJson |
225 |
226 |
227 | OverScroller源码解析 |
228 | lizardmia |
229 |
230 |
231 | Context源码解析 |
232 | messishow |
233 |
234 |
235 | Actionbar/AlertController源码解析 |
236 | rickdynasty |
237 |
238 |
239 | SnackBar源码解析 |
240 | cnLGMing |
241 |
242 |
243 | LauncherActivity源码解析 |
244 | kaiyangjia |
245 |
246 |
247 | Html源码解析 |
248 | DennyCai |
249 |
250 |
251 | EditText源码解析 |
252 | johnwatsondev |
253 |
254 |
255 | TextureView源码解析 |
256 | BeEagle |
257 |
258 |
259 | DownloadManager源码解析 |
260 | xiaohongmaosimida |
261 |
262 |
263 | ImageButton源码解析 |
264 | chenbinzhou |
265 |
266 |
267 | PopupMenu源码解析 |
268 | jimmyguo |
269 |
270 |
271 | AlarmManager源码解析 |
272 | huanglongyu |
273 |
274 |
275 | Glide源码解析 |
276 | Krbit |
277 |
278 |
279 | DataBinding源码解析 |
280 | xdsjs |
281 |
282 |
283 | PreferenceActivity源码解析 |
284 | FightingLarry |
285 |
286 |
287 |
288 |
289 | ## 待认领文章
290 |
291 | **Sdk**
292 |
293 |
294 |
295 |
296 | Class |
297 | 认领状态 |
298 |
299 |
300 |
301 |
302 | ActionBar源码解析 |
303 | 未认领 |
304 |
305 |
306 | AccountManager源码解析 |
307 | 未认领 |
308 |
309 |
310 | BluetoothSocket源码解析 |
311 | 未认领 |
312 |
313 |
314 | BoringLayout源码解析 |
315 | 未认领 |
316 |
317 |
318 | DynamicLayout源码解析 |
319 | 未认领 |
320 |
321 |
322 | Paint源码解析 |
323 | 未认领 |
324 |
325 |
326 | Selector原理(Drawable源码解析) |
327 | 未认领 |
328 |
329 |
330 | Spinner源码解析 |
331 | 未认领 |
332 |
333 |
334 | TabHost源码解析 |
335 | 未认领 |
336 |
337 |
338 | TableLayout源码解析 |
339 | 未认领 |
340 |
341 |
342 |
343 |
344 | **v4**
345 |
346 |
347 |
348 |
349 | Class |
350 | 认领状态 |
351 |
352 |
353 |
354 |
355 | CircularArray源码解析 |
356 | 未认领 |
357 |
358 |
359 | CircularIntArray源码解析 |
360 | 未认领 |
361 |
362 |
363 | MapCollections源码解析 |
364 | 未认领 |
365 |
366 |
367 |
368 |
369 | **v7**
370 |
371 |
372 |
373 | Class |
374 | 认领状态 |
375 |
376 |
377 |
378 |
379 | ActionMenuView源码解析 |
380 | 未认领 |
381 |
382 |
383 | ActionBarDrawerToggle源码解析 |
384 | 未认领 |
385 |
386 |
387 | ButtonBarLayout源码解析 |
388 | 未认领 |
389 |
390 |
391 | DrawerArrowDrawable源码解析 |
392 | 未认领 |
393 |
394 |
395 | ListMenuItemView源码解析 |
396 | 未认领 |
397 |
398 |
399 | ActionMenuView源码解析 |
400 | 未认领 |
401 |
402 |
403 | WindowDecorActionBar源码解析 |
404 | 未认领 |
405 |
406 |
407 |
408 |
409 | **design**
410 |
411 |
412 |
413 |
414 | Class |
415 | 认领状态 |
416 |
417 |
418 |
419 |
420 | CollapsingToolbarLayout源码解析 |
421 | 未认领 |
422 |
423 |
424 |
425 |
426 | ### 联系方式:
427 | 源码解析群 369144556
428 |
429 | ------
430 |
431 | ## 许可协议
432 |
433 | - [署名-非商业性使用-相同方式共享 4.0 国际](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode)
434 |
435 | ------
436 |
--------------------------------------------------------------------------------
/article/LruCache源码解析.md:
--------------------------------------------------------------------------------
1 | LruCache 源码解析
2 | ==
3 |
4 | ## 1. 简介
5 |
6 | > LRU 是 Least Recently Used 最近最少使用算法。
7 |
8 | >曾经,在各大缓存图片的框架没流行的时候。有一种很常用的内存缓存技术:SoftReference 和 WeakReference(软引用和弱引用)。但是走到了 Android 2.3(Level 9)时代,垃圾回收机制更倾向于回收 SoftReference 或 WeakReference 的对象。后来,又来到了 Android3.0,图片缓存在内容中,因为不知道要在是什么时候释放内存,没有策略,没用一种可以预见的场合去将其释放。这就造成了内存溢出。
9 |
10 |
11 | ## 2. 使用方法
12 |
13 | **当成一个 Map 用就可以了,只不过实现了 LRU 缓存策略**。
14 |
15 | 使用的时候记住几点即可:
16 | - **1.(必填)**你需要提供一个缓存容量作为构造参数。
17 | - **2.(必填)** 覆写 `sizeOf` 方法 ,自定义设计一条数据放进来的容量计算,如果不覆写就无法预知数据的容量,不能保证缓存容量限定在最大容量以内。
18 | - **3.(选填)** 覆写 `entryRemoved` 方法 ,你可以知道最少使用的缓存被清除时的数据( evicted, key, oldValue, newVaule )。
19 | - **4.(记住)**LruCache是线程安全的,在内部的 get、put、remove 包括 trimToSize 都是安全的(因为都上锁了)。
20 | - **5.(选填)** 还有就是覆写 `create` 方法 。
21 |
22 | 一般做到 **1、2、3、4就足够了,5可以无视** 。
23 |
24 |
25 | 以下是 一个 **LruCache 实现 Bitmap 小缓存的案例**, `entryRemoved` 里的自定义逻辑可以无视,想看的可以去到我的我的展示 [demo](https://github.com/CaMnter/AndroidLife/blob/master/app/src/main/java/com/camnter/newlife/views/activity/lrucache/LruCacheActivity.java) 里的看自定义 `entryRemoved` 逻辑。
26 | ```java
27 | private static final float ONE_MIB = 1024 * 1024;
28 | // 7MB
29 | private static final int CACHE_SIZE = (int) (7 * ONE_MIB);
30 | private LruCache bitmapCache;
31 | this.bitmapCache = new LruCache(CACHE_SIZE) {
32 | protected int sizeOf(String key, Bitmap value) {
33 | return value.getByteCount();
34 | }
35 |
36 | @Override
37 | protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
38 | ...
39 | }
40 | };
41 | ```
42 |
43 | ## 3. 效果展示
44 |
45 | [LruCache 效果展示](https://github.com/CaMnter/AndroidLife/blob/master/article/LruCache%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90_%E6%95%88%E6%9E%9C%E5%B1%95%E7%A4%BA.md)
46 |
47 |
48 | ## 4. 源码分析
49 |
50 | ### 4.1 LruCache 原理概要解析
51 |
52 | LruCache 就是 **利用 LinkedHashMap 的一个特性( accessOrder=true 基于访问顺序 )再加上对 LinkedHashMap 的数据操作上锁实现的缓存策略**。
53 |
54 | **LruCache 的数据缓存是内存中的**。
55 |
56 | - 1.首先设置了内部 `LinkedHashMap` 构造参数 `accessOrder=true`, 实现了数据排序按照访问顺序。
57 |
58 | - 2.然后在每次 `LruCache.get(K key)` 方法里都会调用 `LinkedHashMap.get(Object key)`。
59 |
60 | - 3.如上述设置了 `accessOrder=true` 后,每次 `LinkedHashMap.get(Object key)` 都会进行 `LinkedHashMap.makeTail(LinkedEntry e)`。
61 |
62 | - 4.`LinkedHashMap` 是双向循环链表,然后每次 `LruCache.get` -> `LinkedHashMap.get` 的数据就被放到最末尾了。
63 |
64 | - 5.在 `put` 和 `trimToSize` 的方法执行下,如果发生数据量移除,会优先移除掉最前面的数据(因为最新访问的数据在尾部)。
65 |
66 | **具体解析在:** *4.2*、*4.3*、*4.4*、*4.5* 。
67 |
68 |
69 | ### 4.2 LruCache 的唯一构造方法
70 | ```java
71 | /**
72 | * LruCache的构造方法:需要传入最大缓存个数
73 | */
74 | public LruCache(int maxSize) {
75 |
76 | ...
77 |
78 | this.maxSize = maxSize;
79 | /*
80 | * 初始化LinkedHashMap
81 | * 第一个参数:initialCapacity,初始大小
82 | * 第二个参数:loadFactor,负载因子=0.75f
83 | * 第三个参数:accessOrder=true,基于访问顺序;accessOrder=false,基于插入顺序
84 | */
85 | this.map = new LinkedHashMap(0, 0.75f, true);
86 | }
87 | ```
88 | 第一个参数 `initialCapacity` 用于初始化该 LinkedHashMap 的大小。
89 |
90 | 先简单介绍一下 第二个参数 `loadFactor`,这个其实的 HashMap 里的构造参数,涉及到**扩容问题**,比如 HashMap 的最大容量是100,那么这里设置0.75f的话,到75容量的时候就会扩容。
91 |
92 | 主要是第三个参数 `accessOrder=true` ,**这样的话 LinkedHashMap 数据排序就会基于数据的访问顺序,从而实现了 LruCache 核心工作原理**。
93 |
94 | ### 4.3 LruCache.get(K key)
95 | ```java
96 | /**
97 | * 根据 key 查询缓存,如果存在于缓存或者被 create 方法创建了。
98 | * 如果值返回了,那么它将被移动到双向循环链表的的尾部。
99 | * 如果如果没有缓存的值,则返回 null。
100 | */
101 | public final V get(K key) {
102 |
103 | ...
104 |
105 | V mapValue;
106 | synchronized (this) {
107 | // 关键点:LinkedHashMap每次get都会基于访问顺序来重整数据顺序
108 | mapValue = map.get(key);
109 | // 计算 命中次数
110 | if (mapValue != null) {
111 | hitCount++;
112 | return mapValue;
113 | }
114 | // 计算 丢失次数
115 | missCount++;
116 | }
117 |
118 | /*
119 | * 官方解释:
120 | * 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时
121 | * 候,一个冲突的值被添加到Map,我们在Map中删除这个值,释放被创造的值。
122 | */
123 | V createdValue = create(key);
124 | if (createdValue == null) {
125 | return null;
126 | }
127 |
128 | /***************************
129 | * 不覆写create方法走不到下面 *
130 | ***************************/
131 |
132 | /*
133 | * 正常情况走不到这里
134 | * 走到这里的话 说明 实现了自定义的 create(K key) 逻辑
135 | * 因为默认的 create(K key) 逻辑为null
136 | */
137 | synchronized (this) {
138 | // 记录 create 的次数
139 | createCount++;
140 | // 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值
141 | mapValue = map.put(key, createdValue);
142 |
143 | // 如果之前存在相同key的value,即有冲突。
144 | if (mapValue != null) {
145 | /*
146 | * 有冲突
147 | * 所以 撤销 刚才的 操作
148 | * 将 之前相同key 的值 重新放回去
149 | */
150 | map.put(key, mapValue);
151 | } else {
152 | // 拿到键值对,计算出在容量中的相对长度,然后加上
153 | size += safeSizeOf(key, createdValue);
154 | }
155 | }
156 |
157 | // 如果上面 判断出了 将要放入的值发生冲突
158 | if (mapValue != null) {
159 | /*
160 | * 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
161 | * 告诉 自定义 的 entryRemoved 方法
162 | */
163 | entryRemoved(false, key, createdValue, mapValue);
164 | return mapValue;
165 | } else {
166 | // 上面 进行了 size += 操作 所以这里要重整长度
167 | trimToSize(maxSize);
168 | return createdValue;
169 | }
170 | }
171 | ```
172 | 上述的 `get` 方法表面并没有看出哪里有实现了 LRU 的缓存策略。主要在 `mapValue = map.get(key)`;里,**调用了 LinkedHashMap 的 get 方法,再加上 LruCache 构造里默认设置 LinkedHashMap 的 accessOrder=true**。
173 |
174 |
175 | ### 4.4 LinkedHashMap.get(Object key)
176 | ```java
177 | /**
178 | * Returns the value of the mapping with the specified key.
179 | *
180 | * @param key
181 | * the key.
182 | * @return the value of the mapping with the specified key, or {@code null}
183 | * if no mapping for the specified key is found.
184 | */
185 | @Override public V get(Object key) {
186 | /*
187 | * This method is overridden to eliminate the need for a polymorphic
188 | * invocation in superclass at the expense of code duplication.
189 | */
190 | if (key == null) {
191 | HashMapEntry e = entryForNullKey;
192 | if (e == null)
193 | return null;
194 | if (accessOrder)
195 | makeTail((LinkedEntry) e);
196 | return e.value;
197 | }
198 |
199 | int hash = Collections.secondaryHash(key);
200 | HashMapEntry[] tab = table;
201 | for (HashMapEntry e = tab[hash & (tab.length - 1)];
202 | e != null; e = e.next) {
203 | K eKey = e.key;
204 | if (eKey == key || (e.hash == hash && key.equals(eKey))) {
205 | if (accessOrder)
206 | makeTail((LinkedEntry) e);
207 | return e.value;
208 | }
209 | }
210 | return null;
211 | }
212 | ```
213 | 其实仔细看 `if (accessOrder)` 的逻辑即可,如果 `accessOrder=true` 那么每次 `get` 都会执行 N 次 `makeTail(LinkedEntry e)` 。
214 |
215 | 接下来看看:
216 |
217 | ### 4.5 LinkedHashMap.makeTail(LinkedEntry e)
218 | ```java
219 | /**
220 | * Relinks the given entry to the tail of the list. Under access ordering,
221 | * this method is invoked whenever the value of a pre-existing entry is
222 | * read by Map.get or modified by Map.put.
223 | */
224 | private void makeTail(LinkedEntry e) {
225 | // Unlink e
226 | e.prv.nxt = e.nxt;
227 | e.nxt.prv = e.prv;
228 |
229 | // Relink e as tail
230 | LinkedEntry header = this.header;
231 | LinkedEntry oldTail = header.prv;
232 | e.nxt = header;
233 | e.prv = oldTail;
234 | oldTail.nxt = header.prv = e;
235 | modCount++;
236 | }
237 | ```
238 |
239 | *// Unlink e*
240 |
241 |
242 | *// Relink e as tail*
243 |
244 |
245 | LinkedHashMap 是双向循环链表,然后此次 **LruCache.get -> LinkedHashMap.get** 的数据就被放到最末尾了。
246 |
247 | **以上就是 LruCache 核心工作原理**。
248 |
249 | ---
250 |
251 | 接下来介绍 **LruCache 的容量溢出策略**。
252 |
253 |
254 |
255 | ### 4.6 LruCache.put(K key, V value)
256 | ```java
257 | public final V put(K key, V value) {
258 | ...
259 | synchronized (this) {
260 | ...
261 | // 拿到键值对,计算出在容量中的相对长度,然后加上
262 | size += safeSizeOf(key, value);
263 | ...
264 | }
265 | ...
266 | trimToSize(maxSize);
267 | return previous;
268 | }
269 | ```
270 | 记住几点:
271 | - **1.**put 开始的时候确实是把值放入 LinkedHashMap 了,**不管超不超过你设定的缓存容量**。
272 | - **2.**然后根据 `safeSizeOf` 方法计算 此次添加数据的容量是多少,并且加到 `size` 里 。
273 | - **3.**说到 `safeSizeOf` 就要讲到 `sizeOf(K key, V value)` 会计算出此次添加数据的大小 。
274 | - **4.**直到 put 要结束时,进行了 `trimToSize` 才判断 `size` 是否 大于 `maxSize` 然后进行最近很少访问数据的移除。
275 |
276 | ### 4.7 LruCache.trimToSize(int maxSize)
277 | ```java
278 | public void trimToSize(int maxSize) {
279 | /*
280 | * 这是一个死循环,
281 | * 1.只有 扩容 的情况下能立即跳出
282 | * 2.非扩容的情况下,map的数据会一个一个删除,直到map里没有值了,就会跳出
283 | */
284 | while (true) {
285 | K key;
286 | V value;
287 | synchronized (this) {
288 | // 在重新调整容量大小前,本身容量就为空的话,会出异常的。
289 | if (size < 0 || (map.isEmpty() && size != 0)) {
290 | throw new IllegalStateException(
291 | getClass().getName() + ".sizeOf() is reporting inconsistent results!");
292 | }
293 | // 如果是 扩容 或者 map为空了,就会中断,因为扩容不会涉及到丢弃数据的情况
294 | if (size <= maxSize || map.isEmpty()) {
295 | break;
296 | }
297 |
298 | Map.Entry toEvict = map.entrySet().iterator().next();
299 | key = toEvict.getKey();
300 | value = toEvict.getValue();
301 | map.remove(key);
302 | // 拿到键值对,计算出在容量中的相对长度,然后减去。
303 | size -= safeSizeOf(key, value);
304 | // 添加一次收回次数
305 | evictionCount++;
306 | }
307 | /*
308 | * 将最后一次删除的最少访问数据回调出去
309 | */
310 | entryRemoved(true, key, value, null);
311 | }
312 | }
313 | ```
314 | 简单描述:会判断之前 `size` 是否大于 `maxSize` 。是的话,直接跳出后什么也不做。不是的话,证明已经溢出容量了。由 `makeTail` 图已知,最近经常访问的数据在最末尾。拿到一个存放 key 的 Set,然后一直一直从头开始删除,删一个判断是否溢出,直到没有溢出。
315 |
316 | ---
317 |
318 | 最后看看:
319 |
320 | ### 4.8 覆写 entryRemoved 的作用
321 |
322 | entryRemoved被LruCache调用的场景:
323 | - **1.(put)** put 发生 key 冲突时被调用,**evicted=false,key=此次 put 的 key,oldValue=被覆盖的冲突 value,newValue=此次 put 的 value**。
324 | - **2.(trimToSize)** trimToSize 的时候,只会被调用一次,就是最后一次被删除的最少访问数据带回来。**evicted=true,key=最后一次被删除的 key,oldValue=最后一次被删除的 value,newValue=null(此次没有冲突,只是 remove)**。
325 | - **3.(remove)** remove的时候,存在对应 key,并且被成功删除后被调用。**evicted=false,key=此次 put的 key,oldValue=此次删除的 value,newValue=null(此次没有冲突,只是 remove)**。
326 | - **4.(get后半段,查询丢失后处理情景,不过建议忽略)** get 的时候,正常的话不实现自定义 `create` 的话,代码上看 get 方法只会走一半,如果你实现了自定义的 `create(K key)` 方法,并且在 你 create 后的值放入 LruCache 中发生 key 冲突时被调用,**evicted=false,key=此次 get 的 key,oldValue=被你自定义 create(key)后的 value,newValue=原本存在 map 里的 key-value**。
327 |
328 | 解释一下第四点吧:**<1>.**第四点是这样的,先 get(key),然后没拿到,丢失。**<2>.**如果你提供了 自定义的 `create(key)` 方法,那么 LruCache 会根据你的逻辑自造一个 value,但是当放入的时候发现冲突了,但是已经放入了。**<3>.**此时,会将那个冲突的值再让回去覆盖,此时调用上述4.的 entryRemoved。
329 |
330 | 因为 HashMap 在数据量大情况下,拿数据可能造成丢失,导致前半段查不到,你自定义的 `create(key)` 放入的时候发现又查到了**(有冲突)**。然后又急忙把原来的值放回去,此时你就白白create一趟,无所作为,还要走一遍entryRemoved。
331 |
332 |
333 | 综上就如同注释写的一样:
334 | ```java
335 | /**
336 | * 1.当被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用
337 | * 或者替换条目值时put调用,默认实现什么都没做。
338 | * 2.该方法没用同步调用,如果其他线程访问缓存时,该方法也会执行。
339 | * 3.evicted=true:如果该条目被删除空间 (表示 进行了trimToSize or remove) evicted=false:put冲突后 或 get里成功create后
340 | * 导致
341 | * 4.newValue!=null,那么则被put()或get()调用。
342 | */
343 | protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
344 | }
345 | ```
346 | 可以参考我的 [demo](https://github.com/CaMnter/AndroidLife/blob/master/app/src/main/java/com/camnter/newlife/views/activity/lrucache/LruCacheActivity.java) 里的 `entryRemoved` 。
347 |
348 | ### 4.9 LruCache 局部同步锁
349 |
350 | 在 `get`, `put`, `trimToSize`, `remove` 四个方法里的 `entryRemoved` 方法都不在同步块里。因为 `entryRemoved` 回调的参数都属于方法域参数,不会线程不安全。
351 |
352 | > 本地方法栈和程序计数器是线程隔离的数据区
353 |
354 |
355 |
356 |
357 | ## 5. 开源项目中的使用
358 |
359 | [square/picasso](https://github.com/square/picasso)
360 |
361 |
362 | ## 6. 总结
363 |
364 | LruCache重要的几点:
365 |
366 | - **1.**LruCache 是通过 LinkedHashMap 构造方法的第三个参数的 `accessOrder=true` 实现了 `LinkedHashMap` 的数据排序**基于访问顺序** (最近访问的数据会在链表尾部),在容量溢出的时候,将链表头部的数据移除。从而,实现了 LRU 数据缓存机制。
367 |
368 | - **2.**LruCache 在内部的get、put、remove包括 trimToSize 都是安全的(因为都上锁了)。
369 |
370 | - **3.**LruCache 自身并没有释放内存,将 LinkedHashMap 的数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存。
371 |
372 | - **4.**覆写 `entryRemoved` 方法能知道 LruCache 数据移除是是否发生了冲突,也可以去手动释放资源。
373 |
374 | - **5.**`maxSize` 和 `sizeOf(K key, V value)` 方法的覆写息息相关,必须相同单位。( 比如 maxSize 是7MB,自定义的 sizeOf 计算每个数据大小的时候必须能算出与MB之间有联系的单位 )
375 |
376 |
377 |
378 |
379 | ## 7. 资源
380 |
381 | [LruCacheActivity](https://github.com/CaMnter/AndroidLife/blob/master/app/src/main/java/com/camnter/newlife/views/activity/lrucache/LruCacheActivity.java)
382 |
383 |
384 | [LruCache 注释源码](https://github.com/CaMnter/AndroidLife/blob/master/app/src/main/java/com/camnter/newlife/utils/cache/LruCache.java)
385 |
--------------------------------------------------------------------------------
/article/NavigationView源码解析.md:
--------------------------------------------------------------------------------
1 | # NavigationView源码解析
2 |
3 | >分析版本`com.android.support:design:23.1.0`
4 |
5 |
6 | ## 一、概述
7 |
8 | `NavigationView`属于[`android_design_supprot_library`](http://android-developers.blogspot.com/2015/05/android-design-support-library.html)库的控件,主要是为了帮助大家去更加的方便的实现`material design`风格的app。
9 |
10 | 为了更好去对源码分析就行理解,首先我们这里简单贴一下用法:
11 |
12 | 其效果一般为:
13 |
14 |
15 |
16 | 用法也非常简单,只需要在布局文件中声明即可:
17 |
18 | ```xml
19 |
25 |
26 |
27 |
28 |
34 |
35 | ```
36 |
37 | 其一般和`DrawerLayout`配合使用,作为左侧的菜单布局,这里我们主要关注`NavigationView`的声明。
38 |
39 | 对应效果图和布局文件的声明,可以看出其布局主要分为header和menu两部分,在布局文件的声明中也可以得到体现,header对应一个布局文库,menu则对应一个菜单的xml文件。
40 |
41 | ok,简单回顾完成用法以后,我们在分析源码前,先考虑一下,对于这样的UI效果,如果没有`NavigationView `,我们如何去做,很简单的,我们想到使用:
42 |
43 | * ListView去实现`NavigationView `所对应的效果
44 |
45 | 的确是可以的,header作为addHeaderView,menu则对应item数据项即可。
46 |
47 | 当然既然能用`ListView`,那么肯定也可以使用`RecyclerView`,是的,为了更好的去理解源码分析,这里需要指出
48 |
49 | * 其实NavigationView内部就是通过`RecyclerView `实现的(针对目前分析版本)
50 |
51 | 在早一点的版本,例如`22.2.0`,其内部是ListView实现的。
52 |
53 | ## 二、源码分析
54 |
55 | ###(1)寻找RecyclerView
56 |
57 | 那么,既然我们已经清楚`NavigationView `内部其实就是`RecyclerView`实现了,那么接下来看源码的过程,就可以有针对有目的的去阅读了。
58 |
59 | 首先看`NavigationView`的构造方法:
60 |
61 | ```java
62 | #NavigationView
63 | public class NavigationView extends ScrimInsetsFrameLayout {
64 | private final NavigationMenu mMenu;
65 | private final NavigationMenuPresenter mPresenter = new NavigationMenuPresenter();
66 |
67 | private OnNavigationItemSelectedListener mListener;
68 | private int mMaxWidth;
69 |
70 | private MenuInflater mMenuInflater;
71 |
72 |
73 | public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
74 | super(context, attrs, defStyleAttr);
75 |
76 | ThemeUtils.checkAppCompatTheme(context);
77 |
78 | // Create the menu
79 | mMenu = new NavigationMenu(context);
80 |
81 | //省略了获取部分自定义属性的代码
82 |
83 | mMenu.setCallback(new MenuBuilder.Callback() {
84 | @Override
85 | public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
86 | return mListener != null && mListener.onNavigationItemSelected(item);
87 | }
88 |
89 | @Override
90 | public void onMenuModeChange(MenuBuilder menu) {}
91 | });
92 | mPresenter.setId(PRESENTER_NAVIGATION_VIEW_ID);
93 | mPresenter.initForMenu(context, mMenu);
94 | mPresenter.setItemIconTintList(itemIconTint);
95 | if (textAppearanceSet) {
96 | mPresenter.setItemTextAppearance(textAppearance);
97 | }
98 | mPresenter.setItemTextColor(itemTextColor);
99 | mPresenter.setItemBackground(itemBackground);
100 | mMenu.addMenuPresenter(mPresenter);
101 | addView((View) mPresenter.getMenuView(this));
102 |
103 | if (a.hasValue(R.styleable.NavigationView_menu)) {
104 | inflateMenu(a.getResourceId(R.styleable.NavigationView_menu, 0));
105 | }
106 |
107 | if (a.hasValue(R.styleable.NavigationView_headerLayout)) {
108 | inflateHeaderView(a.getResourceId(R.styleable.NavigationView_headerLayout, 0));
109 | }
110 |
111 | a.recycle();
112 | }
113 | ```
114 |
115 | `NavigationView `是继承自FrameLayout的,首先我们看起构造方法,因为我们刚才已经预先说明其内部是`RecyclerView`实现的,那么我们首先定位在哪里加入的`RecyclerView`.
116 |
117 | 关注到上述代码的39行:
118 |
119 | ```java
120 | addView((View) mPresenter.getMenuView(this));
121 | ```
122 |
123 | 这里添加了一个view,调用是`mPresenter.getMenuView `,源码更进去,可以看到
124 |
125 | ```java
126 | #NavigationMenuPresenter
127 | @Override
128 | public MenuView getMenuView(ViewGroup root) {
129 | if (mMenuView == null) {
130 | mMenuView = (NavigationMenuView) mLayoutInflater.inflate(
131 | R.layout.design_navigation_menu, root, false);
132 | if (mAdapter == null) {
133 | mAdapter = new NavigationMenuAdapter();
134 | }
135 | mHeaderLayout = (LinearLayout) mLayoutInflater
136 | .inflate(R.layout.design_navigation_item_header,
137 | mMenuView, false);
138 | mMenuView.setAdapter(mAdapter);
139 | }
140 | return mMenuView;
141 | }
142 | ```
143 |
144 | 这个方法返回值是`NavigationMenuView `,而这个类实际上继承自`RecyclerView`。除此以外呢,可以看到这里还初始化了`NavigationMenuAdapter `,并且调用了`setAdapter()`;以及初始化了`mHeaderLayout `。顾名思义,adapter肯定是为`RecyclerView `准备的,而`mHeaderLayout `肯定是用于放置我们设置的`app:headerLayout`.
145 |
146 | ###(2)数据源的初始化
147 |
148 | 到这里我们已经确定了`NavigationView`是个FrameLayout,其内部放置了一个`RecyclerView`,根据我们的使用情况,并不需要去单独的设置item数据,只需要使用属性`app:menu="@menu/drawer"`,所以,RecylcerView对应的Adapter所需要的数据源,肯定也是在构造方法中获取的。
149 |
150 | 简单看一眼构造中的代码,找到如下几行:
151 |
152 | ```java
153 | #NavigationView
154 | if (a.hasValue(R.styleable.NavigationView_menu)) {
155 | inflateMenu(a.getResourceId(R.styleable.NavigationView_menu, 0));
156 | }
157 |
158 | public void inflateMenu(int resId) {
159 | getMenuInflater().inflate(resId, mMenu);
160 | mPresenter.updateMenuView(false);
161 | }
162 | ```
163 |
164 | 可以看到呢,对我们的menu读取后,调用的是`getMenuInflater().inflate`,通过该方法呢,会完成对我们资源文件中定义的menuItem读取值mMenu中。读取完成以后呢,调用了`mPresenter.updateMenuView `,该方法会间接的为`Adapter`设置值以及调用`notifyDataSetChanged`,具体代码如下:
165 |
166 | ```java
167 | #NavigationMenuPresenter
168 | @Override
169 | public void updateMenuView(boolean cleared) {
170 | if (mAdapter != null) {
171 | mAdapter.update();
172 | }
173 | }
174 |
175 | #NavigationMenuPresenter.NavigationMenuAdapter
176 | public void update() {
177 | prepareMenuItems();
178 | notifyDataSetChanged();
179 | }
180 | ```
181 | 第一个方法肯定是准备数据,第二个方法通知更新了。对于数据的准备呢,我们需要去了解下,因为`NavigationView`中的item并不是一样的,涉及到多种类型。
182 |
183 | ```java
184 | #NavigationMenuPresenter.NavigationMenuAdapter
185 | private void prepareMenuItems() {
186 |
187 | mItems.clear();
188 | mItems.add(new NavigationMenuHeaderItem());
189 |
190 | int currentGroupId = -1;
191 | int currentGroupStart = 0;
192 | boolean currentGroupHasIcon = false;
193 | for (int i = 0, totalSize = mMenu.getVisibleItems().size(); i < totalSize; i++) {
194 | MenuItemImpl item = mMenu.getVisibleItems().get(i);
195 | //..省略几行代码
196 | if (item.hasSubMenu()) {
197 | SubMenu subMenu = item.getSubMenu();
198 | if (subMenu.hasVisibleItems()) {
199 | if (i != 0) {
200 | mItems.add(new NavigationMenuSeparatorItem(mPaddingSeparator, 0));
201 | }
202 | mItems.add(new NavigationMenuTextItem(item));
203 | boolean subMenuHasIcon = false;
204 | int subMenuStart = mItems.size();
205 | for (int j = 0, size = subMenu.size(); j < size; j++) {
206 | MenuItemImpl subMenuItem = (MenuItemImpl) subMenu.getItem(j);
207 | //..
208 | mItems.add(new NavigationMenuTextItem(subMenuItem));
209 | }
210 | }
211 | } else {
212 | int groupId = item.getGroupId();
213 | if (groupId != currentGroupId) { // first item in group
214 | if (i != 0) {
215 | currentGroupStart++;
216 | mItems.add(new NavigationMenuSeparatorItem(
217 | mPaddingSeparator, mPaddingSeparator));
218 | }
219 | }
220 | mItems.add(new NavigationMenuTextItem(item));
221 | currentGroupId = groupId;
222 | }
223 | }
224 | }
225 | ```
226 | 上面代码蛮长的,主要关注什么呢?很明显mItems是Adapter对应的数据源,那么我们关注的应该就是`mItems.add()`方法。
227 |
228 | 首先加了个`NavigationMenuHeaderItem`,我们都知道RecylerView本身并没有addHeaderView方法,看来对于headLayout也是通过多种item type实现的。
229 |
230 |
231 | 接下来是遍历`mMenu.getVisibleItems()`,那么我们按顺序看代码吧,首先判断的是:
232 |
233 |
234 | * item.hasSubMenu()
235 |
236 | 如果包含subMenu,则调用:
237 |
238 | ```java
239 | mItems.add(new NavigationMenuTextItem(item));
240 | ```
241 |
242 | 如果当前并非是第一个,则还需要添加一个分隔符(NavigationMenuSeparatorItem)
243 |
244 | ```java
245 | mItems.add(new NavigationMenuSeparatorItem(mPaddingSeparator, 0));
246 | ```
247 | 再往下就是遍历所有的subMenu了,也很简单,直接添加`NavigationMenuTextItem`
248 |
249 | ```java
250 | mItems.add(new NavigationMenuTextItem(subMenuItem));
251 | ```
252 |
253 | * else分支(即不包含subMenu)
254 |
255 | 首先判断是否是group的第一个item,如果是的话,需要额外添加一个分隔符(`NavigationMenuSeparatorItem`),否则的话直接添加一个`NavigationMenuTextItem`。
256 |
257 | 好了,这样的话,我们就分析完了,可以看到上面呢,一共包含几种MenuItem呢?
258 |
259 | * NavigationMenuHeaderItem 对应HeaderLayout
260 | * NavigationMenuTextItem 对应于一般的Item
261 | * NavigationMenuSeparatorItem 对应分隔符
262 |
263 | 从源码上看只有上述3中,那么我们看一个包含多种menu item的效果图:
264 |
265 |
266 |
267 | 简单数一下,怎么好像是4种,恩,其实上图2,4都是`NavigationMenuTextItem `,只不过4中传入的item的`hasSubMenu=true`.
268 |
269 | 这样的话,我们就分析完成了数据源的初始化。
270 |
271 | 那么Adapter有了数据源,并且调用了`notifyDataSetChanged`,接下来应该看的代码就是Adapter内部的`onCreateViewHolder和onCreateViewHolder`等代码了。
272 |
273 | ###(3)NavigationMenuAdapter
274 |
275 | 因为我们涉及到多个item type,所以重点看三个方法,分别为:`getItemViewType`,`onCreateViewHolder`,`onBindViewHolder`。
276 |
277 | * getItemViewType
278 |
279 | ```java
280 | #NavigationMenuPresenter.NavigationMenuAdapter
281 | @Override
282 | public int getItemViewType(int position) {
283 | NavigationMenuItem item = mItems.get(position);
284 | if (item instanceof NavigationMenuSeparatorItem) {
285 | return VIEW_TYPE_SEPARATOR;
286 | } else if (item instanceof NavigationMenuHeaderItem) {
287 | return VIEW_TYPE_HEADER;
288 | } else if (item instanceof NavigationMenuTextItem) {
289 | NavigationMenuTextItem textItem = (NavigationMenuTextItem) item;
290 | if (textItem.getMenuItem().hasSubMenu()) {
291 | return VIEW_TYPE_SUBHEADER;
292 | } else {
293 | return VIEW_TYPE_NORMAL;
294 | }
295 | }
296 | throw new RuntimeException("Unknown item type.");
297 | }
298 | ```
299 | 根据item的类型去决定ItemViewType,那么我们上面已经进行了详细的分析,一种有3种item类型,其中`NavigationMenuTextItem `分为`hasSubMenu() `为true,false两种情况。刚好对应上述代码。
300 |
301 | * onCreateViewHolder
302 |
303 | ```java
304 | #NavigationMenuPresenter.NavigationMenuAdapter
305 | @Override
306 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
307 | switch (viewType) {
308 | case VIEW_TYPE_NORMAL:
309 | return new NormalViewHolder(mLayoutInflater, parent, mOnClickListener);
310 | case VIEW_TYPE_SUBHEADER:
311 | return new SubheaderViewHolder(mLayoutInflater, parent);
312 | case VIEW_TYPE_SEPARATOR:
313 | return new SeparatorViewHolder(mLayoutInflater, parent);
314 | case VIEW_TYPE_HEADER:
315 | return new HeaderViewHolder(mHeaderLayout);
316 | }
317 | return null;
318 | }
319 | ```
320 |
321 | 根据不同的itemViewType,返回不同的ViewHolder,每个viewholder也对应一个布局文件,分别为:
322 |
323 | * SubheaderViewHolder 对应的布局文件为一个TextView
324 | * SeparatorViewHolder 对应一个FrameLayout,其内部是`height=1dp`的View
325 | * HeaderViewHolder 对应mHeaderLayout,其实就是LinearLayout包裹我们设置的headerLayout
326 | * NormalViewHolder 这个对应于一个`NavigationMenuItemView`
327 |
328 |
329 | 那么大概知道每个itemViewType对应的布局文件之后,就可以看onBindViewHolder了
330 |
331 | * onBindViewHolder
332 |
333 | ```java
334 | @Override
335 | #NavigationMenuPresenter.NavigationMenuAdapter
336 | public void onBindViewHolder(ViewHolder holder, int position) {
337 | switch (getItemViewType(position)) {
338 | case VIEW_TYPE_NORMAL: {
339 | NavigationMenuItemView itemView = (NavigationMenuItemView) holder.itemView;
340 | itemView.setIconTintList(mIconTintList);
341 | if (mTextAppearanceSet) {
342 | itemView.setTextAppearance(itemView.getContext(), mTextAppearance);
343 | }
344 | if (mTextColor != null) {
345 | itemView.setTextColor(mTextColor);
346 | }
347 | itemView.setBackgroundDrawable(mItemBackground != null ?
348 | mItemBackground.getConstantState().newDrawable() : null);
349 | NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position);
350 | itemView.initialize(item.getMenuItem(), 0);
351 | break;
352 | }
353 | case VIEW_TYPE_SUBHEADER: {
354 | TextView subHeader = (TextView) holder.itemView;
355 | NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position);
356 | subHeader.setText(item.getMenuItem().getTitle());
357 | break;
358 | }
359 | case VIEW_TYPE_SEPARATOR: {
360 | NavigationMenuSeparatorItem item =
361 | (NavigationMenuSeparatorItem) mItems.get(position);
362 | holder.itemView.setPadding(0, item.getPaddingTop(), 0,
363 | item.getPaddingBottom());
364 | break;
365 | }
366 | case VIEW_TYPE_HEADER: {
367 | break;
368 | }
369 | }
370 |
371 | }
372 | ```
373 |
374 | 恩,这里主要就是为4中itemTypeView所对应的item进行控件的赋值了,从简单到难来看:
375 |
376 | * `VIEW_TYPE_HEADER` 什么都不管,只要把我们设置的headerLayout显示即可
377 | * `VIEW_TYPE_SEPARATOR`首先拿到`NavigationMenuSeparatorItem `,主要是为了拿到padding的值,然后调用`holder.itemView`设置上下的padding,这个padding默认值为8dp。
378 | * `VIEW_TYPE_SUBHEADER`拿到TextView设置下title即可。
379 | * `VIEW_TYPE_NORMAL `
380 |
381 | 最后这个呢,对应`NavigationMenuItemView`,通过它呢去设置字体,图标,字体颜色,图标颜色,以及根据`item.getMenuItem`设置各种状态。
382 |
383 | 那么到这里呢,我们就学习完成了`NavigationMenuAdapter `它的内部的处理,那么我们的`NavigationView`已经能够正常的显示了。
384 |
385 | 显示完了之后,还有个问题,`NavigationView`的Item是可以点击了,如果大家有印象的话,`RecyclerView`自身是没有提供Item点击的回调的,那么`NavigationView`是如何做的。
386 |
387 | ###(3)Item点击[onNavigationItemSelected]
388 |
389 | 对于接受Item点击,只有`VIEW_TYPE_NORMAL`的itemViewType才可以,那么我们回顾下`onCreateViewHolder `方法
390 |
391 | ```java
392 | #NavigationMenuPresenter.NavigationMenuAdapter
393 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
394 | switch (viewType) {
395 | case VIEW_TYPE_NORMAL:
396 | return new NormalViewHolder(mLayoutInflater, parent, mOnClickListener);
397 | //...
398 | return null ;
399 | }
400 | ```
401 | 注意当为`VIEW_TYPE_NORMAL `的时候创建的ViewHolder传入了一个`mOnClickListener `.
402 |
403 | ```java
404 | #NavigationMenuPresenter.NavigationMenuAdapter
405 | private static class NormalViewHolder extends ViewHolder {
406 |
407 | public NormalViewHolder(LayoutInflater inflater, ViewGroup parent,
408 | View.OnClickListener listener) {
409 | super(inflater.inflate(R.layout.design_navigation_item, parent, false));
410 | itemView.setOnClickListener(listener);
411 | }
412 |
413 | }
414 | ```
415 |
416 | 可以看到,直接调用了`itemView.setOnClickListener `。那么我们看下这个listener
417 |
418 | ```java
419 | #NavigationMenuPresenter
420 | private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
421 |
422 | @Override
423 | public void onClick(View v) {
424 | NavigationMenuItemView itemView = (NavigationMenuItemView) v;
425 | MenuItemImpl item = itemView.getItemData();
426 | boolean result = mMenu.performItemAction(item, NavigationMenuPresenter.this, 0);
427 | if (item != null && item.isCheckable() && result) {
428 | mAdapter.setCheckedItem(item);
429 | }
430 | updateMenuView(false);
431 | }
432 | };
433 | ```
434 |
435 | 代码不多,经过扫描,应该关注`mMenu.performItemAction`方法:
436 |
437 | ```java
438 | #MenuBuilder
439 | public boolean performItemAction(MenuItem item, MenuPresenter preferredPresenter, int flags) {
440 | MenuItemImpl itemImpl = (MenuItemImpl) item;
441 | //省略了一些代码
442 | boolean invoked = itemImpl.invoke();
443 | return invoked;
444 | }
445 | ```
446 | 继续跟进去
447 |
448 | ```java
449 | #MenuItemImpl
450 | public boolean invoke() {
451 | //省略了一些代码
452 | if (mMenu.dispatchMenuItemSelected(mMenu.getRootMenu(), this)) {
453 | return true;
454 | }
455 | return false;
456 | }
457 | ```
458 |
459 | 那么主要是`mMenu.dispatchMenuItemSelected `了
460 |
461 | ```java
462 | #MenuBuilder
463 | boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) {
464 | return mCallback != null && mCallback.onMenuItemSelected(menu, item);
465 | }
466 | ```
467 | 终于看到mCallback了,回顾下,在onCreateViewHolder的时候,给我们的itemView添加了clicklistener,当我们点击的时候会辗转调用至`mCallback.onMenuItemSelected(menu, item);`。
468 |
469 | 而这个`mCallback`正是在`NavigationView`构造方法中设置的。
470 |
471 | ```java
472 | #NavigationView
473 | mMenu.setCallback(new MenuBuilder.Callback() {
474 | @Override
475 | public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
476 | return mListener != null && mListener.onNavigationItemSelected(item);
477 | }
478 |
479 | @Override
480 | public void onMenuModeChange(MenuBuilder menu) {}
481 | });
482 | ```
483 |
484 | ok,到这我们就理清楚了onMenuItemSelected的回调。
485 |
486 | 还有个问题,当我们点击的时候,如果添加监听,并且在`onMenuItemSelected `直接`return true`,我们的选中的Item会被高亮显示。
487 |
488 | 这个其实也在刚才的`mOnClickListener `里面
489 |
490 |
491 | ```java
492 | private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
493 |
494 | @Override
495 | public void onClick(View v) {
496 | NavigationMenuItemView itemView = (NavigationMenuItemView) v;
497 | MenuItemImpl item = itemView.getItemData();
498 | boolean result = mMenu.performItemAction(item, NavigationMenuPresenter.this, 0);
499 | if (item != null && item.isCheckable() && result) {
500 | mAdapter.setCheckedItem(item);
501 | }
502 | updateMenuView(false);
503 | }
504 | };
505 | ```
506 |
507 | 当我们回调返回true,也就意味着`result = true`,那么正常情况下(`checkable=true`)会调用` mAdapter.setCheckedItem(item);`
508 |
509 | ```java
510 | #NavigationMenuAdapter
511 | public void setCheckedItem(MenuItemImpl checkedItem) {
512 | if (mCheckedItem == checkedItem || !checkedItem.isCheckable()) {
513 | return;
514 | }
515 | if (mCheckedItem != null) {
516 | mCheckedItem.setChecked(false);
517 | }
518 | mCheckedItem = checkedItem;
519 | checkedItem.setChecked(true);
520 | }
521 | ```
522 | 在这里的对上一次选择的item调用了`setChecked(false);`对本次的选中的item调用了`setChecked(true);`从而得到状态切换的效果。
523 |
524 | 如下图:
525 |
526 |
527 |
528 | ps:早起的版本中,对于`setCheckedItem `的操作,是需要我们自行处理的,建议对于desgin库,尽可能使用最新的版本。
529 |
530 | 到这里,我们的整个分析就结束了,整个分析从寻找RecylerView,数据源的初始化与设置,Item点击的设置3个点进行展开,源码并不复杂,希望大家可以通过本文加深对`NavigationView`的理解,从而进一步加深对`RecyclerView`用法的理解。
531 |
--------------------------------------------------------------------------------
/article/ViewDragHelper源码分析.md:
--------------------------------------------------------------------------------
1 | ViewDragHelper源码解析
2 | =========
3 |
4 | ### 1.简介
5 | 我们了解了`ViewDragHelper`是可以帮助我们处理各种拖拽事件的类.使用好`ViewDragHelper`能帮助我们做出各种酷炫的交互,今天我们就来分析一下`ViewDragHelper`的使用与实现:
6 |
7 | ### 2.使用方法
8 |
9 | 我们这里就以[翔总的这篇文章](http://blog.csdn.net/lmj623565791/article/details/46858663)中的例子来介绍一下`ViewDragHelper`的使用.另外,本文中的demo可以在
10 | [这里找到](https://github.com/Skykai521/ViewDragHelperDemo)
11 |
12 | 首先我们创建一个`DragLayout`类并继承自`LinearLayout`,然后我们准备在`DragLayout`放置三个`View`第一个用来被我们拖动然后停止在松手的位置,第二个可以被我们拖动,松手的时候滑动到指定位置,第三个只可以通过触摸边缘来进行拖动,
13 |
14 | ```java
15 |
16 | public class DragLayout extends LinearLayout {
17 |
18 | private ViewDragHelper mDragger;
19 | private View mDragView;
20 | private View mAutoBackView;
21 | private View mEdgeTrackerView;
22 |
23 | private Point mAutoBackOriginPos = new Point();
24 |
25 | public DragLayout(Context context) {
26 | this(context, null);
27 | }
28 |
29 | public DragLayout(Context context, AttributeSet attrs) {
30 | this(context, attrs, 0);
31 | }
32 |
33 | public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
34 | super(context, attrs, defStyleAttr);
35 | initViewDragHelper();
36 | }
37 |
38 | private void initViewDragHelper() {
39 | mDragger = ViewDragHelper.create(this,myCallback);
40 | mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);
41 | }
42 |
43 | ViewDragHelper.Callback myCallback = new ViewDragHelper.Callback() {
44 | @Override
45 | //child为当前触摸区域下的View,如果返回true,就可以拖拽.
46 | public boolean tryCaptureView(View child, int pointerId) {
47 | return child == mDragView || child == mAutoBackView;
48 | }
49 |
50 | //松手时的回调
51 | @Override
52 | public void onViewReleased(View releasedChild, float xvel, float yvel) {
53 | if (releasedChild == mAutoBackView) {
54 | mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);
55 | invalidate();
56 | }
57 | }
58 |
59 | //边缘触摸开始时的回调
60 | @Override
61 | public void onEdgeDragStarted(int edgeFlags, int pointerId) {
62 | mDragger.captureChildView(mEdgeTrackerView, pointerId);
63 | }
64 |
65 | //获取水平方向允许拖拽的区域,这里是父布局的宽-子控件的宽
66 | @Override
67 | public int getViewHorizontalDragRange(View child) {
68 | return getMeasuredWidth() - child.getMeasuredWidth();
69 | }
70 |
71 | //获取垂直方向允许拖拽的范围
72 | @Override
73 | public int getViewVerticalDragRange(View child) {
74 | return getMeasuredHeight() - child.getMeasuredHeight();
75 | }
76 |
77 | //left为child即将移动到的水平位置的值,但是返回值会最终决定移动到的值
78 | //这里直接返回了left
79 | @Override
80 | public int clampViewPositionHorizontal(View child, int left, int dx) {
81 | return left;
82 | }
83 | //同上只是这里是垂直方向
84 | @Override
85 | public int clampViewPositionVertical(View child, int top, int dy) {
86 | return top;
87 | }
88 | };
89 |
90 | @Override
91 | public void computeScroll() {
92 | if (mDragger.continueSettling(true)) {
93 | invalidate();
94 | }
95 | }
96 |
97 | @Override
98 | public boolean onInterceptTouchEvent(MotionEvent ev) {
99 | return mDragger.shouldInterceptTouchEvent(ev);
100 | }
101 |
102 | @Override
103 | public boolean onTouchEvent(MotionEvent event) {
104 | mDragger.processTouchEvent(event);
105 | return true;
106 | }
107 |
108 | @Override
109 | protected void onFinishInflate() {
110 | super.onFinishInflate();
111 | mDragView = getChildAt(0);
112 | mAutoBackView = getChildAt(1);
113 | mEdgeTrackerView = getChildAt(2);
114 | }
115 |
116 | @Override
117 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
118 | super.onLayout(changed, l, t, r, b);
119 | mAutoBackOriginPos.x = mAutoBackView.getLeft();
120 | mAutoBackOriginPos.y = mAutoBackView.getTop();
121 | }
122 | }
123 |
124 | ```
125 | 1. 我们首先在构造方法里传入了当前类的对象和我们定义的`ViewDragHelper.Callback`对象初始化了我们的`ViewDragHelper`,然后我们希望所有的边缘触摸都能触发`mEdgeTrackerView`的拖动,所以我们紧接着调用了`mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);`方法.
126 | 2. 在我们定义的`Callback`中,有多个回调方法,每个回调方法都有它的作用,在代码里注释比较清楚了,我们下面也会解析每一个`Callback`中回调方法的作用.
127 | 3. 第三步我们需要在`onInterceptTouchEvent()`方法和`onTouchEvent()`将事件委托给`ViewDragHelper`去处理,这样`ViewDragHelper`才能根据响应的事件并回调我们自己编写的`Callback`接口来进行响应的处理,
128 | 4. 由于`ViewDragHelper`中的滑动是交给`Srcoller`类来处理的所以这里我们要重写`computeScroll()`方法,配合`Scroller`完成滚动动画.
129 | 5. 最后在`onFinishInflate()`里获取到我们的`View`对象即可.
130 |
131 | ### 3.类关系图
132 |
133 | 由于就一个类类图我们就不画了,但是作为一个强迫症患者,这个标题必须有...
134 |
135 | ### 4.源码分析
136 | #### 1.ViewDragHelper.Callback的实现
137 | 在分析`ViewDragHelper`之前,我们先来分析一下`Callback`的定义,看看`Callback`都定义了哪些方法:
138 | ```java
139 |
140 | public static abstract class Callback {
141 |
142 | //当View的拖拽状态改变时回调,state为STATE_IDLE,STATE_DRAGGING,STATE_SETTLING的一种
143 | //STATE_IDLE: 当前未被拖拽
144 | //STATE_DRAGGING:正在被拖拽
145 | //STATE_SETTLING: 被拖拽后需要被安放到一个位置中的状态
146 | public void onViewDragStateChanged(int state) {}
147 |
148 | //当View被拖拽位置发生改变时回调
149 | //changedView :被拖拽的View
150 | //left : 被拖拽后View的left边缘坐标
151 | //top : 被拖拽后View的top边缘坐标
152 | //dx : 拖动的x偏移量
153 | //dy : 拖动的y偏移量
154 | public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}
155 |
156 | //当一个View被捕获到准备开始拖动时回调,
157 | //capturedChild : 捕获的View
158 | //activePointerId : 对应的PointerId
159 | public void onViewCaptured(View capturedChild, int activePointerId) {}
160 |
161 | //当被捕获拖拽的View被释放是回调
162 | //releasedChild : 被释放的View
163 | //xvel : 释放View的x方向上的加速度
164 | //yvel : 释放View的y方向上的加速度
165 | public void onViewReleased(View releasedChild, float xvel, float yvel) {}
166 |
167 | //如果parentView订阅了边缘触摸,则如果有边缘触摸就回调的接口
168 | //edgeFlags : 当前触摸的flag 有: EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM
169 | //pointerId : 用来描述边缘触摸操作的id
170 | public void onEdgeTouched(int edgeFlags, int pointerId) {}
171 |
172 | //是否锁定该边缘的触摸,默认返回false,返回true表示锁定
173 | public boolean onEdgeLock(int edgeFlags) {
174 | return false;
175 | }
176 |
177 | //边缘触摸开始时回调
178 | //edgeFlags : 当前触摸的flag 有: EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM
179 | //pointerId : 用来描述边缘触摸操作的id
180 | public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
181 |
182 | //在寻找当前触摸点下的子View时会调用此方法,寻找到的View会提供给tryCaptureViewForDrag()来尝试捕获。
183 | //如果需要改变子View的遍历查询顺序可改写此方法,例如让下层的View优先于上层的View被选中。
184 | public int getOrderedChildIndex(int index) {
185 | return index;
186 | }
187 |
188 | //获取被拖拽View child 的水平拖拽范围,返回0表示无法被水平拖拽
189 | public int getViewHorizontalDragRange(View child) {
190 | return 0;
191 | }
192 |
193 | //获取被拖拽View child 的垂直拖拽范围,返回0表示无法被水平拖拽
194 | public int getViewVerticalDragRange(View child) {
195 | return 0;
196 | }
197 |
198 | //尝试捕获被拖拽的View
199 | public abstract boolean tryCaptureView(View child, int pointerId);
200 |
201 | //决定拖拽View在水平方向上应该移动到的位置
202 | //child : 被拖拽的View
203 | //left : 期望移动到位置的View的left值
204 | //dx : 移动的水平距离
205 | //返回值 : 直接决定View在水平方向的位置
206 | public int clampViewPositionHorizontal(View child, int left, int dx) {
207 | return 0;
208 | }
209 |
210 | //决定拖拽View在垂直方向上应该移动到的位置
211 | //child : 被拖拽的View
212 | //top : 期望移动到位置的View的top值
213 | //dy : 移动的垂直距离
214 | //返回值 : 直接决定View在垂直方向的位置
215 | public int clampViewPositionVertical(View child, int top, int dy) {
216 | return 0;
217 | }
218 | }
219 |
220 | ```
221 | 想必注释已经很清楚了,正是这些回调方法,再结合`ViewDragHelper`中的各种方法,来帮助我们实现各种各样的拖拽的效果。
222 |
223 | #### 2.shouldInterceptTouchEvent()方法的实现
224 |
225 | 在这里我们假设大家都清楚了`Android`的事件分发机制,如果不清楚请看[这里](http://blog.csdn.net/guolin_blog/article/details/9097463),要想处理触摸事件,我们需要在`onInterceptTouchEvent(MotionEvent ev)`方法里判断是否需要拦截这次触摸事件,如果此方法返回`true`则触摸事件将会交给`onTouchEvent(MotionEvent event)`处理,这样我们就能处理触摸事件了,所以我们在上面的使用方法里会这样写:
226 |
227 | ```java
228 |
229 | @Override
230 | public boolean onInterceptTouchEvent(MotionEvent ev) {
231 | return mDragger.shouldInterceptTouchEvent(ev);
232 | }
233 |
234 | @Override
235 | public boolean onTouchEvent(MotionEvent event) {
236 | mDragger.processTouchEvent(event);
237 | return true;
238 | }
239 | ```
240 | 这样就将是否拦截触摸事件,以及处理触摸事件委托给`ViewDragHelper`来处理了,所以我们先来看看`ViewDragHelper`中`shouldInterceptTouchEvent();`方法的实现:
241 |
242 | ```java
243 |
244 | public boolean shouldInterceptTouchEvent(MotionEvent ev) {
245 | //获取action
246 | final int action = MotionEventCompat.getActionMasked(ev);
247 | //获取action对应的index
248 | final int actionIndex = MotionEventCompat.getActionIndex(ev);
249 |
250 | //如果是按下的action则重置一些信息,包括各种事件点的数组
251 | if (action == MotionEvent.ACTION_DOWN) {
252 | // Reset things for a new event stream, just in case we didn't get
253 | // the whole previous stream.
254 | cancel();
255 | }
256 | //初始化mVelocityTracker
257 | if (mVelocityTracker == null) {
258 | mVelocityTracker = VelocityTracker.obtain();
259 | }
260 | mVelocityTracker.addMovement(ev);
261 |
262 | //根据action来做相应的处理
263 | switch (action) {
264 | case MotionEvent.ACTION_DOWN: {
265 | final float x = ev.getX();
266 | final float y = ev.getY();
267 | //获取这个事件对应的pointerId,一般情况下只有一个手指触摸时为0
268 | //两个手指触摸时第二个手指触摸返回的pointerId为1,以此类推
269 | final int pointerId = MotionEventCompat.getPointerId(ev, 0);
270 | //保存点的数据
271 | //TODO (1)
272 | saveInitialMotion(x, y, pointerId);
273 | //获取当前触摸点下最顶层的子View
274 | //TODO (2)
275 | final View toCapture = findTopChildUnder((int) x, (int) y);
276 |
277 | //如果toCapture是已经捕获的View,而且正在处于被释放状态
278 | //那么就重新捕获
279 | if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
280 | tryCaptureViewForDrag(toCapture, pointerId);
281 | }
282 |
283 | //如果触摸了边缘,回调callback的onEdgeTouched()方法
284 | final int edgesTouched = mInitialEdgesTouched[pointerId];
285 | if ((edgesTouched & mTrackingEdges) != 0) {
286 | mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
287 | }
288 | break;
289 | }
290 |
291 | //当又有一个手指触摸时
292 | case MotionEventCompat.ACTION_POINTER_DOWN: {
293 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
294 | final float x = MotionEventCompat.getX(ev, actionIndex);
295 | final float y = MotionEventCompat.getY(ev, actionIndex);
296 |
297 | //保存触摸信息
298 | saveInitialMotion(x, y, pointerId);
299 |
300 | //因为同一时间ViewDragHelper只能操控一个View,所以当有新的手指触摸时
301 | //只讨论当无触摸发生时,回调边缘触摸的callback
302 | //或者正在处于释放状态时重新捕获View
303 | if (mDragState == STATE_IDLE) {
304 | final int edgesTouched = mInitialEdgesTouched[pointerId];
305 | if ((edgesTouched & mTrackingEdges) != 0) {
306 | mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
307 | }
308 | } else if (mDragState == STATE_SETTLING) {
309 | // Catch a settling view if possible.
310 | final View toCapture = findTopChildUnder((int) x, (int) y);
311 | if (toCapture == mCapturedView) {
312 | tryCaptureViewForDrag(toCapture, pointerId);
313 | }
314 | }
315 | break;
316 | }
317 |
318 | //当手指移动时
319 | case MotionEvent.ACTION_MOVE: {
320 | if (mInitialMotionX == null || mInitialMotionY == null) break;
321 |
322 | // First to cross a touch slop over a draggable view wins. Also report edge drags.
323 | //得到触摸点的数量,并循环处理,只处理第一个发生了拖拽的事件
324 | final int pointerCount = MotionEventCompat.getPointerCount(ev);
325 | for (int i = 0; i < pointerCount; i++) {
326 | final int pointerId = MotionEventCompat.getPointerId(ev, i);
327 | final float x = MotionEventCompat.getX(ev, i);
328 | final float y = MotionEventCompat.getY(ev, i);
329 | //获得拖拽偏移量
330 | final float dx = x - mInitialMotionX[pointerId];
331 | final float dy = y - mInitialMotionY[pointerId];
332 | //获取当前触摸点下最顶层的子View
333 | final View toCapture = findTopChildUnder((int) x, (int) y);
334 | //如果找到了最顶层View,并且产生了拖动(checkTouchSlop()返回true)
335 | //TODO (3)
336 | final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
337 | if (pastSlop) {
338 | //根据callback的四个方法getView[Horizontal|Vertical]DragRange和
339 | //clampViewPosition[Horizontal|Vertical]来检查是否可以拖动
340 | final int oldLeft = toCapture.getLeft();
341 | final int targetLeft = oldLeft + (int) dx;
342 | final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
343 | targetLeft, (int) dx);
344 | final int oldTop = toCapture.getTop();
345 | final int targetTop = oldTop + (int) dy;
346 | final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
347 | (int) dy);
348 | final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
349 | toCapture);
350 | final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
351 | //如果都不允许移动则跳出循环
352 | if ((horizontalDragRange == 0 || horizontalDragRange > 0
353 | && newLeft == oldLeft) && (verticalDragRange == 0
354 | || verticalDragRange > 0 && newTop == oldTop)) {
355 | break;
356 | }
357 | }
358 | //记录并回调是否有边缘触摸
359 | reportNewEdgeDrags(dx, dy, pointerId);
360 | if (mDragState == STATE_DRAGGING) {
361 | // Callback might have started an edge drag
362 | break;
363 | }
364 | //如果产生了拖动则调用tryCaptureViewForDrag()
365 | //TODO (4)
366 | if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
367 | break;
368 | }
369 | }
370 | //保存触摸点的信息
371 | saveLastMotion(ev);
372 | break;
373 | }
374 |
375 | //当有一个手指抬起时,清除这个手指的触摸数据
376 | case MotionEventCompat.ACTION_POINTER_UP: {
377 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
378 | clearMotionHistory(pointerId);
379 | break;
380 | }
381 |
382 | //清除所有触摸数据
383 | case MotionEvent.ACTION_UP:
384 | case MotionEvent.ACTION_CANCEL: {
385 | cancel();
386 | break;
387 | }
388 | }
389 |
390 | //如果mDragState等于正在拖拽则返回true
391 | return mDragState == STATE_DRAGGING;
392 | }
393 |
394 | ```
395 | 上面就是整个`shouldInterceptTouchEvent()`的实现,上面的注释也足够清楚了,我们这里就先不分析某一种触摸事件,大家可以看到我上面留了几个TODO,下文会一起分析,这里我假设大家都已经对触摸事件分发处理都有充分的理解了,我们下面就直接看`ViewDragHelper`里`processTouchEvent()`方法的实现.
396 |
397 | #### 3.processTouchEvent()方法的实现
398 |
399 | ```java
400 |
401 | public void processTouchEvent(MotionEvent ev) {
402 | final int action = MotionEventCompat.getActionMasked(ev);
403 | final int actionIndex = MotionEventCompat.getActionIndex(ev);
404 |
405 | ...(省去部分代码)
406 | switch (action) {
407 | case MotionEvent.ACTION_DOWN: {
408 | ...(省去部分代码)
409 | break;
410 | }
411 |
412 | case MotionEventCompat.ACTION_POINTER_DOWN: {
413 | ...(省去部分代码)
414 | break;
415 | }
416 |
417 | case MotionEvent.ACTION_MOVE: {
418 | //如果现在已经是拖拽状态
419 | if (mDragState == STATE_DRAGGING) {
420 | final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
421 | final float x = MotionEventCompat.getX(ev, index);
422 | final float y = MotionEventCompat.getY(ev, index);
423 | final int idx = (int) (x - mLastMotionX[mActivePointerId]);
424 | final int idy = (int) (y - mLastMotionY[mActivePointerId]);
425 |
426 | //拖拽至指定位置
427 | //TODO (5)
428 | dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
429 |
430 | saveLastMotion(ev);
431 | } else {
432 | // Check to see if any pointer is now over a draggable view.
433 | //如果还不是拖拽状态,就检测是否经过了一个View
434 | final int pointerCount = MotionEventCompat.getPointerCount(ev);
435 | for (int i = 0; i < pointerCount; i++) {
436 | final int pointerId = MotionEventCompat.getPointerId(ev, i);
437 | final float x = MotionEventCompat.getX(ev, i);
438 | final float y = MotionEventCompat.getY(ev, i);
439 | final float dx = x - mInitialMotionX[pointerId];
440 | final float dy = y - mInitialMotionY[pointerId];
441 |
442 | reportNewEdgeDrags(dx, dy, pointerId);
443 | if (mDragState == STATE_DRAGGING) {
444 | // Callback might have started an edge drag.
445 | break;
446 | }
447 |
448 | final View toCapture = findTopChildUnder((int) x, (int) y);
449 | if (checkTouchSlop(toCapture, dx, dy) &&
450 | tryCaptureViewForDrag(toCapture, pointerId)) {
451 | break;
452 | }
453 | }
454 | saveLastMotion(ev);
455 | }
456 | break;
457 | }
458 | //当多个手指中的一个手机松开时
459 | case MotionEventCompat.ACTION_POINTER_UP: {
460 | final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
461 | //如果当前点正在被拖拽,则再剩余还在触摸的点钟寻找是否正在View上
462 | if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
463 | // Try to find another pointer that's still holding on to the captured view.
464 | int newActivePointer = INVALID_POINTER;
465 | final int pointerCount = MotionEventCompat.getPointerCount(ev);
466 | for (int i = 0; i < pointerCount; i++) {
467 | final int id = MotionEventCompat.getPointerId(ev, i);
468 | if (id == mActivePointerId) {
469 | // This one's going away, skip.
470 | continue;
471 | }
472 |
473 | final float x = MotionEventCompat.getX(ev, i);
474 | final float y = MotionEventCompat.getY(ev, i);
475 | if (findTopChildUnder((int) x, (int) y) == mCapturedView &&
476 | tryCaptureViewForDrag(mCapturedView, id)) {
477 | newActivePointer = mActivePointerId;
478 | break;
479 | }
480 | }
481 |
482 | if (newActivePointer == INVALID_POINTER) {
483 | // We didn't find another pointer still touching the view, release it.
484 | //如果没找到则释放View
485 | //TODO (6)
486 | releaseViewForPointerUp();
487 | }
488 | }
489 | clearMotionHistory(pointerId);
490 | break;
491 | }
492 |
493 | case MotionEvent.ACTION_UP: {
494 | //如果是拖拽状态的释放则调用
495 | //releaseViewForPointerUp()
496 | if (mDragState == STATE_DRAGGING) {
497 | releaseViewForPointerUp();
498 | }
499 | cancel();
500 | break;
501 | }
502 |
503 | case MotionEvent.ACTION_CANCEL: {
504 | if (mDragState == STATE_DRAGGING) {
505 | dispatchViewReleased(0, 0);
506 | }
507 | cancel();
508 | break;
509 | }
510 | }
511 | }
512 |
513 | ```
514 |
515 | 上面就是`processTouchEvent()`方法的实现,我们省去了部分大致与`shouldInterceptTouchEvent()`相同的逻辑代码,通过事件传递机制我们知道,如果程序已经进入到`processTouchEvent()`中,也就意味着触摸事件就不会再向下传递,都会交给此方法处理,所以在这里我们就需要处理拖拽事件了,通过上面的注释,我们也看到了在`MotionEvent.ACTION_MOVE`,`MotionEventCompat.ACTION_POINTER_UP`,`MotionEvent.ACTION_UP`和`MotionEvent.ACTION_CANCEL`都分别进行了处理 ,我们知道触摸事件大致的流程是:
516 |
517 | ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP
518 |
519 | 再配合事件的分发机制,我们就能很清晰的分析出一次完整的事件调用过程,所以整个`ViewDragHelper`的拖拽过程也能很清晰的分为三个步骤:
520 |
521 | 捕获拖拽目标View -> 拖拽目标View -> 处理目标View释放操作
522 |
523 | 最后我们再分析上面两段代码的6个TODO:
524 |
525 | #### 4.saveInitialMotion()方法
526 |
527 | ```java
528 | private void saveInitialMotion(float x, float y, int pointerId) {
529 | //确保各个数组的大小足够存放数据
530 | ensureMotionHistorySizeForId(pointerId);
531 | //保存x坐标
532 | mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
533 | //保存y坐标
534 | mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
535 | //保存是否触摸到边缘
536 | mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
537 | //保存当前id是否在触摸,用于后续验证
538 | mPointersDown |= 1 << pointerId;
539 | }
540 |
541 | ```
542 | #### 5.findTopChildUnder()方法
543 |
544 | ```java
545 | public View findTopChildUnder(int x, int y) {
546 | final int childCount = mParentView.getChildCount();
547 | for (int i = childCount - 1; i >= 0; i--) {
548 | final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
549 | if (x >= child.getLeft() && x < child.getRight() &&
550 | y >= child.getTop() && y < child.getBottom()) {
551 | return child;
552 | }
553 | }
554 | return null;
555 | }
556 | ```
557 | 代码很简单就是根据`x`和`y`坐标和来找到指定`View`,注意这里回调了`callback`中的`getOrderedChildIndex()`方法,所以我们可以在这里返回指定的`View`的`index`.
558 |
559 | #### 6.checkTouchSlop()方法
560 |
561 | ```java
562 | private boolean checkTouchSlop(View child, float dx, float dy) {
563 | if (child == null) {
564 | return false;
565 | }
566 | final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
567 | final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;
568 |
569 | if (checkHorizontal && checkVertical) {
570 | return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
571 | } else if (checkHorizontal) {
572 | return Math.abs(dx) > mTouchSlop;
573 | } else if (checkVertical) {
574 | return Math.abs(dy) > mTouchSlop;
575 | }
576 | return false;
577 | }
578 | ```
579 | 用来根据`mTouchSlop`最小拖动的距离来判断是否属于拖动,`mTouchSlop`根据我们设定的灵敏度决定.
580 |
581 | #### 7.tryCaptureViewForDrag()方法
582 |
583 | ```java
584 |
585 | boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
586 | //如果已经捕获该View 直接返回true
587 | if (toCapture == mCapturedView && mActivePointerId == pointerId) {
588 | // Already done!
589 | return true;
590 | }
591 | //根据mCallback.tryCaptureView()方法来最终决定是否可以捕获View
592 | if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
593 | mActivePointerId = pointerId;
594 | //如果可以则调用captureChildView(),并返回true
595 | captureChildView(toCapture, pointerId);
596 | return true;
597 | }
598 | return false;
599 | }
600 |
601 | ```
602 | 可以看到如果可以捕获`View`则调用了`captureChildView()`方法:
603 |
604 | ```java
605 |
606 | public void captureChildView(View childView, int activePointerId) {
607 | if (childView.getParent() != mParentView) {
608 | throw new IllegalArgumentException("captureChildView: parameter must be a descendant " +
609 | "of the ViewDragHelper's tracked parent view (" + mParentView + ")");
610 | }
611 | //赋值mCapturedView
612 | mCapturedView = childView;
613 | mActivePointerId = activePointerId;
614 | //回调callback
615 | mCallback.onViewCaptured(childView, activePointerId);
616 | //设定mDragState的状态为STATE_DRAGGING
617 | setDragState(STATE_DRAGGING);
618 | }
619 |
620 | ```
621 | 如果程序执行到这里,就证明`View`已经处于拖拽状态了,后续的触摸操作,将直接根据`mDragState`为`STATE_DRAGGING`的状态处理.
622 |
623 | #### 8.dragTo()方法的实现
624 |
625 | ```java
626 |
627 | private void dragTo(int left, int top, int dx, int dy) {
628 | int clampedX = left;
629 | int clampedY = top;
630 | final int oldLeft = mCapturedView.getLeft();
631 | final int oldTop = mCapturedView.getTop();
632 | if (dx != 0) {
633 | //回调callback来决定View最终被拖拽的x方向上的偏移量
634 | clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
635 | //移动View
636 | ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
637 | }
638 | if (dy != 0) {
639 | //回调callback来决定View最终被拖拽的y方向上的偏移量
640 | clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
641 | //移动View
642 | ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
643 | }
644 |
645 | if (dx != 0 || dy != 0) {
646 | final int clampedDx = clampedX - oldLeft;
647 | final int clampedDy = clampedY - oldTop;
648 | //回调callback
649 | mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
650 | clampedDx, clampedDy);
651 | }
652 | }
653 |
654 | ```
655 | 因为`dragTo()`方法是在`processTouchEvent()`中的`MotionEvent.ACTION_MOVE case`被调用所以当程序运行到这里时`View`就会不断的被拖动了。如果一旦手指释放则最终会调用`releaseViewForPointerUp()`方法
656 |
657 | #### 8.releaseViewForPointerUp()方法的实现
658 |
659 | ```java
660 |
661 | private void releaseViewForPointerUp() {
662 | //计算出当前x和y方向上的加速度
663 | mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
664 | final float xvel = clampMag(
665 | VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
666 | mMinVelocity, mMaxVelocity);
667 | final float yvel = clampMag(
668 | VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
669 | mMinVelocity, mMaxVelocity);
670 | dispatchViewReleased(xvel, yvel);
671 | }
672 |
673 | ```
674 | 计算完加速度后就调用了`dispatchViewReleased()`:
675 |
676 | ```java
677 |
678 | private void dispatchViewReleased(float xvel, float yvel) {
679 | //设定当前正处于释放阶段
680 | mReleaseInProgress = true;
681 | //回调callback的onViewReleased()方法
682 | mCallback.onViewReleased(mCapturedView, xvel, yvel);
683 | mReleaseInProgress = false;
684 |
685 | //设定状态
686 | if (mDragState == STATE_DRAGGING) {
687 | // onViewReleased didn't call a method that would have changed this. Go idle.
688 | //如果onViewReleased()中没有调用任何方法,则状态设定为STATE_IDLE
689 | setDragState(STATE_IDLE);
690 | }
691 | }
692 |
693 | ```
694 | 所以最后释放后的处理交给了`callback`中的`onViewReleased()`方法,如果我们什么都不做,那么这个被拖拽的`View`就是停止在当前位置,或者我们可以调用`ViewDragHelper`提供给我们的这几个方法:
695 |
696 | - settleCapturedViewAt(int finalLeft, int finalTop)
697 | 以松手前的滑动速度为初速动,让捕获到的View自动滚动到指定位置。只能在Callback的onViewReleased()中调用。
698 | - flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)
699 | 以松手前的滑动速度为初速动,让捕获到的View在指定范围内fling。只能在Callback的onViewReleased()中调用。
700 | - smoothSlideViewTo(View child, int finalLeft, int finalTop)
701 | 指定某个View自动滚动到指定的位置,初速度为0,可在任何地方调用。
702 |
703 | 引用自[这篇文章](http://www.cnphp6.com/archives/87727),具体释放后的原理我们就不分析了,其实就是配合`Scroller`这个类来实现,具体也可以参照上面这篇文章。好,我们关于`ViewDragHelper`的源码分析就到这里.
704 |
705 | ### 5.开源项目中的使用
706 |
707 | `ViewDragHelper`在各种关于拖拽和各种手势动画的开源库中使用广泛,我这里就简要列出一些,大家可以多去看看是如何使用`ViewDragHelper`的:
708 |
709 | - [SwipeBackLayout](https://github.com/ikew0ng/SwipeBackLayout)
710 | - [android-card-slide-panel](https://github.com/xmuSistone/android-card-slide-panel)
711 | - [FlowingDrawer](https://github.com/mxn21/FlowingDrawer)
712 |
713 | ### 6.个人评价
714 | `ViewDragHelper`的出现,大大简化了我们开发相关触摸和拖拽功能的复杂度和代码量,帮助我们比较容易的实现各种效果,让我们开发酷炫的交互更加容易了。但是从一些开源项目中发现,`ViewDragHelper`中还是有一些不足之处,比如给`Scroller`提供了一个固定的`Interpolator`,导致如果我们想实现例如反弹效果的话,还要把`ViewDragHelper`的代码拷贝一份并修改`Interpolator`,这样做肯定是不太好的.当然建议我们自己修改一个`ViewDragHelper`后如果项目里有多处使用,可以包装成一个提供给我们自己项目的模块使用,防止出现更多的多余代码.
715 |
--------------------------------------------------------------------------------
/article/ViewGroup 源码解析.md:
--------------------------------------------------------------------------------
1 | # ViewGroup 源码解析
2 |
3 | ViewGroup应该算是日常Android开发中最常涉及到,但是也最少直接使用到的View子类。我们平时所谈论的触摸事件的传递、测量和布局的过程等,其实都源自ViewGroup。同时也是各种其他事件,比如onAttachedToWindow、onDetacheFromWindow、onConfigurationChanged等的中间人。
4 |
5 |
6 |
7 | ## ViewGroup生命周期
8 |
9 | 
10 |
11 | 可以看到ViewGroup也是和View一样在onAttachedToWindow以后,经理测量(onMeasure)、布局(onLayout)、然后才是绘制(ViewGroup默认不绘制自身,而是调用dispatchDraw来绘制子View)。和View不同的是,这每一步都需要对子View进行相同的操作。
12 |
13 |
14 |
15 | #### 测量和布局过程
16 |
17 | ViewGroup由于需要兼顾子View的测量和布局,以及在部分情况下子View的测量反过来影响ViewGroup的测量(例如ViewGroup的宽高为WRAP_CONTENT的时候),整体的测量和布局需要经历比较复杂的过程。
18 |
19 |
20 |
21 | 首先从onMeasure方法开始,onMeasure传入了两个作为测量依据的参数,widthMeasureSpec, heightMeasureSpec。这两个参数表面上是int,但实际上是测量模式和父View要求的尺寸两个值组成的。这个int值的最高两位被用来储存测量模式,剩下的30位才是尺寸。测量模式有以下三种:
22 |
23 | * UNSPECIFIED 父View并没有强制要求当前ViewGroup遵从某个规范,当前ViewGroup可以是任何尺寸
24 | * EXACTLY 父View要求当前ViewGroup的宽度或者高度必须等于MeasureSpec传入的尺寸
25 | * AT_MOST 父View要求当前ViewGroup的宽度或者高度不能超过MeasureSpec传入的尺寸
26 |
27 | 然后ViewGroup根据传入的MeasureSpec以及自身对子View的排版规则对子View进行测量,在对子View的测量中又涉及到另一个类:LayoutParams。这个类相信大家都不陌生,不同的布局控件(RelativeLayout、LinearLayout、FrameLayout等)都有自己的一个LayoutParams。里面包含了针对当前布局的一些参数,这些参数影响了ViewGroup对子View的测量。
28 |
29 |
30 |
31 | LayoutParams和MeasureSpec是比较容易搞混的,两个都是和测量布局有关的参数,每个控件都有针对于父View的LayoutParams成员变量,但父View对自身进行测量的时候又要传入MeasureSpec。这两个参数的区别是什么呢?可以简单地理解为:LayoutParams是View自己对自己的布局要求,而MeasureSpec是父View参考了View的LayoutParams后,提出来的对View的布局要求。这就像孩子吵着要跟父母拿钱买十颗糖吃(LayoutParams),父母经过深思熟虑后,决定只给孩子买五颗糖(MeasureSpec)。
32 |
33 |
34 |
35 | ViewGroup内部并没有真正处理子View的测量,因为ViewGroup本身并没有针对子View的规则限制。只有在具体的布局中,比如RelativeLayout、LinearLayout,由于有子View的排版规则才会对子View进行测量。但是ViewGroup本身提供了默认的测量子View的方法:
36 |
37 | * measureChildren 根据ViewGroup自身onMeasure传入的MeasureSpec对所有子View进行测量,实际是遍历子View并调用了measureChild方法:
38 |
39 | ```java
40 | protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
41 | final int size = mChildrenCount;
42 | final View[] children = mChildren;
43 | // 遍历子View
44 | for (int i = 0; i < size; ++i) {
45 | final View child = children[i];
46 | // 检测当前子View的Visibility状态,如果是GONE则跳过
47 | if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
48 | measureChild(child, widthMeasureSpec, heightMeasureSpec);
49 | }
50 | }
51 | }
52 | ```
53 |
54 |
55 |
56 | * measureChild 根据ViewGroup自身onMeasure传入的MeasureSpec对某一个子View进行测量
57 |
58 | ```java
59 | protected void measureChild(View child, int parentWidthMeasureSpec,
60 | int parentHeightMeasureSpec) {
61 | // 获取子View的LayoutParams
62 | final LayoutParams lp = child.getLayoutParams();
63 |
64 | // 计算并返回子View的widthMeasureSpec
65 | final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
66 | mPaddingLeft + mPaddingRight, lp.width);
67 | // 计算并返回子View的heightMeasureSpec
68 | final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
69 | mPaddingTop + mPaddingBottom, lp.height);
70 |
71 | // 调用子View的measure方法,measure方法最终会调用子View的onMeasure,具体的实现在View内
72 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
73 | }
74 | ```
75 |
76 |
77 |
78 | 这里面实际核心的是getChildMeasureSpec方法,这是一个ViewGroup提供的根据ViewGroup自身的MeasureSpec,ViewGroup的padding以及子View的LayoutParams的width/height返回子View的MeasureSpec的方法:
79 |
80 | ```java
81 | public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
82 | // 用MeasureSpec提供的方法来获取具体的模式和尺寸
83 | // (实际上是把高2位和低30位分开, mode = spec & 0xC0000000, size = spec & 0x3FFFFFFF)
84 | int specMode = MeasureSpec.getMode(spec);
85 | int specSize = MeasureSpec.getSize(spec);
86 |
87 | // 由于上一步获取的是ViewGroup自身的specSize,
88 | // 而ViewGroup留给子View的区域是要扣除padding的,这边需要减去padding
89 | int size = Math.max(0, specSize - padding);
90 |
91 | int resultSize = 0;
92 | int resultMode = 0;
93 |
94 | switch (specMode) {
95 | // ViewGroup自身的MeasureSpec为EXACTLY的时候
96 | case MeasureSpec.EXACTLY:
97 | if (childDimension >= 0) {
98 | // 子View的LayoutParams传入的尺寸既不是MATCH_PARENT也不是WRAP_CONTENT的时候,
99 | // 直接让子View的尺寸固定为传入的值
100 | resultSize = childDimension;
101 | resultMode = MeasureSpec.EXACTLY;
102 | } else if (childDimension == LayoutParams.MATCH_PARENT) {
103 | // 子View的LayoutParams传入的为MATCH_PARENT,即是和父View同样尺寸,
104 | // 直接给ViewGroup的size值
105 | resultSize = size;
106 | resultMode = MeasureSpec.EXACTLY;
107 | } else if (childDimension == LayoutParams.WRAP_CONTENT) {
108 | // 子View的LayoutParams传入的为WRAP_CONTENT,但是ViewGroup自身是固定的尺寸,
109 | // 这时候让子View在不超出ViewGroup的size的情况下自行决定大小
110 | resultSize = size;
111 | resultMode = MeasureSpec.AT_MOST;
112 | }
113 | break;
114 |
115 | // 传入的ViewGroup的MeasureSpec规定在给定的尺寸范围内自行决定大小,
116 | // 这通常是在ViewGroup本身的尺寸设置为WRAP_CONTENT的情况下传入
117 | // (参考上面MeasureSpec.EXACTLY的case里面childDimension == LayoutParams.WRAP_CONTNET的情况)
118 | case MeasureSpec.AT_MOST:
119 | if (childDimension >= 0) {
120 | //子View的LayoutParams传入的尺寸既不是MATCH_PARENT也不是WRAP_CONTENT的时候,
121 | //直接让子View的尺寸固定为传入的值
122 | resultSize = childDimension;
123 | resultMode = MeasureSpec.EXACTLY;
124 | } else if (childDimension == LayoutParams.MATCH_PARENT) {
125 | // 子View的LayoutParams传入的为MATCH_PARENT,但是ViewGroup自身的尺寸还未确定,
126 | //只能让子View在ViewGroup的size范围内自行决定大小
127 | resultSize = size;
128 | resultMode = MeasureSpec.AT_MOST;
129 | } else if (childDimension == LayoutParams.WRAP_CONTENT) {
130 | // 子View的LayoutParams传入的为WRAP_CONTENT,
131 | // 这时候让子View在不超出ViewGroup的size的情况下自行决定大小
132 | resultSize = size;
133 | resultMode = MeasureSpec.AT_MOST;
134 | }
135 | break;
136 |
137 | case MeasureSpec.UNSPECIFIED:
138 | if (childDimension >= 0) {
139 | //子View的LayoutParams传入的尺寸既不是MATCH_PARENT也不是WRAP_CONTENT的时候,
140 | //直接让子View的尺寸固定为传入的值
141 | resultSize = childDimension;
142 | resultMode = MeasureSpec.EXACTLY;
143 | } else if (childDimension == LayoutParams.MATCH_PARENT) {
144 | // 在ViewGroup自身的MeasureSpec未定义的情况下(UNSPECIFIED),
145 | // 给子View也传未定义mode(这边sUseZeroUnspecifiedMeasureSpec)
146 | resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
147 | resultMode = MeasureSpec.UNSPECIFIED;
148 | } else if (childDimension == LayoutParams.WRAP_CONTENT) {
149 | resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
150 | resultMode = MeasureSpec.UNSPECIFIED;
151 | }
152 | break;
153 | }
154 | // 用MeasureSpec类的makeMeasureSpec把size和mode拼装成一个int并返回
155 | return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
156 | }
157 | ```
158 |
159 |
160 |
161 | 
162 |
163 |
164 |
165 | 当然这只是ViewGroup提供的默认的方法,并不是强制的要求以这个规则返回子View的MeasureSpec。一些布局控件例如RelativeLayout会使用自己的子View的MeasureSpec生成规则。根据子View的LayoutParams来生成MeasureSpec,并对子View进行测量后,ViewGroup(继承自ViewGroup的类)就可以进行布局了。(布局对应的方法onLayout是一个抽象方法,在不同的ViewGroup的子类中有不一样的实现)
166 |
167 |
168 |
169 | ### 子View的添加和删除
170 |
171 |
172 |
173 | ViewGroup添加子View的方法有多个,但是最终都会调addView(child, index, params)方法,并在这个方法内调用私有方法addViewInner来实现的。
174 |
175 |
176 |
177 | 我们前面知道了每个子View都需要有一个LayoutParams来告诉ViewGroup它布局的时候需要的一些参数,而当添加View的时候,View没有LayoutParams,或者我们调用了没有LayoutParams作为形参的addView方法的时候,ViewGroup会调用自身的generatedDefaultLayoutParams来帮子View生成一个默认的LayoutParams:
178 |
179 | ```java
180 | public void addView(View child, int index) {
181 | if (child == null) {
182 | throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
183 | }
184 | LayoutParams params = child.getLayoutParams();
185 | if (params == null) {
186 | // params为空的情况,调用generateDefaultLayoutParams方法生成LayoutParams
187 | params = generateDefaultLayoutParams();
188 | if (params == null) {
189 | throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
190 | }
191 | }
192 | addView(child, index, params);
193 | }
194 |
195 | public void addView(View child, int width, int height) {
196 | // 调用这个方法的时候,会直接无视子View原来的LayoutParams,
197 | // 直接调用generateDefaultLayoutParams生成新的LayoutParams
198 | final LayoutParams params = generateDefaultLayoutParams();
199 | params.width = width;
200 | params.height = height;
201 | addView(child, -1, params);
202 | }
203 | ```
204 |
205 |
206 |
207 | 然后是私有方法addViewInner:
208 |
209 | ```java
210 | private void addViewInner(View child, int index, LayoutParams params,
211 | boolean preventRequestLayout) {
212 | ...
213 |
214 | // 子View的parent不为空,说明子View已经被添加到其他ViewGroup了。直接抛出异常。
215 | // 也就是说子View是不允许同时被添加到多个ViewGroup中的。这边挺好理解的,因为子View的布局相关参数都是唯一的,
216 | // 如果同时被添加到多个ViewGroup,而ViewGroup的布局规则各不相同,会导致我们从某一个ViewGroup获取子View的时候,没法得到它正确的尺寸等相关信息
217 | if (child.getParent() != null) {
218 | throw new IllegalStateException("The specified child already has a parent. " +
219 | "You must call removeView() on the child's parent first.");
220 | }
221 |
222 | // 调用checkLayoutParams判断当前的params是不是我们需要的LayoutParams,
223 | // 很多继承自ViewGroup的布局都会用自己的LayoutParams,并有独立的一些布局属性。
224 | // 如果不是当前布局所需的LayoutParams则调用generateLayoutParams来转换
225 | if (!checkLayoutParams(params)) {
226 | params = generateLayoutParams(params);
227 | }
228 |
229 | // 给子View设置LayoutParams
230 | if (preventRequestLayout) {
231 | child.mLayoutParams = params;
232 | } else {
233 | child.setLayoutParams(params);
234 | }
235 |
236 | if (index < 0) {
237 | index = mChildrenCount;
238 | }
239 |
240 | // 添加到子View的数组中
241 | addInArray(child, index);
242 |
243 | // 给子View设置parent
244 | if (preventRequestLayout) {
245 | child.assignParent(this);
246 | } else {
247 | child.mParent = this;
248 | }
249 |
250 | // 判断并设置焦点
251 | if (child.hasFocus()) {
252 | requestChildFocus(child, child.findFocus());
253 | }
254 |
255 | // 判断AttachInfo是否为空,AttachInfo不为空说明当前的ViewGroup是已经添加到Window上了。
256 | // 调用子View的dispatchAttachedToWindow。通知当前子View已经被添加到Window
257 | AttachInfo ai = mAttachInfo;
258 | if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
259 | boolean lastKeepOn = ai.mKeepScreenOn;
260 | ai.mKeepScreenOn = false;
261 | child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
262 | if (ai.mKeepScreenOn) {
263 | needGlobalAttributesUpdate(true);
264 | }
265 | ai.mKeepScreenOn = lastKeepOn;
266 | }
267 |
268 | if (child.isLayoutDirectionInherited()) {
269 | child.resetRtlProperties();
270 | }
271 |
272 | dispatchViewAdded(child);
273 |
274 | // 判断是否需要通知子View状态改变(state这边指按下、放开、选中等状态)
275 | if ((child.mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE) {
276 | mGroupFlags |= FLAG_NOTIFY_CHILDREN_ON_DRAWABLE_STATE_CHANGE;
277 | }
278 |
279 | ...
280 | }
281 | ```
282 |
283 |
284 |
285 | 子View的移除则是最终调用了私有方法removeViewInternal:
286 |
287 |
288 |
289 | ```java
290 | private void removeViewInternal(int index, View view) {
291 | if (mTransition != null) {
292 | mTransition.removeChild(this, view);
293 | }
294 |
295 | boolean clearChildFocus = false;
296 |
297 | // 判断并清除焦点
298 | if (view == mFocused) {
299 | view.unFocus(null);
300 | clearChildFocus = true;
301 | }
302 |
303 | view.clearAccessibilityFocus();
304 |
305 | // 取消相关的事件Target
306 | cancelTouchTarget(view);
307 | cancelHoverTarget(view);
308 |
309 | // 当子View自身还有动画没有结束的时候,把子View添加到disappearingChildren列表中,
310 | // 在disappearingChildren列表中的子View会在动画结束后被移除
311 | if (view.getAnimation() != null ||
312 | (mTransitioningViews != null && mTransitioningViews.contains(view))) {
313 | addDisappearingView(view);
314 | } else if (view.mAttachInfo != null) {
315 | // 子View自身没有动画在执行中,通知子View从Window中脱离
316 | view.dispatchDetachedFromWindow();
317 | }
318 |
319 | if (view.hasTransientState()) {
320 | childHasTransientStateChanged(view, false);
321 | }
322 |
323 | needGlobalAttributesUpdate(false);
324 |
325 | // 把子View移出子View数组
326 | removeFromArray(index);
327 |
328 | if (clearChildFocus) {
329 | clearChildFocus(view);
330 | if (!rootViewRequestFocus()) {
331 | notifyGlobalFocusCleared(this);
332 | }
333 | }
334 |
335 | dispatchViewRemoved(view);
336 |
337 | if (view.getVisibility() != View.GONE) {
338 | notifySubtreeAccessibilityStateChangedIfNeeded();
339 | }
340 |
341 | ...
342 | }
343 | ```
344 |
345 |
346 |
347 |
348 |
349 | ### 绘制
350 |
351 | ViewGroup在默认的情况下自身并不绘制内容,而是调用dispatchDraw方法来安排子View绘制自身,我们来看看这个方法具体做了什么:
352 |
353 |
354 |
355 | ```java
356 | protected void dispatchDraw(Canvas canvas) {
357 | boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
358 | final int childrenCount = mChildrenCount;
359 | final View[] children = mChildren;
360 | int flags = mGroupFlags;
361 |
362 | // 检测是否有布局动画
363 | if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
364 | final boolean buildCache = !isHardwareAccelerated();
365 |
366 | // 给所有当前可见的子View绑定布局动画
367 | for (int i = 0; i < childrenCount; i++) {
368 | final View child = children[i];
369 | if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
370 | final LayoutParams params = child.getLayoutParams();
371 | attachLayoutAnimationParameters(child, params, i, childrenCount);
372 | bindLayoutAnimation(child);
373 | }
374 | }
375 |
376 | final LayoutAnimationController controller = mLayoutAnimationController;
377 | if (controller.willOverlap()) {
378 | mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
379 | }
380 |
381 | // 启动布局动画
382 | controller.start();
383 |
384 | mGroupFlags &= ~FLAG_RUN_ANIMATION;
385 | mGroupFlags &= ~FLAG_ANIMATION_DONE;
386 |
387 | if (mAnimationListener != null) {
388 | mAnimationListener.onAnimationStart(controller.getAnimation());
389 | }
390 | }
391 |
392 | // 处理clipToPadding的情况,这边是直接调用canvas的clipRect方法来剪切出除去padding的区域
393 | int clipSaveCount = 0;
394 | final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
395 | if (clipToPadding) {
396 | clipSaveCount = canvas.save();
397 | canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
398 | mScrollX + mRight - mLeft - mPaddingRight,
399 | mScrollY + mBottom - mTop - mPaddingBottom);
400 | }
401 |
402 | ...
403 |
404 | for (int i = 0; i < childrenCount; i++) {
405 | ...
406 |
407 | final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
408 | final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
409 | if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
410 | // 子View是可见状态或者子View的动画还在运行的时候,调用drawChild来绘制子View,
411 | // drawChild方法内部则是直接调用子View的draw(canvas, parent, draingTime)方法让子View对自身进行绘制
412 | more |= drawChild(canvas, child, drawingTime);
413 | }
414 | }
415 | ...
416 | if (preorderedList != null) preorderedList.clear();
417 |
418 | // 绘制那些即将消失的View,
419 | // 所有被移除或者Visibility不是VISIBLE但是自身还有动画没有完成的子View,
420 | // 都会被添加到mDisappearingChildren里面,等动画完成后才被移除。
421 | if (mDisappearingChildren != null) {
422 | final ArrayList disappearingChildren = mDisappearingChildren;
423 | final int disappearingCount = disappearingChildren.size() - 1;
424 | for (int i = disappearingCount; i >= 0; i--) {
425 | final View child = disappearingChildren.get(i);
426 | more |= drawChild(canvas, child, drawingTime);
427 | }
428 | }
429 | if (usingRenderNodeProperties) canvas.insertInorderBarrier();
430 |
431 | // 绘制界面边界(开发者选项中开启显示边界后,实际处理相关的边界绘制的即是在这边)
432 | if (debugDraw()) {
433 | onDebugDraw(canvas);
434 | }
435 |
436 | if (clipToPadding) {
437 | canvas.restoreToCount(clipSaveCount);
438 | }
439 |
440 | flags = mGroupFlags;
441 |
442 | if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
443 | invalidate(true);
444 | }
445 |
446 | if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
447 | mLayoutAnimationController.isDone() && !more) {
448 | mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
449 | final Runnable end = new Runnable() {
450 | @Override
451 | public void run() {
452 | notifyAnimationListener();
453 | }
454 | };
455 | post(end);
456 | }
457 | }
458 | ```
459 |
460 |
461 |
462 | dispatchDraw方法里面做的事情还是比较简单的,如果有布局动画则把布局动画绑定到子View上,然后接着便进行子View的绘制。而绑定到子View上的布局动画或者子View自身设置的动画以及View自身的绘制区域裁剪(这边比较让人想不通的是android的clipChildren,也就是对子View绘制区域裁剪是在ViewGroup中统一设置的,一个ViewGroup下的所有子View只能同时处于裁剪或者不裁剪的状态,但是具体执行却是在子View中。应该设置成类似iOS的UIView,每个子View都能控制自己是否被裁剪会比较合理),都是在子View的draw(canvas, parent, drawingTime)方法里面进行的。
463 |
464 |
465 |
466 | ## 事件
467 |
468 |
469 |
470 | ### 触摸事件
471 |
472 | Android的触摸事件是Android开发进阶必须了解的机制以及面试中必问的问题之一。涉及的方法有dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。而这一整套机制的真正实现,便是在ViewGroup的dispatchTouchEvent内部。下面是简化后的触摸事件流程图:
473 |
474 | 
475 |
476 |
477 |
478 | 触摸事件发生后,在Activity内最先接收到事件的是Activity自身的dispatchTouchEvent,然后Activity传递给Activity的Window。接着Window传递给最顶端的View,也就是DecorView。接下来才是我们熟悉的触摸事件流程:首先是最顶端的ViewGroup(这边便是DecorView)的dispatchTouchEvent接收到事件。并通过onInterceptTouchEvent判断是否需要拦截。如果拦截则分配到ViewGroup自身的onTouchEvent,如果不拦截则查找位于点击区域的子View(当事件是ACTION_DOWN的时候,会做一次查找并根据查找到的子View设定一个TouchTarget,有了TouchTarget以后,后续的对应id的事件如果不被拦截都会分发给这一个TouchTarget)。查找到子View以后则调用dispatchTransformedTouchEvent把MotionEvent的坐标转换到子View的坐标空间,这不仅仅是x,y的偏移,还包括根据子View自身矩阵的逆矩阵对坐标进行变换(这就是使用setTranslationX,setScaleX等方法调用后,子View的点击区域还能保持和自身绘制内容一致的原因。使用Animation做变换点击区域不同步是因为Animation使用的是Canvas的矩阵而不是View自身的矩阵来做变换)。
479 |
480 |
481 |
482 | 下面我们来看一下ViewGroup的dispatchTouchEvent方法的源码:
483 |
484 |
485 |
486 | ```java
487 | public boolean dispatchTouchEvent(MotionEvent ev) {
488 | ...
489 |
490 | boolean handled = false;
491 | if (onFilterTouchEventForSecurity(ev)) {
492 | final int action = ev.getAction();
493 | final int actionMasked = action & MotionEvent.ACTION_MASK;
494 |
495 | if (actionMasked == MotionEvent.ACTION_DOWN) {
496 | // 触摸事件流开始,重置触摸相关的状态
497 | cancelAndClearTouchTargets(ev);
498 | resetTouchState();
499 | }
500 |
501 | // 检测当前是否需要拦截事件。
502 | final boolean intercepted;
503 | if (actionMasked == MotionEvent.ACTION_DOWN
504 | || mFirstTouchTarget != null) {
505 |
506 | // 处理调用requestDisallowInterceptTouchEvent来防止ViewGroup拦截事件的情况
507 | final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
508 | if (!disallowIntercept) {
509 | intercepted = onInterceptTouchEvent(ev);
510 | ev.setAction(action);
511 | } else {
512 | intercepted = false;
513 | }
514 | } else {
515 | // 当前没有TouchTarget也不是事件流的起始的话,则直接默认拦截,不通过onInterceptTouchEvent判断。
516 | intercepted = true;
517 | }
518 |
519 | final boolean canceled = resetCancelNextUpFlag(this)
520 | || actionMasked == MotionEvent.ACTION_CANCEL;
521 |
522 | // 检测是否需要把多点触摸事件分配给不同的子View
523 | final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
524 |
525 | // 当前事件流对应的TouchTarget对象
526 | TouchTarget newTouchTarget = null;
527 | boolean alreadyDispatchedToNewTouchTarget = false;
528 | if (!canceled && !intercepted) {
529 |
530 | View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
531 | ? findChildWithAccessibilityFocus() : null;
532 |
533 | if (actionMasked == MotionEvent.ACTION_DOWN
534 | || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
535 | || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
536 | final int actionIndex = ev.getActionIndex(); // always 0 for down
537 | final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
538 | : TouchTarget.ALL_POINTER_IDS;
539 |
540 | // 当前事件是事件流的初始事件(包括多点触摸时第二、第三点灯的DOWN事件),清除之前相应的TouchTarget的状态
541 | removePointersFromTouchTargets(idBitsToAssign);
542 |
543 | final int childrenCount = mChildrenCount;
544 | if (newTouchTarget == null && childrenCount != 0) {
545 | final float x = ev.getX(actionIndex);
546 | final float y = ev.getY(actionIndex);
547 | final ArrayList preorderedList = buildTouchDispatchChildList();
548 | final boolean customOrder = preorderedList == null
549 | && isChildrenDrawingOrderEnabled();
550 | final View[] children = mChildren;
551 | for (int i = childrenCount - 1; i >= 0; i--) {
552 | final int childIndex = getAndVerifyPreorderedIndex(
553 | childrenCount, i, customOrder);
554 | final View child = getAndVerifyPreorderedView(
555 | preorderedList, children, childIndex);
556 |
557 | ...
558 |
559 | // 判断当前遍历到的子View能否接受事件,如果不能则直接continue进入下一次循环
560 | if (!canViewReceivePointerEvents(child)
561 | || !isTransformedTouchPointInView(x, y, child, null)) {
562 | ev.setTargetAccessibilityFocus(false);
563 | continue;
564 | }
565 |
566 | // 当前子View能接收事件,为子View创建TouchTarget
567 | newTouchTarget = getTouchTarget(child);
568 | if (newTouchTarget != null) {
569 | newTouchTarget.pointerIdBits |= idBitsToAssign;
570 | break;
571 | }
572 |
573 | resetCancelNextUpFlag(child);
574 | // 调用dispatchTransformedTouchEvent把事件分配给子View
575 | if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
576 | mLastTouchDownTime = ev.getDownTime();
577 | if (preorderedList != null) {
578 | for (int j = 0; j < childrenCount; j++) {
579 | if (children[childIndex] == mChildren[j]) {
580 | mLastTouchDownIndex = j;
581 | break;
582 | }
583 | }
584 | } else {
585 | mLastTouchDownIndex = childIndex;
586 | }
587 | mLastTouchDownX = ev.getX();
588 | mLastTouchDownY = ev.getY();
589 |
590 | // 把TouchTarget添加到TouchTarget列表的第一位
591 | newTouchTarget = addTouchTarget(child, idBitsToAssign);
592 | alreadyDispatchedToNewTouchTarget = true;
593 | break;
594 | }
595 |
596 | ev.setTargetAccessibilityFocus(false);
597 | }
598 | if (preorderedList != null) preorderedList.clear();
599 | }
600 |
601 | if (newTouchTarget == null && mFirstTouchTarget != null) {
602 | newTouchTarget = mFirstTouchTarget;
603 | while (newTouchTarget.next != null) {
604 | newTouchTarget = newTouchTarget.next;
605 | }
606 | newTouchTarget.pointerIdBits |= idBitsToAssign;
607 | }
608 | }
609 | }
610 |
611 | if (mFirstTouchTarget == null) {
612 | // 目前没有任何TouchTarget,所以直接传null给dispatchTransformedTouchEvent
613 | handled = dispatchTransformedTouchEvent(ev, canceled, null,
614 | TouchTarget.ALL_POINTER_IDS);
615 | } else {
616 | // 把事件根据pointer id分发给TouchTarget列表内的所有TouchTarget,用来处理多点触摸的情况
617 | TouchTarget predecessor = null;
618 | TouchTarget target = mFirstTouchTarget;
619 | // 遍历TouchTarget列表
620 | while (target != null) {
621 | final TouchTarget next = target.next;
622 | if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
623 | handled = true;
624 | } else {
625 | final boolean cancelChild = resetCancelNextUpFlag(target.child)
626 | || intercepted;
627 |
628 | // 根据TouchTarget的pointerIdBits来执行dispatchTransformedTouchEvent
629 | if (dispatchTransformedTouchEvent(ev, cancelChild,
630 | target.child, target.pointerIdBits)) {
631 | handled = true;
632 | }
633 | if (cancelChild) {
634 | if (predecessor == null) {
635 | mFirstTouchTarget = next;
636 | } else {
637 | predecessor.next = next;
638 | }
639 | target.recycle();
640 | target = next;
641 | continue;
642 | }
643 | }
644 | predecessor = target;
645 | target = next;
646 | }
647 | }
648 |
649 | // 处理CANCEL和UP事件的情况
650 | if (canceled
651 | || actionMasked == MotionEvent.ACTION_UP
652 | || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
653 | resetTouchState();
654 | } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
655 | final int actionIndex = ev.getActionIndex();
656 | final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
657 | removePointersFromTouchTargets(idBitsToRemove);
658 | }
659 | }
660 |
661 | if (!handled && mInputEventConsistencyVerifier != null) {
662 | mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
663 | }
664 | return handled;
665 | }
666 | ```
667 |
668 |
669 |
670 | 再来看一下dispatchTransformedMotionEvent方法:
671 |
672 |
673 |
674 | ```java
675 | private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
676 | View child, int desiredPointerIdBits) {
677 | final boolean handled;
678 |
679 | final int oldAction = event.getAction();
680 | // 处理CANCEL的情况,直接把MotionEvent的原始数据分发给子View或者自身的onTouchEvent
681 | // (这边调用View.dispatchTouchEvent,而View.dispatchTouchEvent会再调用onTouchEvent方法,把MotionEvent传入)
682 | if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
683 | event.setAction(MotionEvent.ACTION_CANCEL);
684 | if (child == null) {
685 | handled = super.dispatchTouchEvent(event);
686 | } else {
687 | handled = child.dispatchTouchEvent(event);
688 | }
689 | event.setAction(oldAction);
690 | return handled;
691 | }
692 |
693 | // 对MotionEvent自身的pointer id和当前我们需要处理的pointer id做按位与,得到共有的pointer id
694 | final int oldPointerIdBits = event.getPointerIdBits();
695 | final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
696 |
697 | // 没有pointer id需要处理,直接返回
698 | if (newPointerIdBits == 0) {
699 | return false;
700 | }
701 |
702 | final MotionEvent transformedEvent;
703 | if (newPointerIdBits == oldPointerIdBits) {
704 | // MotionEvent自身的pointer id和当前处理pointer id相同
705 | if (child == null || child.hasIdentityMatrix()) {
706 | if (child == null) {
707 | // 子View为空,直接交还给自身的onTouchEvent处理
708 | handled = super.dispatchTouchEvent(event);
709 | } else {
710 | // 子View矩阵是单位矩阵,说明子View并没有做过任何变换,直接对x、y做偏移并分配给子View处理
711 | final float offsetX = mScrollX - child.mLeft;
712 | final float offsetY = mScrollY - child.mTop;
713 | event.offsetLocation(offsetX, offsetY);
714 |
715 | handled = child.dispatchTouchEvent(event);
716 |
717 | event.offsetLocation(-offsetX, -offsetY);
718 | }
719 | return handled;
720 | }
721 | transformedEvent = MotionEvent.obtain(event);
722 | } else {
723 | // MotionEvent自身的pointer id和当前需要处理的pointer id不同,把不需要处理的pointer id相关的信息剔除掉。
724 | transformedEvent = event.split(newPointerIdBits);
725 | }
726 |
727 | if (child == null) {
728 | // 子View为空,直接交还给自身的onTouchEvent处理
729 | handled = super.dispatchTouchEvent(transformedEvent);
730 | } else {
731 | // 根据当前的scrollX、scrollY和子View的left、top对MotionEvent的触摸坐标x、y进行偏移
732 | final float offsetX = mScrollX - child.mLeft;
733 | final float offsetY = mScrollY - child.mTop;
734 | transformedEvent.offsetLocation(offsetX, offsetY);
735 | if (! child.hasIdentityMatrix()) {
736 | // 获取子View自身矩阵的逆矩阵,并对MotionEvent的坐标相关信息进行矩阵变换
737 | transformedEvent.transform(child.getInverseMatrix());
738 | }
739 |
740 | // 把经过偏移以及矩阵变换的事件传递给子View处理
741 | handled = child.dispatchTouchEvent(transformedEvent);
742 | }
743 |
744 | transformedEvent.recycle();
745 | return handled;
746 | }
747 | ```
748 |
749 |
750 |
751 | ### 总结
752 |
753 | 当然ViewGroup作为一个"万物之源",还有很多代码值得我们去阅读。通过阅读源码,我们也能发现ViewGroup也是有一些看起来"不太合理"的设计。这边只列出我们平时开发中,最常接触的部分,其他的读者可以自己尝试阅读和理解,毕竟源码才是"第一手资料",其他人的总结分析甚至官方文档都只能算作"二手资料"。 Read the fucking source code!
--------------------------------------------------------------------------------
/article/ViewStub 源码解析.md:
--------------------------------------------------------------------------------
1 | # ViewStub
2 |
3 | ViewStub 是一个看不见的,没有大小,不占布局位置的 View,可以用来懒加载布局。当 ViewStub 变得可见或 ```inflate()``` 的时候,布局就会被加载(替换 ViewStub)。因此,ViewStub 一直存在于视图层次结构中直到调用了 ```setVisibility(int)``` 或 ```inflate()```。
4 |
5 | 我们先来看看构造方法:
6 |
7 | ```java
8 | public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
9 | super(context);
10 |
11 | final TypedArray a = context.obtainStyledAttributes(attrs,
12 | R.styleable.ViewStub, defStyleAttr, defStyleRes);
13 | // 要被加载的布局 Id
14 | mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
15 | // 要被加载的布局
16 | mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
17 | // ViewStub 的 Id
18 | mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
19 | a.recycle();
20 |
21 | // 初始状态为 GONE
22 | setVisibility(GONE);
23 | // 设置为不会绘制
24 | setWillNotDraw(true);
25 | }
26 | ```
27 |
28 | 接下来就看看关键的方法,ViewStub 很轻量,代码真的很少,很值得去看看具体实现,花不了多少时间。
29 |
30 | ```java
31 | // 复写了 setVisibility(int) 方法
32 | @Override
33 | @android.view.RemotableViewMethod
34 | public void setVisibility(int visibility) {
35 | // private WeakReference mInflatedViewRef;
36 | // mInflatedViewRef 是对布局的弱引用
37 | if (mInflatedViewRef != null) {
38 | // 如果不为 null,就拿到懒加载的 View
39 | View view = mInflatedViewRef.get();
40 | if (view != null) {
41 | // 然后就直接对 View 进行 setVisibility 操作
42 | view.setVisibility(visibility);
43 | } else {
44 | // 如果为 null,就抛出异常
45 | throw new IllegalStateException("setVisibility called on un-referenced view");
46 | }
47 | } else {
48 | super.setVisibility(visibility);
49 | // 之前说过,setVisibility(int) 也可以进行加载布局
50 | if (visibility == VISIBLE || visibility == INVISIBLE) {
51 | // 因为在这里调用了 inflate()
52 | inflate();
53 | }
54 | }
55 | }
56 | ```
57 |
58 | inflate() 是关键的加载实现
59 |
60 | ```java
61 | public View inflate() {
62 | // 获取父视图
63 | final ViewParent viewParent = getParent();
64 |
65 | if (viewParent != null && viewParent instanceof ViewGroup) {
66 | // 如果没有指定布局,就会抛出异常
67 | if (mLayoutResource != 0) {
68 | // viewParent 需为 ViewGroup
69 | final ViewGroup parent = (ViewGroup) viewParent;
70 | final LayoutInflater factory;
71 | if (mInflater != null) {
72 | factory = mInflater;
73 | } else {
74 | // 如果没有指定 LayoutInflater
75 | factory = LayoutInflater.from(mContext);
76 | }
77 | // 获取布局
78 | final View view = factory.inflate(mLayoutResource, parent,
79 | false);
80 | // 为 view 设置 Id
81 | if (mInflatedId != NO_ID) {
82 | view.setId(mInflatedId);
83 | }
84 | // 计算出 ViewStub 在 parent 中的位置
85 | final int index = parent.indexOfChild(this);
86 | // 把 ViewStub 从 parent 中移除
87 | parent.removeViewInLayout(this);
88 |
89 | // 接下来就是把 view 加到 parent 的 index 位置中
90 | final ViewGroup.LayoutParams layoutParams = getLayoutParams();
91 | if (layoutParams != null) {
92 | // 如果 ViewStub 的 layoutParams 不为空
93 | // 就设置给 view
94 | parent.addView(view, index, layoutParams);
95 | } else {
96 | parent.addView(view, index);
97 | }
98 |
99 | // mInflatedViewRef 就是在这里对 view 进行了弱引用
100 | mInflatedViewRef = new WeakReference(view);
101 |
102 | if (mInflateListener != null) {
103 | // 回调
104 | mInflateListener.onInflate(this, view);
105 | }
106 |
107 | return view;
108 | } else {
109 | throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
110 | }
111 | } else {
112 | throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
113 | }
114 | }
115 | ```
116 |
117 | 好了,主要的都分析完了,知道原理之后就可以自己动手写一个加强版的 ViewStub 了,例如我以前写的一个 [StateView](https://github.com/nukc/StateView)
--------------------------------------------------------------------------------
/article/images/fontmetrics.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/6d9b5eab580d2cd9a8263f8f5dec10185a205cdf/article/images/fontmetrics.gif
--------------------------------------------------------------------------------
/article/images/generate_child_measurespec.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/6d9b5eab580d2cd9a8263f8f5dec10185a205cdf/article/images/generate_child_measurespec.png
--------------------------------------------------------------------------------
/article/images/viewgroup_lifecycle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/6d9b5eab580d2cd9a8263f8f5dec10185a205cdf/article/images/viewgroup_lifecycle.png
--------------------------------------------------------------------------------
/article/images/viewgroup_touchevent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/6d9b5eab580d2cd9a8263f8f5dec10185a205cdf/article/images/viewgroup_touchevent.png
--------------------------------------------------------------------------------
/article/imgs-navigationview/nv-sample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/6d9b5eab580d2cd9a8263f8f5dec10185a205cdf/article/imgs-navigationview/nv-sample.png
--------------------------------------------------------------------------------
/article/imgs-navigationview/nv-sample_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/6d9b5eab580d2cd9a8263f8f5dec10185a205cdf/article/imgs-navigationview/nv-sample_02.png
--------------------------------------------------------------------------------
/article/imgs-navigationview/nv-sample_03.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/6d9b5eab580d2cd9a8263f8f5dec10185a205cdf/article/imgs-navigationview/nv-sample_03.gif
--------------------------------------------------------------------------------
/article/textview源碼解析.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # TextView源码解析
4 |
5 | ***
6 | ## 1.简介
7 | ***
8 | > TextView作为Android系统上显示和排版文字以及提供对文字的增删改查、图文混排等功能的控件,内部是相对比较复杂的。这么一个复杂的控件自然需要依赖于一些其他的辅助类,例如:Layout以及Layout的相关子类、Span相关的类、MovementMethod接口、TransformationMethod接口等。这篇文章主要介绍TextView的结构和内部处理文字的流程以及TextView相关的辅助类在TextView处理文字过程中的作用。
9 |
10 | ## 2.TextView的内部结构和辅助类
11 | ***
12 | TextView内部除了继承自View的相关属性和measure、layout、draw步骤,还包括:
13 |
14 | 1. **Layout**: TextView的文字排版、折行策略以及文本绘制都是在Layout里面完成的,TextView的自身测量也受Layout的影响。Layout是TextView执行setText方法后,由TextView内部创建的实例,并不能由外部提供。可以用getLayout()方法获取。
15 | 2. **TransformationMethod**: 用来处理最终的显示结果的类,例如显示密码的时候把密码转换成圆点。这个类并不直接影响TextView内部储存的Text,只影响显示的结果。
16 | 3. **MovementMethod**: 用来处理TextView内部事件响应的类,可以针对TextView内文本的某一个区域做软键盘输入或者触摸事件的响应。
17 | 4. **Drawables**: TextView的静态内部类,用来处理和储存TextView的CompoundDrawables,包括TextView的上下左右的Drawable以及错误提示的Drawable。
18 | 5. **Spans**: Spans并不是特定的某一个类或者实现了某一个接口的类。它可以是任意类型,Spans实际上做的事情是在TextView的内部的text的某一个区域做标记。其中有部分Spans可以影响TextView的绘制和测量,如ImageSpan、BackgroundColorSpan、AbsoluteSizeSpan。还有可以响应点击事件的ClickableSpan。
19 | 6. **Editor**: TextView作为可编辑文本控件的时候(EditText),使用Editor来处理文本的区域选择处理和判断、拼写检查、弹出文本菜单等。
20 | 7. **InputConnection**: EditText的文本输入部分是在TextView中完成的。而InputConnection是软键盘和TextView之间的桥梁,所有的软键盘的输入文字、修改文字和删除文字都是通过InputConnection传递给TextView的。
21 |
22 | ## 3.TextView的onTouchEvent处理
23 |
24 | ***
25 |
26 | TextView内部能处理触摸事件的,包括自身的触摸处理、Editor的onTouchEvent、MovementMethod的onTouchEvent。Editor的onTouchEvent主要处理出于编辑状态下的触摸事件,比如点击选中、长按等。MovementMethod则主要负责文本内部有Span的时候的相关处理,比较常见的就是LinkMovementMethod处理ClickableSpan的点击事件。我们来看一下TextView内部对这些触摸事件的处理和优先级的分配:
27 |
28 | ```java
29 | public boolean onTouchEvent(MotionEvent event) {
30 | final int action = event.getActionMasked();
31 |
32 | //当Editor不为空的时候,给Editor的双击事件预设值
33 | if (mEditor != null && action == MotionEvent.ACTION_DOWN) {
34 | if (mFirstTouch && (SystemClock.uptimeMillis() - mLastTouchUpTime) <=
35 | ViewConfiguration.getDoubleTapTimeout()) {
36 | mEditor.mDoubleTap = true;
37 | mFirstTouch = false;
38 | } else {
39 | mEditor.mDoubleTap = false;
40 | mFirstTouch = true;
41 | }
42 | }
43 |
44 | if (action == MotionEvent.ACTION_UP) {
45 | mLastTouchUpTime = SystemClock.uptimeMillis();
46 | }
47 |
48 | //当Editor不为空,优先处理Editor的触摸事件
49 | if (mEditor != null) {
50 | mEditor.onTouchEvent(event);
51 |
52 | //由于Editor内部onTouchEvent实际上交给了mSelectionModifierCursorController处理,所以这边判断mSelectionModifierCursorController是否需要处理接下来的一系列事件,如果是则直接返回跳过下面的步骤
53 | if (mEditor.mSelectionModifierCursorController != null &&
54 | mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) {
55 | return true;
56 | }
57 | }
58 |
59 | final boolean superResult = super.onTouchEvent(event);
60 |
61 | //处理API 23新加入的InsertionActinoMode
62 | if (mEditor != null && mEditor.mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
63 | mEditor.mDiscardNextActionUp = false;
64 |
65 | if (mEditor.mIsInsertionActionModeStartPending) {
66 | mEditor.startInsertionActionMode();
67 | mEditor.mIsInsertionActionModeStartPending = false;
68 | }
69 | return superResult;
70 | }
71 |
72 | final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) &&
73 | (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();
74 |
75 | if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
76 | && mText instanceof Spannable && mLayout != null) {
77 | boolean handled = false;
78 |
79 | //MovementMethod的触摸时间处理,如果MovementMethod类型是LinkMovementMethod则会处理文本内的所有ClickableSpan的点击
80 | if (mMovement != null) {
81 | handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
82 | }
83 |
84 | final boolean textIsSelectable = isTextSelectable();
85 | if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
86 |
87 | //在文本可选择的情况下,默认是没有LinkMovementMethod来处理ClickableSpan相关的点击的,所以在文本可选择情况,TextView对所有的ClickableSpan进行统一处理
88 | ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
89 | getSelectionEnd(), ClickableSpan.class);
90 |
91 | if (links.length > 0) {
92 | links[0].onClick(this);
93 | handled = true;
94 | }
95 | }
96 |
97 | if (touchIsFinished && (isTextEditable() || textIsSelectable)) {
98 | final InputMethodManager imm = InputMethodManager.peekInstance();
99 | viewClicked(imm);
100 | if (!textIsSelectable && mEditor.mShowSoftInputOnFocus) {
101 | handled |= imm != null && imm.showSoftInput(this, 0);
102 | }
103 |
104 | mEditor.onTouchUpEvent(event);
105 |
106 | handled = true;
107 | }
108 |
109 | if (handled) {
110 | return true;
111 | }
112 | }
113 |
114 | return superResult;
115 | }
116 | ```
117 |
118 | ## 4.TextView的创建Layout的过程
119 |
120 | ***
121 |
122 | TextView内部并不仅仅只有一个用来显示文本内容的Layout,在设置了hint的时候,还需要有一个mHintLayout来处理hint的内容。如果设置了Ellipsize类型为Marquee时,还会有一个mSavedMarqueeModeLayout专门用来显示marquee效果。这些Layout都是通过内部的makeNewLayout方法来创建的:
123 |
124 | ```java
125 | protected void makeNewLayout(int wantWidth, int hintWidth
126 | BoringLayout.Metrics boring,
127 | BoringLayout.Metrics hintBoring,
128 | int ellipsisWidth, boolean bringIntoView) {
129 | //如果当前有marquee动画,则先停止动画
130 | stopMarquee();
131 |
132 | mOldMaximum = mMaximum;
133 | mOldMaxMode = mMaxMode;
134 |
135 | mHighlightPathBogus = true;
136 |
137 | if (wantWidth < 0) {
138 | wantWidth = 0;
139 | }
140 | if (hintWidth < 0) {
141 | hintWidth = 0;
142 | }
143 |
144 | //文本对齐方式
145 | Layout.Alignment alignment = getLayoutAlignment();
146 | final boolean testDirChange = mSingleLine && mLayout != null &&
147 | (alignment == Layout.Alignment.ALIGN_NORMAL ||
148 | alignment == Layout.Alignment.ALIGN_OPPOSITE);
149 | int oldDir = 0;
150 | if (testDirChange) oldDir = mLayout.getParagraphDirection(0);
151 |
152 | //检测是否设置了ellipsize
153 | boolean shouldEllipsize = mEllipsize != null && getKeyListener() == null;
154 | final boolean switchEllipsize = mEllipsize == TruncateAt.MARQUEE &&
155 | mMarqueeFadeMode != MARQUEE_FADE_NORMAL;
156 | TruncateAt effectiveEllipsize = mEllipsize;
157 | if (mEllipsize == TruncateAt.MARQUEE &&
158 | mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
159 | effectiveEllipsize = TruncateAt.END_SMALL;
160 | }
161 |
162 | //文本方向
163 | if (mTextDir == null) {
164 | mTextDir = getTextDirectionHeuristic();
165 | }
166 |
167 | //创建主Layout
168 | mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
169 | effectiveEllipsize, effectiveEllipsize == mEllipsize);
170 |
171 | //非常规的Marquee模式下,需要创建mSavedMarqueeModeLayout来保存marquee动画时所用的Layout,并且在动画期间把它和TextView的主Layout对换
172 | if (switchEllipsize) {
173 | TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE ?
174 | TruncateAt.END : TruncateAt.MARQUEE;
175 | mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
176 | shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
177 | }
178 |
179 | shouldEllipsize = mEllipsize != null;
180 | mHintLayout = null;
181 |
182 | //判断是否需要创建hintLayout
183 | if (mHint != null) {
184 | if (shouldEllipsize) hintWidth = wantWidth;
185 |
186 | if (hintBoring == UNKNOWN_BORING) {
187 | hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
188 | mHintBoring);
189 | if (hintBoring != null) {
190 | mHintBoring = hintBoring;
191 | }
192 | }
193 |
194 | //判断是否为boring,如果是则创建BoringLayout
195 | if (hintBoring != null) {
196 | if (hintBoring.width <= hintWidth &&
197 | (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
198 | if (mSavedHintLayout != null) {
199 | mHintLayout = mSavedHintLayout.
200 | replaceOrMake(mHint, mTextPaint,
201 | hintWidth, alignment, mSpacingMult, mSpacingAdd,
202 | hintBoring, mIncludePad);
203 | } else {
204 | mHintLayout = BoringLayout.make(mHint, mTextPaint,
205 | hintWidth, alignment, mSpacingMult, mSpacingAdd,
206 | hintBoring, mIncludePad);
207 | }
208 |
209 | mSavedHintLayout = (BoringLayout) mHintLayout;
210 | } else if (shouldEllipsize && hintBoring.width <= hintWidth) {
211 | if (mSavedHintLayout != null) {
212 | mHintLayout = mSavedHintLayout.
213 | replaceOrMake(mHint, mTextPaint,
214 | hintWidth, alignment, mSpacingMult, mSpacingAdd,
215 | hintBoring, mIncludePad, mEllipsize,
216 | ellipsisWidth);
217 | } else {
218 | mHintLayout = BoringLayout.make(mHint, mTextPaint,
219 | hintWidth, alignment, mSpacingMult, mSpacingAdd,
220 | hintBoring, mIncludePad, mEllipsize,
221 | ellipsisWidth);
222 | }
223 | }
224 | }
225 |
226 | //不是boring的状态下,用StaticLayout来创建
227 | if (mHintLayout == null) {
228 | StaticLayout.Builder builder = StaticLayout.Builder.obtain(mHint, 0,
229 | mHint.length(), mTextPaint, hintWidth)
230 | .setAlignment(alignment)
231 | .setTextDirection(mTextDir)
232 | .setLineSpacing(mSpacingAdd, mSpacingMult)
233 | .setIncludePad(mIncludePad)
234 | .setBreakStrategy(mBreakStrategy)
235 | .setHyphenationFrequency(mHyphenationFrequency);
236 | if (shouldEllipsize) {
237 | builder.setEllipsize(mEllipsize)
238 | .setEllipsizedWidth(ellipsisWidth)
239 | .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
240 | }
241 | mHintLayout = builder.build();
242 | }
243 | }
244 |
245 | if (bringIntoView || (testDirChange && oldDir != mLayout.getParagraphDirection(0))) {
246 | registerForPreDraw();
247 | }
248 |
249 | //判断是否需要开始Marquee动画
250 | if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
251 | if (!compressText(ellipsisWidth)) {
252 | final int height = mLayoutParams.height;
253 | if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
254 | startMarquee();
255 | } else {
256 | mRestartMarquee = true;
257 | }
258 | }
259 | }
260 |
261 | if (mEditor != null) mEditor.prepareCursorControllers();
262 | }
263 | ```
264 |
265 | TextView的布局创建过程涉及到一个boring的概念,boring是指布局所用的文本里面不包含任何Span,所有的文本方向都是从左到右的布局,并且仅需一行就能显示完全的布局。这种情况下,TextView会使用BoringLayout类来创建相关的布局,以节省不必要的文本测量以及文本折行、Span宽度、文本方向等的计算。下面我们来看一下makeNewLayout中使用频率比较高的makeSingleLayout的代码:
266 |
267 | ```java
268 | private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
269 | Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
270 | boolean useSaved) {
271 | Layout result = null;
272 | //判断是否Spannable,如果是则用DynamicLayout类来创建布局,DynamicLayout内部实际也是使用StaticLayout来做文本的测量绘制,并在StaticLayout的基础上增加了文本或者Span改变时的监听,及时对文本或者Span的变化做出反应。
273 | if (mText instanceof Spannable) {
274 | result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
275 | alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad,
276 | mBreakStrategy, mHyphenationFrequency,
277 | getKeyListener() == null ? effectiveEllipsize : null, ellipsisWidth);
278 | } else {
279 | //如果boring是未知状态,则重新判断一次是否boring
280 | if (boring == UNKNOWN_BORING) {
281 | boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
282 | if (boring != null) {
283 | mBoring = boring;
284 | }
285 | }
286 |
287 | //根据boring的属性来创建对应的布局,如果有mSavedLayout则从mSavedLayout创建
288 | if (boring != null) {
289 | if (boring.width <= wantWidth &&
290 | (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
291 | if (useSaved && mSavedLayout != null) {
292 | //从之前保存的Layout中创建
293 | result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
294 | wantWidth, alignment, mSpacingMult, mSpacingAdd,
295 | boring, mIncludePad);
296 | } else {
297 | //创建新的Layout
298 | result = BoringLayout.make(mTransformed, mTextPaint,
299 | wantWidth, alignment, mSpacingMult, mSpacingAdd,
300 | boring, mIncludePad);
301 | }
302 |
303 | if (useSaved) {
304 | mSavedLayout = (BoringLayout) result;
305 | }
306 | } else if (shouldEllipsize && boring.width <= wantWidth) {
307 | if (useSaved && mSavedLayout != null) {
308 | result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
309 | wantWidth, alignment, mSpacingMult, mSpacingAdd,
310 | boring, mIncludePad, effectiveEllipsize,
311 | ellipsisWidth);
312 | } else {
313 | result = BoringLayout.make(mTransformed, mTextPaint,
314 | wantWidth, alignment, mSpacingMult, mSpacingAdd,
315 | boring, mIncludePad, effectiveEllipsize,
316 | ellipsisWidth);
317 | }
318 | }
319 | }
320 | }
321 |
322 | //如果没有创建BoringLayout, 则使用StaticLayout类来创建布局
323 | if (result == null) {
324 | StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
325 | 0, mTransformed.length(), mTextPaint, wantWidth)
326 | .setAlignment(alignment)
327 | .setTextDirection(mTextDir)
328 | .setLineSpacing(mSpacingAdd, mSpacingMult)
329 | .setIncludePad(mIncludePad)
330 | .setBreakStrategy(mBreakStrategy)
331 | .setHyphenationFrequency(mHyphenationFrequency);
332 | if (shouldEllipsize) {
333 | builder.setEllipsize(effectiveEllipsize)
334 | .setEllipsizedWidth(ellipsisWidth)
335 | .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
336 | }
337 | result = builder.build();
338 | }
339 | return result;
340 | }
341 | ```
342 |
343 | ## 5.TextView的文字处理和绘制
344 |
345 | ---
346 |
347 | TextView主要的文字排版和渲染并不是在TextView里面完成的,而是由Layout类来处理文字排版工作。在单纯地使用TextView来展示静态文本的时候,这件事情则是由Layout的子类StaticLayout来完成的。
348 |
349 | StaticLayout接收到字符串后,首先做的事情是根据字符串里面的换行符对字符串进行拆分。
350 |
351 | ```java
352 | for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
353 | paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
354 | if (paraEnd < 0)
355 | paraEnd = bufEnd;
356 | else
357 | paraEnd++;
358 | ```
359 |
360 | 拆分后的段落(Paragraph)被分配给辅助类MeasuredText进行测量得到每个字符的宽度以及每个段落的FontMetric。并通过LineBreaker进行折行的判断
361 |
362 | ```java
363 | //把段落载入到MeasuredText中,并分配对应的缓存空间
364 | measured.setPara(source, paraStart, paraEnd, textDir, b);
365 | char[] chs = measured.mChars;
366 | float[] widths = measured.mWidths;
367 | byte[] chdirs = measured.mLevels;
368 | int dir = measured.mDir;
369 | boolean easy = measured.mEasy;
370 |
371 | ```
372 |
373 | //把相关属性传给JNI层的LineBreaker
374 | nSetupParagraph(b.mNativePtr, chs, paraEnd - paraStart,
375 | firstWidth, firstWidthLineCount, restWidth,
376 | variableTabStops, TAB_INCREMENT, b.mBreakStrategy, b.mHyphenationFrequency);
377 | ```
378 |
379 | int fmCacheCount = 0;
380 | int spanEndCacheCount = 0;
381 | for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
382 | if (fmCacheCount * 4 >= fmCache.length) {
383 | int[] grow = new int[fmCacheCount * 4 * 2];
384 | System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4);
385 | fmCache = grow;
386 | }
387 |
388 | if (spanEndCacheCount >= spanEndCache.length) {
389 | int[] grow = new int[spanEndCacheCount * 2];
390 | System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount);
391 | spanEndCache = grow;
392 | }
393 |
394 | if (spanned == null) {
395 | spanEnd = paraEnd;
396 | int spanLen = spanEnd - spanStart;
397 | //段落没有Span的情况下,把整个段落交给MeasuredText计算每个字符的宽度和FontMetric
398 | measured.addStyleRun(paint, spanLen, fm);
399 | } else {
400 | spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
401 | MetricAffectingSpan.class);
402 | int spanLen = spanEnd - spanStart;
403 | MetricAffectingSpan[] spans =
404 | spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
405 | spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
406 | //把对排版有影响的Span交给MeasuredText测量宽度并计算FontMetric
407 | measured.addStyleRun(paint, spans, spanLen, fm);
408 | }
409 |
410 | //把测量后的FontMetric缓存下来方便后面使用
411 | fmCache[fmCacheCount * 4 + 0] = fm.top;
412 | fmCache[fmCacheCount * 4 + 1] = fm.bottom;
413 | fmCache[fmCacheCount * 4 + 2] = fm.ascent;
414 | fmCache[fmCacheCount * 4 + 3] = fm.descent;
415 | fmCacheCount++;
416 |
417 | spanEndCache[spanEndCacheCount] = spanEnd;
418 | spanEndCacheCount++;
419 | }
420 |
421 | nGetWidths(b.mNativePtr, widths);
422 | //计算段落中需要折行的位置,并返回折行的数量
423 | int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks,
424 | lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length);
425 | ```
426 |
427 | 计算完每一行的测量相关信息、Span宽高以及折行位置,就可以开始按照最终的行数一行一行地保存下来,以供后面绘制和获取对应文本信息的时候使用。
428 |
429 | ```java
430 | for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
431 | spanEnd = spanEndCache[spanEndCacheIndex++];
432 |
433 | // 获取之前缓存的FontMetric信息
434 | fm.top = fmCache[fmCacheIndex * 4 + 0];
435 | fm.bottom = fmCache[fmCacheIndex * 4 + 1];
436 | fm.ascent = fmCache[fmCacheIndex * 4 + 2];
437 | fm.descent = fmCache[fmCacheIndex * 4 + 3];
438 | fmCacheIndex++;
439 |
440 | if (fm.top < fmTop) {
441 | fmTop = fm.top;
442 | }
443 | if (fm.ascent < fmAscent) {
444 | fmAscent = fm.ascent;
445 | }
446 | if (fm.descent > fmDescent) {
447 | fmDescent = fm.descent;
448 | }
449 | if (fm.bottom > fmBottom) {
450 | fmBottom = fm.bottom;
451 | }
452 |
453 | while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) {
454 | breakIndex++;
455 | }
456 |
457 | while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) {
458 | int endPos = paraStart + breaks[breakIndex];
459 |
460 | boolean moreChars = (endPos < bufEnd);
461 |
462 | //逐行把相关信息储存下来
463 | v = out(source, here, endPos,
464 | fmAscent, fmDescent, fmTop, fmBottom,
465 | v, spacingmult, spacingadd, chooseHt, chooseHtv, fm, flags[breakIndex],
466 | needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad,
467 | chs, widths, paraStart, ellipsize, ellipsizedWidth,
468 | lineWidths[breakIndex], paint, moreChars);
469 |
470 | if (endPos < spanEnd) {
471 | fmTop = fm.top;
472 | fmBottom = fm.bottom;
473 | fmAscent = fm.ascent;
474 | fmDescent = fm.descent;
475 | } else {
476 | fmTop = fmBottom = fmAscent = fmDescent = 0;
477 | }
478 |
479 | here = endPos;
480 | breakIndex++;
481 |
482 | if (mLineCount >= mMaximumVisibleLineCount) {
483 | return;
484 | }
485 | }
486 | }
487 | ```
488 |
489 | 这样StaticLayout的排版过程就完成了。文本的绘制则是交给父类Layout来做的,Layout的绘制分为两大部分,drawBackground和drawText。drawBackground做的事情是如果文本内有LineBackgroundSpan则绘制所有的LineBackgroundSpan,然后判断是否有高亮背景(文本选中的背景),如果有则绘制高亮背景。
490 |
491 | ```java
492 | public void drawBackground(Canvas canvas, Path highlight, Paint highlightPaint,
493 | int cursorOffsetVertical, int firstLine, int lastLine) {
494 |
495 | //判断并绘制LineBackgroundSpan
496 | if (mSpannedText) {
497 | if (mLineBackgroundSpans == null) {
498 | mLineBackgroundSpans = new SpanSet(LineBackgroundSpan.class);
499 | }
500 |
501 | Spanned buffer = (Spanned) mText;
502 | int textLength = buffer.length();
503 | mLineBackgroundSpans.init(buffer, 0, textLength);
504 |
505 | if (mLineBackgroundSpans.numberOfSpans > 0) {
506 | int previousLineBottom = getLineTop(firstLine);
507 | int previousLineEnd = getLineStart(firstLine);
508 | ParagraphStyle[] spans = NO_PARA_SPANS;
509 | int spansLength = 0;
510 | TextPaint paint = mPaint;
511 | int spanEnd = 0;
512 | final int width = mWidth;
513 | //逐行绘制LineBackgroundSpan
514 | for (int i = firstLine; i <= lastLine; i++) {
515 | int start = previousLineEnd;
516 | int end = getLineStart(i + 1);
517 | previousLineEnd = end;
518 |
519 | int ltop = previousLineBottom;
520 | int lbottom = getLineTop(i + 1);
521 | previousLineBottom = lbottom;
522 | int lbaseline = lbottom - getLineDescent(i);
523 |
524 | if (start >= spanEnd) {
525 | spanEnd = mLineBackgroundSpans.getNextTransition(start, textLength);
526 |
527 | spansLength = 0;
528 | if (start != end || start == 0) {
529 | //排除不在绘制范围内的LineBackgroundSpan
530 | for (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) {
531 | if (mLineBackgroundSpans.spanStarts[j] >= end ||
532 | mLineBackgroundSpans.spanEnds[j] <= start) continue;
533 | spans = GrowingArrayUtils.append(
534 | spans, spansLength, mLineBackgroundSpans.spans[j]);
535 | spansLength++;
536 | }
537 | }
538 | }
539 | //对当前行内的LineBackgroundSpan进行绘制
540 | for (int n = 0; n < spansLength; n++) {
541 | LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n];
542 | lineBackgroundSpan.drawBackground(canvas, paint, 0, width,
543 | ltop, lbaseline, lbottom,
544 | buffer, start, end, i);
545 | }
546 | }
547 | }
548 | mLineBackgroundSpans.recycle();
549 | }
550 |
551 | //判断并绘制高亮背景(即选中的文本)
552 | if (highlight != null) {
553 | if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);
554 | canvas.drawPath(highlight, highlightPaint);
555 | if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical);
556 | }
557 | }
558 | ```
559 |
560 |
561 |
562 | drawText用来逐行绘制Layout的文本、影响显示效果的Span、以及Emoji表情等。当有Emoji或者Span的时候,实际绘制工作交给TextLine类来完成。
563 |
564 | ```java
565 | public void drawText(Canvas canvas, int firstLine, int lastLine) {
566 | int previousLineBottom = getLineTop(firstLine);
567 | int previousLineEnd = getLineStart(firstLine);
568 | ParagraphStyle[] spans = NO_PARA_SPANS;
569 | int spanEnd = 0;
570 | TextPaint paint = mPaint;
571 | CharSequence buf = mText;
572 |
573 | Alignment paraAlign = mAlignment;
574 | TabStops tabStops = null;
575 | boolean tabStopsIsInitialized = false;
576 |
577 | //获取TextLine实例
578 | TextLine tl = TextLine.obtain();
579 |
580 | //逐行绘制文本
581 | for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
582 | int start = previousLineEnd;
583 | previousLineEnd = getLineStart(lineNum + 1);
584 | int end = getLineVisibleEnd(lineNum, start, previousLineEnd);
585 |
586 | int ltop = previousLineBottom;
587 | int lbottom = getLineTop(lineNum + 1);
588 | previousLineBottom = lbottom;
589 | int lbaseline = lbottom - getLineDescent(lineNum);
590 |
591 | int dir = getParagraphDirection(lineNum);
592 | int left = 0;
593 | int right = mWidth;
594 |
595 | if (mSpannedText) {
596 | Spanned sp = (Spanned) buf;
597 | int textLength = buf.length();
598 | //检测是否段落的第一行
599 | boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n');
600 |
601 | //获得所有的段落风格相关的Span
602 | if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) {
603 | spanEnd = sp.nextSpanTransition(start, textLength,
604 | ParagraphStyle.class);
605 | spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
606 |
607 | paraAlign = mAlignment;
608 | for (int n = spans.length - 1; n >= 0; n--) {
609 | if (spans[n] instanceof AlignmentSpan) {
610 | paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
611 | break;
612 | }
613 | }
614 |
615 | tabStopsIsInitialized = false;
616 | }
617 |
618 | //获取影响行缩进的Span
619 | final int length = spans.length;
620 | boolean useFirstLineMargin = isFirstParaLine;
621 | for (int n = 0; n < length; n++) {
622 | if (spans[n] instanceof LeadingMarginSpan2) {
623 | int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
624 | int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
625 | if (lineNum < startLine + count) {
626 | useFirstLineMargin = true;
627 | break;
628 | }
629 | }
630 | }
631 | for (int n = 0; n < length; n++) {
632 | if (spans[n] instanceof LeadingMarginSpan) {
633 | LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
634 | if (dir == DIR_RIGHT_TO_LEFT) {
635 | margin.drawLeadingMargin(canvas, paint, right, dir, ltop,
636 | lbaseline, lbottom, buf,
637 | start, end, isFirstParaLine, this);
638 | right -= margin.getLeadingMargin(useFirstLineMargin);
639 | } else {
640 | margin.drawLeadingMargin(canvas, paint, left, dir, ltop,
641 | lbaseline, lbottom, buf,
642 | start, end, isFirstParaLine, this);
643 | left += margin.getLeadingMargin(useFirstLineMargin);
644 | }
645 | }
646 | }
647 | }
648 |
649 | boolean hasTabOrEmoji = getLineContainsTab(lineNum);
650 | if (hasTabOrEmoji && !tabStopsIsInitialized) {
651 | if (tabStops == null) {
652 | tabStops = new TabStops(TAB_INCREMENT, spans);
653 | } else {
654 | tabStops.reset(TAB_INCREMENT, spans);
655 | }
656 | tabStopsIsInitialized = true;
657 | }
658 |
659 | //判断当前行的第五方式
660 | Alignment align = paraAlign;
661 | if (align == Alignment.ALIGN_LEFT) {
662 | align = (dir == DIR_LEFT_TO_RIGHT) ?
663 | Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
664 | } else if (align == Alignment.ALIGN_RIGHT) {
665 | align = (dir == DIR_LEFT_TO_RIGHT) ?
666 | Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
667 | }
668 |
669 | int x;
670 | if (align == Alignment.ALIGN_NORMAL) {
671 | if (dir == DIR_LEFT_TO_RIGHT) {
672 | x = left + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
673 | } else {
674 | x = right + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
675 | }
676 | } else {
677 | int max = (int)getLineExtent(lineNum, tabStops, false);
678 | if (align == Alignment.ALIGN_OPPOSITE) {
679 | if (dir == DIR_LEFT_TO_RIGHT) {
680 | x = right - max + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
681 | } else {
682 | x = left - max + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
683 | }
684 | } else { // Alignment.ALIGN_CENTER
685 | max = max & ~1;
686 | x = ((right + left - max) >> 1) +
687 | getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
688 | }
689 | }
690 |
691 | paint.setHyphenEdit(getHyphen(lineNum));
692 | Directions directions = getLineDirections(lineNum);
693 | if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTabOrEmoji) {
694 | //没有任何Emoji或者span的时候,直接调用Canvas来绘制文本
695 | canvas.drawText(buf, start, end, x, lbaseline, paint);
696 | } else {
697 | //当有Emoji或者Span的时候,交给TextLine类来绘制
698 | tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops);
699 | tl.draw(canvas, x, ltop, lbaseline, lbottom);
700 | }
701 | paint.setHyphenEdit(0);
702 | }
703 |
704 | TextLine.recycle(tl);
705 | }
706 | ```
707 |
708 |
709 |
710 | 我们下面再来看看TextLine是如何绘制有特殊情况的文本的
711 |
712 | ```java
713 | void draw(Canvas c, float x, int top, int y, int bottom) {
714 | //判断是否有Tab或者Emoji
715 | if (!mHasTabs) {
716 | if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
717 | drawRun(c, 0, mLen, false, x, top, y, bottom, false);
718 | return;
719 | }
720 | if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
721 | drawRun(c, 0, mLen, true, x, top, y, bottom, false);
722 | return;
723 | }
724 | }
725 |
726 | float h = 0;
727 | int[] runs = mDirections.mDirections;
728 | RectF emojiRect = null;
729 |
730 | int lastRunIndex = runs.length - 2;
731 | //逐个绘制
732 | for (int i = 0; i < runs.length; i += 2) {
733 | int runStart = runs[i];
734 | int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
735 | if (runLimit > mLen) {
736 | runLimit = mLen;
737 | }
738 | boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
739 |
740 | int segstart = runStart;
741 | for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
742 | int codept = 0;
743 | Bitmap bm = null;
744 |
745 | if (mHasTabs && j < runLimit) {
746 | codept = mChars[j];
747 | if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) {
748 | codept = Character.codePointAt(mChars, j);
749 | if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) {
750 | //获取Emoji对应的图像
751 | bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
752 | } else if (codept > 0xffff) {
753 | ++j;
754 | continue;
755 | }
756 | }
757 | }
758 |
759 | if (j == runLimit || codept == '\t' || bm != null) {
760 | //绘制文字
761 | h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
762 | i != lastRunIndex || j != mLen);
763 |
764 | if (codept == '\t') {
765 | h = mDir * nextTab(h * mDir);
766 | } else if (bm != null) {
767 | float bmAscent = ascent(j);
768 | float bitmapHeight = bm.getHeight();
769 | float scale = -bmAscent / bitmapHeight;
770 | float width = bm.getWidth() * scale;
771 |
772 | if (emojiRect == null) {
773 | emojiRect = new RectF();
774 | }
775 | //调整emoji图像绘制矩形
776 | emojiRect.set(x + h, y + bmAscent,
777 | x + h + width, y);
778 | //绘制Emoji图像
779 | c.drawBitmap(bm, null, emojiRect, mPaint);
780 | h += width;
781 | j++;
782 | }
783 | segstart = j + 1;
784 | }
785 | }
786 | }
787 | }
788 | ```
789 |
790 | 这样就完成了文本的绘制工作,简单地总结就是:分析整体文本—>拆分为段落—>计算整体段落的文本包括Span的测量信息—>对文本进行折行—>根据最终行数把文本测量信息保存—>绘制文本的行背景—>判断并获取文本种的Span和Emoji图像—>绘制最终的文本和图像。当然我们省略了一部分内容,比如段落文本方向,单行的文本排版方向的计算,实际的处理要更为复杂。
791 |
792 | 接下来我们来看一下在测量过程中出现的FontMetrics,这是一个Paint的静态内部类。主要用来储存文字排版的Y轴相关信息。内部仅包含ascent、descent、top、bottom、leading五个数值。如下图:
793 |
794 | 
795 |
796 | 除了leading以外,其他的数值都是相对于每一行的baseline的,也就是说其他的数值需要加上对应行的baseline才能得到最终真实的坐标。
797 |
798 |
799 |
800 | ## 6.TextView接收软键盘输入
801 |
802 | ***
803 |
804 | Android上的标准文本编辑控件是EditText,而EditText对软键盘输入的处理,却是在TextView内部实现的。Android为所有的View预留了一个接收软键盘输入的接口类,叫InputConnection。软键盘以InputConnection为桥梁把文字输入、文字修改、文字删除等传递给View。任意View只要重写onCheckIsTextEditor()并返回true,然后重写onCreateInputConnection(EditorInfo outAttrs)返回一个InputConnection的实例,便可以接收软键盘的输入。TextView的软键盘输入接收,是通过EditableInputConnection类来实现的。
805 |
806 | ```java
807 | public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
808 | //判断是否处于可编辑状态
809 | if (onCheckIsTextEditor() && isEnabled()) {
810 | mEditor.createInputMethodStateIfNeeded();
811 |
812 | //设置输入法相关的信息
813 | outAttrs.inputType = getInputType();
814 | if (mEditor.mInputContentType != null) {
815 | outAttrs.imeOptions = mEditor.mInputContentType.imeOptions;
816 | outAttrs.privateImeOptions = mEditor.mInputContentType.privateImeOptions;
817 | outAttrs.actionLabel = mEditor.mInputContentType.imeActionLabel;
818 | outAttrs.actionId = mEditor.mInputContentType.imeActionId;
819 | outAttrs.extras = mEditor.mInputContentType.extras;
820 | } else {
821 | outAttrs.imeOptions = EditorInfo.IME_NULL;
822 | }
823 | if (focusSearch(FOCUS_DOWN) != null) {
824 | outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
825 | }
826 | if (focusSearch(FOCUS_UP) != null) {
827 | outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
828 | }
829 | if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION)
830 | == EditorInfo.IME_ACTION_UNSPECIFIED) {
831 |
832 | if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
833 | //把软键盘的enter设为下一步
834 | outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
835 | } else {
836 | //把软键盘的enter设为完成
837 | outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
838 | }
839 | if (!shouldAdvanceFocusOnEnter()) {
840 | outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
841 | }
842 | }
843 | if (isMultilineInputType(outAttrs.inputType)) {
844 | outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
845 | }
846 | outAttrs.hintText = mHint;
847 |
848 | //判断TextView内部文本是否可编辑
849 | if (mText instanceof Editable) {
850 | //返回EditableInputConnection实例
851 | InputConnection ic = new EditableInputConnection(this);
852 | outAttrs.initialSelStart = getSelectionStart();
853 | outAttrs.initialSelEnd = getSelectionEnd();
854 | outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
855 | return ic;
856 | }
857 | }
858 | return null;
859 | }
860 | ```
861 |
862 | 我们再来看一下EditableInputConnection里面的几个主要的方法:
863 |
864 | 首先是commitText方法,这个方法接收输入法输入的字符并提交给TextView。
865 |
866 | ```java
867 | public boolean commitText(CharSequence text, int newCursorPosition) {
868 | //判断TextView是否为空
869 | if (mTextView == null) {
870 | return super.commitText(text, newCursorPosition);
871 | }
872 | //判断文本是否Span,来自输入法的Span一般只有SuggestionSpan,SuggestionSpan携带了输入法的错别字修正的词
873 | if (text instanceof Spanned) {
874 | Spanned spanned = ((Spanned) text);
875 | SuggestionSpan[] spans = spanned.getSpans(0, text.length(), SuggestionSpan.class);
876 | mIMM.registerSuggestionSpansForNotification(spans);
877 | }
878 |
879 | mTextView.resetErrorChangedFlag();
880 | //提交字符
881 | boolean success = super.commitText(text, newCursorPosition);
882 | mTextView.hideErrorIfUnchanged();
883 | //返回是否成功
884 | return success;
885 | }
886 | ```
887 |
888 | getEditable方法,这个方法并不是InputConnection接口的一部分,而是EditableInputConnection的父类BaseInputConnection的方法,用来获取一个可编辑对象,EditableInputConnection里面的所有修改都针对这个可编辑对象来做。
889 |
890 | ```java
891 | public Editable getEditable() {
892 | TextView tv = mTextView;
893 | if (tv != null) {
894 | //返回TextView的可编辑对象
895 | return tv.getEditableText();
896 | }
897 | return null;
898 | }
899 | ```
900 |
901 | deleteSurroundingText方法,这个方法用来删除光标前后的内容:
902 |
903 | ```java
904 |
905 | public boolean deleteSurroundingText(int beforeLength, int afterLength) {
906 | if (DEBUG) Log.v(TAG, "deleteSurroundingText " + beforeLength
907 | + " / " + afterLength);
908 | final Editable content = getEditable();
909 | if (content == null) return false;
910 |
911 | //批量删除标记
912 | beginBatchEdit();
913 |
914 | //获取当前已选择的文本的位置
915 | int a = Selection.getSelectionStart(content);
916 | int b = Selection.getSelectionEnd(content);
917 |
918 | if (a > b) {
919 | int tmp = a;
920 | a = b;
921 | b = tmp;
922 | }
923 |
924 | int ca = getComposingSpanStart(content);
925 | int cb = getComposingSpanEnd(content);
926 | if (cb < ca) {
927 | int tmp = ca;
928 | ca = cb;
929 | cb = tmp;
930 | }
931 | if (ca != -1 && cb != -1) {
932 | if (ca < a) a = ca;
933 | if (cb > b) b = cb;
934 | }
935 |
936 | int deleted = 0;
937 |
938 | //删除光标之前的文本
939 | if (beforeLength > 0) {
940 | int start = a - beforeLength;
941 | if (start < 0) start = 0;
942 | content.delete(start, a);
943 | deleted = a - start;
944 | }
945 | //删除光标之后的文本
946 | if (afterLength > 0) {
947 | b = b - deleted;
948 |
949 | int end = b + afterLength;
950 | if (end > content.length()) end = content.length();
951 |
952 | content.delete(b, end);
953 | }
954 |
955 | //结束批量编辑
956 | endBatchEdit();
957 |
958 | return true;
959 | }
960 | ```
961 |
962 |
963 |
964 | commitCompletion和commitCorrection方法,即是用来补全单词和修正错别字的方法,这两个方法内部都是调用TextView对应的方法来实现的。
965 |
966 | ```java
967 | public boolean commitCompletion(CompletionInfo text) {
968 | if (DEBUG) Log.v(TAG, "commitCompletion " + text);
969 | mTextView.beginBatchEdit();
970 | mTextView.onCommitCompletion(text);
971 | mTextView.endBatchEdit();
972 | return true;
973 | }
974 |
975 | @Override
976 | public boolean commitCorrection(CorrectionInfo correctionInfo) {
977 | if (DEBUG) Log.v(TAG, "commitCorrection" + correctionInfo);
978 | mTextView.beginBatchEdit();
979 | mTextView.onCommitCorrection(correctionInfo);
980 | mTextView.endBatchEdit();
981 | return true;
982 | }
983 | ```
984 |
985 | ## 8.总结
986 |
987 | ***
988 |
989 | 一个展示文本+文本编辑器功能的控件需要做的事情很多,要对文本进行排版、处理不同的段落风格、处理段落内的不同emoji和span、进行折行计算,然后还需要做文本编辑、文本选择等。而TextView把这些事情明确分工给不同的类。这样不仅仅把复杂问题拆分成了一个个简单的小功能,同时也大大增加了可扩展性。
--------------------------------------------------------------------------------
/images/qrcode.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/6d9b5eab580d2cd9a8263f8f5dec10185a205cdf/images/qrcode.jpg
--------------------------------------------------------------------------------