├── 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 | ![](https://github.com/yuxingxin/AndroidWidgetClassGraph/blob/master/img/android.jpg) 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 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 |     276 | 277 | 278 | 279 |     280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 |
Class认领者
Seekbar源码解析JohnTsaiAndroid
ArrayMap源码解析audiebantzhan
SimpleArrayMap源码解析david-wei
ViewPager源码解析cpoopc
LongSparseArray源码解析taoliuh
Dialog源码解析wingjay
Frame/RelativeLayout源码解析wingjay
Drawable源码解析wingjay
AppBarLayout源码解析desmond1121
ProgressBar源码解析carozhu
GestureDetector源码分析lishiwei
RecyclerView/ItemTouchHelper源码解析xdtianyu
Toolbar源码解析SeniorZhai
WebView源码解析markzhai
Bitmap源码解析zimoguo
AdapterView源码解析ShenghuGong
Activity源码解析nekocode
Camera源码解析gcgongchao
Volley源码解析THEONE10211024
AudioPlayer源码解析ayyb1988
TimePicker源码解析shixinzhang
Log源码解析lypeer
Button源码解析pc859107393
Animation源码解析binIoter
Parcelable源码解析neuyu
BroadcastReceiver源码解析tiefeng0606
ImageView源码解析976014121
ListView源码解析KingJA
Intent源码解析imdreamrunner
FragmentTabHost源码分析Tikitoo
Canvas源码解析heavenxue
PopupWindow源码解析GJson
AudioRecord源码解析GJson
OverScroller源码解析lizardmia
Context源码解析messishow
Actionbar/AlertController源码解析rickdynasty
SnackBar源码解析cnLGMing
LauncherActivity源码解析kaiyangjia
Html源码解析DennyCai
EditText源码解析johnwatsondev
TextureView源码解析BeEagle
DownloadManager源码解析xiaohongmaosimida
ImageButton源码解析chenbinzhou
PopupMenu源码解析jimmyguo
AlarmManager源码解析huanglongyu
Glide源码解析Krbit
DataBinding源码解析xdsjs
PreferenceActivity源码解析FightingLarry
288 | 289 | ## 待认领文章 290 | 291 | **Sdk** 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 |
Class认领状态
ActionBar源码解析未认领
AccountManager源码解析未认领
BluetoothSocket源码解析未认领
BoringLayout源码解析未认领
DynamicLayout源码解析未认领
Paint源码解析未认领
Selector原理(Drawable源码解析)未认领
Spinner源码解析未认领
TabHost源码解析未认领
TableLayout源码解析未认领
343 | 344 | **v4** 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 |
Class认领状态
CircularArray源码解析未认领
CircularIntArray源码解析未认领
MapCollections源码解析未认领
368 | 369 | **v7** 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 |
Class认领状态
ActionMenuView源码解析未认领
ActionBarDrawerToggle源码解析未认领
ButtonBarLayout源码解析未认领
DrawerArrowDrawable源码解析未认领
ListMenuItemView源码解析未认领
ActionMenuView源码解析未认领
WindowDecorActionBar源码解析未认领
408 | 409 | **design** 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 |
Class认领状态
CollapsingToolbarLayout源码解析未认领
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 | ![viewgroup_lifecycle](images/viewgroup_lifecycle.png) 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 | ![measurespec_generate](images/generate_child_measurespec.png) 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 | ![touchevent](./images/viewgroup_touchevent.png) 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 | ![1339061786_4121](https://raw.githubusercontent.com/7heaven/AndroidSdkSourceAnalysis/master/article/images/fontmetrics.gif) 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 --------------------------------------------------------------------------------