0.前言
很多小伙伴可能在學(xué)習(xí)view的繪制流程源碼的時(shí)候有點(diǎn)抓不住重點(diǎn),所以在分析代碼的時(shí)候繞來繞去腦袋暈乎乎的。今天我就來給大家化繁為簡(jiǎn),只關(guān)注它最核心的東西。從數(shù)據(jù)結(jié)構(gòu)與算法還有設(shè)計(jì)模式的角度帶領(lǐng)大家真正去掌握。我這篇文章旨在讓大家能更深刻理解View繪制流程的設(shè)計(jì),不涉及具體的細(xì)節(jié)。最好的效果是大家先看這篇文章,然后根據(jù)文中介紹的知識(shí)點(diǎn)去自行查看源碼?;蛘吒械匠粤Φ脑捒梢越Y(jié)合別的大牛寫的文章去看源碼。_
1.先修知識(shí)點(diǎn)
首先,View體系的數(shù)據(jù)結(jié)構(gòu)就是樹形結(jié)構(gòu)。ViewGroup繼承View,而且ViewGroup持有View的引用,所以這不就是一個(gè)樹的節(jié)點(diǎn)嘛。數(shù)據(jù)結(jié)構(gòu)跟他的算法是相關(guān)的,所以至少你要掌握樹的遍歷,尤其是樹的先序遍歷,也就是深度遍歷。
在view體系設(shè)計(jì)中也涉及到了幾個(gè)設(shè)計(jì)模式,分別是組合模式,責(zé)任鏈模式,模板方法模式。(當(dāng)然還有其他如觀察者模式,適配器模式等等,不在這次的討論范圍。)
-
組合模式
如果想實(shí)現(xiàn)一個(gè)樹狀的關(guān)系,那么就可以使用組合模式。如View和ViewGroup的關(guān)系,ViewGroup繼承于View,同時(shí)也含有子View的引用集合。組合模式一般用于樹形結(jié)構(gòu),所以在這里不需要展開。你只需要知道,View體系本身就是組合模式的體現(xiàn)。
-
責(zé)任鏈模式
如果想實(shí)現(xiàn)一個(gè)調(diào)用可以讓多個(gè)類都有機(jī)會(huì)去處理,那么可以使用責(zé)任鏈模式。類Node含有一個(gè)自己的引用,相當(dāng)于一個(gè)鏈表指針,指向下一個(gè)節(jié)點(diǎn)。
class Node { public String name; public Node next; public Node(String name) { this.name = name; } public void operate(int num) { //1.自己先來處理 System.out.println(String.format("我是節(jié)點(diǎn)%s,我在處理:%d", name, num)); //2.分發(fā)給下一節(jié)點(diǎn)處理 if (next != null) { next.operate(num); } } }public class Main { public static void main(String[] args) { Node[] nodes = new Node[5]; Node head = nodes[0] = new Node("0"); for (int i = 1; i < 5; i++) {//構(gòu)造的鏈表為:0->1->2->3->4 nodes[i] = new Node(i + ""); head.next = nodes[i]; head = nodes[i]; } head = nodes[0]; head.operate(100); } } 結(jié)果為: 我是節(jié)點(diǎn)0,我在處理:100 我是節(jié)點(diǎn)1,我在處理:100 我是節(jié)點(diǎn)2,我在處理:100 我是節(jié)點(diǎn)3,我在處理:100 我是節(jié)點(diǎn)4,我在處理:100通過把每個(gè)處理者看成是鏈表上面的一個(gè)節(jié)點(diǎn),實(shí)現(xiàn)一個(gè)調(diào)用可以分發(fā)給多個(gè)處理者去處理。
-
模板方法模式
如果某一個(gè)功能邏輯的流程是比較固定的,但是有一定的步驟,那么可以通過模板方法模式把具體步驟交給子類去實(shí)現(xiàn)。
這個(gè)怎么理解呢?以上面責(zé)任鏈模式為例,每個(gè)節(jié)點(diǎn)的operate的流程是固定的:1.自己處理消息,2.把消息分發(fā)給下一個(gè)節(jié)點(diǎn)。但是可以發(fā)現(xiàn)上面的例子有點(diǎn)雞肋,因?yàn)槊總€(gè)Node節(jié)點(diǎn)的處理是完全一樣的,這看起來沒什么意義。好吧,那結(jié)合模板方法模式來進(jìn)行一個(gè)改造。
abstract class Node { public String name; public Node next; public Node(String name) { this.name = name; } public void operate(int num) { //1.自己先來處理 int result = onOperate(num); System.out.println(String.format("處理的結(jié)果:%d", result)); //2.分發(fā)給下一節(jié)點(diǎn)處理 if (next != null) { next.operate(result); } } //抽象方法,具體的處理交給子類根據(jù)自己的需求去實(shí)現(xiàn) protected abstract int onOperate(int num); }可以看到把原先自己直接處理的邏輯抽成了一個(gè)抽象函數(shù),這樣子類就必須去實(shí)現(xiàn)onOperate方法去做自己的處理邏輯。假設(shè)有這樣的需求:實(shí)現(xiàn)三個(gè)節(jié)點(diǎn),一個(gè)是進(jìn)行+1的操作;一個(gè)是進(jìn)行-1的操作;一個(gè)是乘2的操作。
class AddNode extends Node { public AddNode() { super("加法器"); } @Override protected int onOperate(int num) { System.out.println(String.format("我是%s,我將對(duì)%d進(jìn)行加1操作", name, num)); return num + 1; } } class MinusNode extends Node { public MinusNode() { super("減法器"); } @Override protected int onOperate(int num) { System.out.println(String.format("我是%s,我將對(duì)%d進(jìn)行減1操作", name, num)); return num - 1; } } class MultiNode extends Node { public MultiNode() { super("乘法器"); } @Override protected int onOperate(int num) { System.out.println(String.format("我是%s,我將對(duì)%d進(jìn)行乘2操作", name, num)); return num * 2; } }public class Main { public static void main(String[] args) { int num = 100; //做運(yùn)算:(num+1)*2-1 = 201 Node add = new AddNode(); Node minus = new MinusNode(); Node multi = new MultiNode(); add.next = multi; multi.next = minus; add.operate(num); } } 結(jié)果為: 我是加法器,我將對(duì)100進(jìn)行加1操作 處理的結(jié)果:101 我是乘法器,我將對(duì)101進(jìn)行乘2操作 處理的結(jié)果:202 我是減法器,我將對(duì)202進(jìn)行減1操作 處理的結(jié)果:201模板方法模式體現(xiàn)在:因?yàn)槊總€(gè)節(jié)點(diǎn)具體的消息處理邏輯是不一樣的,通過把operate流程固定,把消息處理邏輯寫成抽象函數(shù)onOperate交給節(jié)點(diǎn)子類去實(shí)現(xiàn)。這樣不同的節(jié)點(diǎn)就可以做不同的處理了。
發(fā)現(xiàn)一個(gè)彩蛋了沒?onOperate函數(shù)怎么那么熟悉?
先來想想經(jīng)常接觸到的onXXX方法:onCreate,onMeasure,onInterceptTouchEvent……沒錯(cuò),事實(shí)上掌握了這幾個(gè)設(shè)計(jì)模式,很多時(shí)候源碼的閱讀都會(huì)很流暢了。如觸摸事件分發(fā),View繪制的三大過程,Activity生命周期回調(diào),AsyncTask...等等的機(jī)制和原理。推薦大家一定要找時(shí)間深入研究,成體系地學(xué)習(xí)一下設(shè)計(jì)模式。這是高級(jí)工程師架構(gòu)設(shè)計(jì)必備技能。
-
樹的遍歷
為了真正關(guān)注核心點(diǎn)而不被其他的東西干擾帶偏,所以我假定View樹是一個(gè)二叉樹,或者說我選取一個(gè)二叉樹View樹來進(jìn)行分析。
首先來回顧一下樹的遍歷(遞歸版):

class Node{
int id;
Node left;
Node right;
public Node(int id) {
this.id = id;
}
}
public class Main {
public static void main(String[] args) {
Node[] nodes = new Node[8];
for (int i = 0; i < 8; i++) {
nodes[i] = new Node(i);
}
nodes[0].left = nodes[1];
nodes[0].right = nodes[2];
nodes[1].left = nodes[3];
nodes[2].left = nodes[4];
nodes[2].right = nodes[5];
nodes[3].left = nodes[6];
nodes[3].right = nodes[7];
dfs(nodes[0]);
}
private static void dfs(Node root) {
if (root == null) {
return;
}
System.out.println(root.id);
if (root.left != null) {
dfs(root.left);
}
if (root.right != null) {
dfs(root.right);
}
}
}
結(jié)果為:
0
1
3
6
7
2
4
5
相信很多人都能寫出上面的深度遍歷代碼,but,這顯然不夠“java”,嚴(yán)格來說這是c語言形式的寫法,只是把節(jié)點(diǎn)看成是數(shù)據(jù)實(shí)體,不那么面向?qū)ο?。那好,我們來?shí)現(xiàn)更加面向?qū)ο蟮纳疃缺闅v寫法。
面向?qū)ο笠簿褪穷惱锩嬗袛?shù)據(jù)也有行為,那我們就把遍歷的行為交給類去做。說白了就是把dfs函數(shù)寫成成員函數(shù)。
class Node {
int id;
Node left;
Node right;
public Node(int id) {
this.id = id;
}
//把dfs寫成成員函數(shù)
public void dfs() {
System.out.println(this.id);
if (left != null) {
left.dfs();
}
if (right != null) {
right.dfs();
}
}
}
public class Main {
public static void main(String[] args) {
Node[] nodes = new Node[8];
for (int i = 0; i < 8; i++) {
nodes[i] = new Node(i);
}
nodes[0].left = nodes[1];
nodes[0].right = nodes[2];
nodes[1].left = nodes[3];
nodes[2].left = nodes[4];
nodes[2].right = nodes[5];
nodes[3].left = nodes[6];
nodes[3].right = nodes[7];
//dfs(nodes[0]);
nodes[0].dfs();
}
}
結(jié)果為:
0
1
3
6
7
2
4
5
好了,樹的遍歷主要是想說明Java版的面向?qū)ο蟮膶懛?。因?yàn)槲以诎俣入S意搜索了一下,發(fā)現(xiàn)基本都是用c語言版本的寫法來寫的。
2.View的Measure流程的核心
前面洋洋灑灑寫了那么多,現(xiàn)在終于可以應(yīng)用啦。理解上面的知識(shí)點(diǎn)能讓你更加容易理解復(fù)雜的View的Measure流程。
為了真正關(guān)注核心點(diǎn)而不被其他的東西干擾帶偏,所以我假定View樹是一個(gè)二叉樹,或者說我選取一個(gè)二叉樹View樹來進(jìn)行分析。
測(cè)量就是計(jì)算每個(gè)View的大小,先來定義View類。
abstract class View {
int id;
int width;
int height;
View left;
View right;
public View(int id) {
this.id = id;
}
final public void measure(int width, int height) {
//1.具體如何測(cè)量交給子類決定
onMeasure(width, height);
}
//設(shè)置測(cè)量值
public void setMeasuredDimension(int w, int h) {
width = w;
height = h;
System.out.println(String.format("%d的測(cè)量結(jié)果是w=%d,h=%d", id, width, height));
}
protected abstract void onMeasure(int width, int height);
}
很簡(jiǎn)陋的一個(gè)類,但是包含了最基本的要素了。measure方法里就用了模板方法模式,把具體如何測(cè)量交給子類實(shí)現(xiàn)。而且用final關(guān)鍵字,所以子類不能覆寫measure,也就是說measure方法的流程不讓改動(dòng)。
注意:下文子節(jié)點(diǎn)是指View樹的子節(jié)點(diǎn),父節(jié)點(diǎn)是指View樹的父節(jié)點(diǎn),注意跟父類子類區(qū)分開。這是兩回事來的。
好了,再來實(shí)現(xiàn)兩個(gè)子類,不妨就叫TextView,ImageView。TextView具體的測(cè)量就是把父節(jié)點(diǎn)傳遞過來的值減去10,而ImageView是減去20。
class TextView extends View {
public TextView(int id) {
super(id);
}
@Override
protected void onMeasure(int width, int height) {
int myW = width - 10;
int myH = height - 10;
setMeasuredDimension(myW, myH);
//去測(cè)量子節(jié)點(diǎn)
if (left != null) {
left.measure(myW, myH);
}
if (right != null) {
right.measure(myW, myH);
}
}
}
class ImageView extends View {
public ImageView(int id) {
super(id);
}
@Override
protected void onMeasure(int width, int height) {
int myW = width - 20;
int myH = height - 20;
setMeasuredDimension(myW, myH);
//去測(cè)量子節(jié)點(diǎn)
if (left != null) {
left.measure(myW, myH);
}
if (right != null) {
right.measure(myW, myH);
}
}
}
大家可以看到,子節(jié)點(diǎn)的測(cè)量也是交給子類去負(fù)責(zé)分發(fā)測(cè)量了。跟之前討論模板方法模式時(shí)有點(diǎn)不同,但是本質(zhì)上是一樣的。只是模板方法模式的例子是父類負(fù)責(zé)分發(fā),這里是子類分發(fā)。

構(gòu)造上圖的View樹進(jìn)行測(cè)試。
public class Main {
public static void main(String[] args) {
View decorView = new ImageView(0);
View imageView1 = new ImageView(1);
View imageView2 = new ImageView(2);
View textView3 = new TextView(3);
View textView4 = new TextView(4);
decorView.left = imageView1;
decorView.right = imageView2;
imageView1.left = textView3;
imageView1.right = textView4;
//獲取window窗口大小(一般是手機(jī)屏幕大?。?,假設(shè)是1080x1920
int windowW = 1080;
int windowH = 1920;
decorView.measure(windowW, windowH);
}
}
結(jié)果為:
0的測(cè)量結(jié)果是w=1060,h=1900
1的測(cè)量結(jié)果是w=1040,h=1880
3的測(cè)量結(jié)果是w=1030,h=1870
4的測(cè)量結(jié)果是w=1030,h=1870
2的測(cè)量結(jié)果是w=1040,h=1880
根據(jù)上圖和運(yùn)行結(jié)果可知,View的測(cè)量是深度遍歷的。測(cè)量到一個(gè)節(jié)點(diǎn)時(shí),這個(gè)節(jié)點(diǎn)負(fù)責(zé)去發(fā)起子節(jié)點(diǎn)的測(cè)量,這是責(zé)任鏈模式;而為了把具體測(cè)量實(shí)現(xiàn)交給子類,使用了模板方法模式。
3.更進(jìn)一步
有的小伙伴可能說了,你這個(gè)跟Android實(shí)際的View代碼出入有點(diǎn)大啊,你看都沒有體現(xiàn)出View跟ViewGroup呢!好吧,那我們來實(shí)現(xiàn)更加貼近Android的代碼實(shí)現(xiàn)吧。

public class Main {
public static void main(String[] args) {
LinearLayout linearLayout0 = new LinearLayout(0);
LinearLayout linearLayout1 = new LinearLayout(1);
TextView textView2 = new TextView(2);
LinearLayout linearLayout3 = new LinearLayout(3);
LinearLayout linearLayout4 = new LinearLayout(4);
linearLayout0.left = linearLayout1;
linearLayout0.right = textView2;
linearLayout1.left = linearLayout3;
linearLayout1.right = linearLayout4;
//獲取window窗口大小,假設(shè)是1080x1920
int windowW = 1080;
int windowH = 1920;
linearLayout0.measure(windowW,windowH);
}
}
class View {
int id;
int width;
int height;
public View(int id) {
this.id = id;
}
final public void measure(int width, int height) {
//1.具體如何測(cè)量交給子類決定
onMeasure(width, height);
}
//設(shè)置測(cè)量值
public void setMeasuredDimension(int w, int h) {
width = w;
height = h;
System.out.println(String.format("%d的測(cè)量結(jié)果是w=%d,h=%d", id, width, height));
}
protected void onMeasure(int width, int height) {
//默認(rèn)實(shí)現(xiàn)為直接設(shè)置父類傳遞過來的參數(shù)
setMeasuredDimension(width, height);
}
}
class ViewGroup extends View {
public ViewGroup(int id) {
super(id);
}
//ViewGroup才有子View
View left;
View right;
@Override
protected void onMeasure(int width, int height) {
//默認(rèn)實(shí)現(xiàn)為把width,height減去50作為自己的參數(shù)
int myW = width - 50;
int myH = height - 50;
setMeasuredDimension(myW, myH);
//發(fā)起子節(jié)點(diǎn)的測(cè)量
if (left != null) {
left.measure(myW, myH);
}
if (right != null) {
right.measure(myW, myH);
}
}
}
//View的子類沒有子節(jié)點(diǎn),只需要關(guān)心自己的測(cè)量
class TextView extends View {
public TextView(int id) {
super(id);
}
//實(shí)現(xiàn)自己的測(cè)量邏輯,把width,height減去10
@Override
protected void onMeasure(int width, int height) {
setMeasuredDimension(width - 10, height - 10);
}
}
//ViewGroup的子類有子節(jié)點(diǎn),需要發(fā)起子節(jié)點(diǎn)的測(cè)量
class LinearLayout extends ViewGroup {
public LinearLayout(int id) {
super(id);
}
//把width,height減去30作為自己的參數(shù)
@Override
protected void onMeasure(int width, int height) {
int myW = width - 30;
int myH = height - 30;
setMeasuredDimension(myW, myH);
//負(fù)責(zé)發(fā)起子節(jié)點(diǎn)的測(cè)量,這里實(shí)現(xiàn)為先測(cè)量右節(jié)點(diǎn)再測(cè)量左節(jié)點(diǎn)
if (right != null) {
right.measure(myW, myH);
}
if (left != null) {
left.measure(myW, myH);
}
}
}
結(jié)果為:
0的測(cè)量結(jié)果是w=1050,h=1890
2的測(cè)量結(jié)果是w=1040,h=1880
1的測(cè)量結(jié)果是w=1020,h=1860
4的測(cè)量結(jié)果是w=990,h=1830
3的測(cè)量結(jié)果是w=990,h=1830
重要的點(diǎn)都在代碼上注釋了。
可以看到,之前的遍歷順序是01342,現(xiàn)在是02143了,因?yàn)長(zhǎng)inearLayout是先進(jìn)行右節(jié)點(diǎn)的測(cè)量。
4.總結(jié)
View的體系設(shè)計(jì)用到了許多設(shè)計(jì)模式,這里主要是責(zé)任鏈模式和模板方法模式,理解設(shè)計(jì)模式能更加容易讀懂源碼。
View的遍歷是深度遍歷,需要掌握J(rèn)ava版的實(shí)現(xiàn)。
layout以及draw流程的核心也差不多也是這樣,大家跟著我說的去分析源碼效果更好。注意子節(jié)點(diǎn)的draw流程直接由父類發(fā)起了,子類只需要在onDraw中繪制自己的內(nèi)容即可。
文中關(guān)注的重點(diǎn)在于如何實(shí)現(xiàn)一顆View樹的測(cè)量過程。還有很多細(xì)節(jié)沒有涉及,例如MeasureSpec。實(shí)際上View最終測(cè)量結(jié)果是結(jié)合我們?cè)趚ml自己定義的參數(shù)和父View自己的參數(shù)去決定的。
小提示:大家在看遞歸代碼時(shí)可以結(jié)合畫一下調(diào)用棧去分析。
如有寫得不準(zhǔn)確的地方,歡迎交流指正。_