該文為匯總網(wǎng)上優(yōu)秀的資源所得虏肾,喜歡請關(guān)注原作者,我記錄下來主要是為了以后我自己查找用的
ref 參考
一才漆、測量與布局
1. ViewGroup繪制流程
注意,View及ViewGroup基本相同佛点,只是在ViewGroup中不僅要繪制自己還是繪制其中的子控件醇滥,而View則只需要繪制自己就可以了,所以我們這里就以ViewGroup為例來講述整個繪制流程超营。
繪制流程分為三步:測量鸳玩、布局、繪制
分別對應(yīng):onMeasure()
-> onLayout()
-> onDraw()
其中演闭,他們?nèi)齻€的作用分別如下:
onMeasure():測量自己的大小不跟,為正式布局提供建議。(注意米碰,只是建議窝革,至于用不用,要看onLayout);
onLayout():使用layout()函數(shù)對所有子控件布局吕座;
onDraw():根據(jù)布局的位置繪圖虐译;
2. onMeasure與MeasureSpec
布局繪畫涉及兩個過程:測量過程和布局過程。
測量過程通過measure方法實現(xiàn)吴趴,是View樹自頂向下的遍歷漆诽,每個View在循環(huán)過程中將尺寸細(xì)節(jié)往下傳遞,當(dāng)測量過程完成之后,所有的View都存儲了自己的尺寸厢拭。第二個過程則是通過方法layout來實現(xiàn)的兰英,也是自頂向下的。在這個過程中蚪腐,每個父View負(fù)責(zé)通過計算好的尺寸放置它的子View箭昵。
前面講過税朴,onMeasure()是用來測量當(dāng)前控件大小的回季,給onLayout() 提供數(shù)值參考,需要特別注意的是:測量完成以后通過setMeasuredDimension(int,int)
設(shè)置給系統(tǒng)正林。
2.1 onMeasure
首先泡一,看一下onMeasure()的聲明:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
這里我們主要關(guān)注傳進(jìn)來的兩個參數(shù):int widthMeasureSpec, int heightMeasureSpec
他們的意義:
他們是父類傳遞過來給當(dāng)前view的一個建議值,即想把當(dāng)前view的尺寸設(shè)置為寬widthMeasureSpec,高h(yuǎn)eightMeasureSpec
有關(guān)他們的組成觅廓,我們就直接轉(zhuǎn)到MeasureSpec部分鼻忠。
2.2 MeasureSpec
雖然表面上看起來他們是int類型的數(shù)字,其實他們是由mode+size兩部分組成的杈绸。
widthMeasureSpec和heightMeasureSpec轉(zhuǎn)化成二進(jìn)制數(shù)字表示帖蔓,他們都是32位的。
前兩位代表mode(測量模式)瞳脓,后面30位才是他們的實際數(shù)值(size)塑娇。
(1)模式分類
它有三種模式:
①、UNSPECIFIED
(未指定)劫侧,父元素不對子元素施加任何束縛埋酬,子元素可以得到任意想要的大小烧栋;
②写妥、EXACTLY
(完全),父元素決定自元素的確切大小审姓,子元素將被限定在給定的邊界里而忽略它本身大小珍特;
③、AT_MOST
(至多)魔吐,子元素至多達(dá)到指定大小的值扎筒。
他們對應(yīng)的二進(jìn)制值分別是:
UNSPECIFIED=00000000000000000000000000000000
EXACTLY =01000000000000000000000000000000
AT_MOST =10000000000000000000000000000000
由于最前面兩位代表模式,所以他們分別對應(yīng)十進(jìn)制的0画畅,1砸琅,2;
(2)模式提取
現(xiàn)在我們知道了widthMeasureSpec和heightMeasureSpec是由模式和數(shù)值組成的轴踱,而且二進(jìn)制的前兩位代表模式症脂,后28位代表數(shù)字。
andorid系統(tǒng)提供提取模式和數(shù)值的類,MeasureSpec
下面兩個函數(shù)就可以實現(xiàn)這個功能:
//MODE的取值為
MeasureSpec.AT_MOST
MeasureSpec.EXACTLY
MeasureSpec.UNSPECIFIED
//獲取MODE
MeasureSpec.getMode(int spec)
//獲取數(shù)值
MeasureSpec.getSize(int spec)
通過下面的代碼就可以分別獲取widthMeasureSpec和heightMeasureSpec的MODE和數(shù)值
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
(3)模式有什么用呢
我們知道有三個模式:EXACTLY诱篷、AT_MOST壶唤、UNSPECIFIED
需要注意的是widthMeasureSpec和heightMeasureSpec各自都有它對應(yīng)的模式,模式的由來分別來自于XML定義:
簡單來說棕所,XML布局和模式有如下對應(yīng)關(guān)系:
- wrap_content-> MeasureSpec.AT_MOST
- match_parent -> MeasureSpec.EXACTLY
- 具體值 -> MeasureSpec.EXACTLY
例如闸盔,下面這個XML
<com.example.harvic.myapplication.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.example.harvic.myapplication.FlowLayout>
那FlowLayout在onMeasure()中傳值時widthMeasureSpec的模式就是 MeasureSpec.EXACTLY,即父窗口寬度值琳省。heightMeasureSpec的模式就是 MeasureSpec.AT_MOST迎吵,即不確定的。
一定要注意是针贬,當(dāng)模式是MeasureSpec.EXACTLY時击费,我們就不必要設(shè)定我們計算的大小了,因為這個大小是用戶指定的桦他,我們不應(yīng)更改蔫巩。但當(dāng)模式是MeasureSpec.AT_MOST時,也就是說用戶將布局設(shè)置成了wrap_content快压,我們就需要將大小設(shè)定為我們計算的數(shù)值圆仔,因為用戶根本沒有設(shè)置具體值是多少,需要我們自己計算蔫劣。
即坪郭,假如width和height是我們經(jīng)過計算的控件所占的寬度和高度。那在onMeasure()中使用setMeasuredDimension()
最后設(shè)置時截粗,代碼應(yīng)該是這樣的:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
//經(jīng)過計算,控件所占的寬和高分別對應(yīng)width和height
//計算過程鸵隧,我們會在下篇細(xì)講
…………
setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height);
}
3. onLayout()
3.1 概述
上面說了绸罗,onLayout()是實現(xiàn)所有子控件布局的函數(shù)。注意豆瘫,是所有子控件I后啊!外驱!那它自己的布局怎么辦育灸?后面我們再講,先講講在onLayout()中我們應(yīng)該做什么昵宇。
我們先看看ViewGroup的onLayout()函數(shù)的默認(rèn)行為是什么
在ViewGroup.java中
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
是一個抽象方法磅崭,說明凡是派生自ViewGroup的類都必須自己去實現(xiàn)這個方法。像LinearLayout瓦哎、RelativeLayout等布局砸喻,都是重寫了這個方法柔逼,然后在內(nèi)部按照各自的規(guī)則對子視圖進(jìn)行布局的。
3.2 實例
下面我們就舉個例子來看一下有關(guān)onMeasure()和onLayout()的具體使用:
//com.richy.kotlindemo.customview.MyLinLayout
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val measureWidth = View.MeasureSpec.getSize(widthMeasureSpec)
val measureHeight = View.MeasureSpec.getSize(heightMeasureSpec)
val measureWidthMode = View.MeasureSpec.getMode(widthMeasureSpec)
val measureHeightMode = View.MeasureSpec.getMode(heightMeasureSpec)
var height = 0
var width = 0
for (i in 0 until childCount) {
//測量子控件
val child = getChildAt(i)
//measureChild 測量子控件
measureChild(child, widthMeasureSpec, heightMeasureSpec)
//獲得子控件的高度和寬度
val childHeight = child.measuredHeight
val childWidth = child.measuredWidth
//得到最大寬度割岛,并且累加高度
height += childHeight
width = Math.max(childWidth, width)
}
//設(shè)置測量結(jié)果
setMeasuredDimension(if (measureWidthMode === View.MeasureSpec.EXACTLY) measureWidth else width, if (measureHeightMode === View.MeasureSpec.EXACTLY) measureHeight else height)
/*
android:layout_width="match_parent"
android:layout_height="wrap_content"
示例中的這里的measureWidthMode應(yīng)該是MeasureSpec.EXACTLY,measureHeightMode應(yīng)該是MeasureSpec.AT_MOST愉适;
* */
}
總體來講,onMeasure()中計算出的width和height癣漆,就是當(dāng)XML布局設(shè)置為layout_width="wrap_content"维咸、layout_height="wrap_content"時所占的寬和高;即整個container所占的最小矩形 (因為 match_parent -> MeasureSpec.EXACTLY惠爽,具體值 -> MeasureSpec.EXACTLY)
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var top = 0;
for (i in 0 until childCount) {
val child = getChildAt(i)
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
child.layout(0, top, childWidth, top + childHeight)
top += childHeight
}
}
(1)癌蓖、getMeasuredWidth()與getWidth()
趁熱打鐵,就這個例子疆股,我們講一個很容易出錯的問題:getMeasuredWidth()與getWidth()的區(qū)別费坊。他們的值大部分時間都是相同的倒槐,但意義確是根本不一樣的旬痹,我們就來簡單分析一下。
區(qū)別主要體現(xiàn)在下面幾點:
- 首先getMeasureWidth()
方法在measure()過程結(jié)束后就可以獲取到了讨越,而getWidth()
方法要在layout()過程結(jié)束后才能獲取到两残。
- getMeasureWidth()
方法中的值是通過setMeasuredDimension()
方法來進(jìn)行設(shè)置的,而getWidth()
方法中的值則是通過layout(left,top,right,bottom)
方法設(shè)置的把跨。
還記得嗎人弓,我們前面講過,setMeasuredDimension()提供的測量結(jié)果只是為布局提供建議着逐,最終的取用與否要看layout()函數(shù)崔赌。大家再看看我們上面重寫的MyLinLayout,是不是我們自己使用child.layout(left,top,right,bottom)來定義了各個子控件所應(yīng)在的位置:
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
child.layout(0, top, childWidth, top + childHeight)
top += childHeight
從代碼中可以看到耸别,我們使用child.layout(0, top, childWidth, top + childHeight);來布局控件的位置健芭,其中g(shù)etWidth()的取值就是這里的右坐標(biāo)減去左坐標(biāo)的寬度;因為我們這里的寬度是直接使用的child.getMeasuredWidth()的值秀姐,當(dāng)然會導(dǎo)致getMeasuredWidth()與getWidth()的值是一樣的慈迈。如果我們在調(diào)用layout()的時候傳進(jìn)去的寬度值不與getMeasuredWidth()相同,那必然getMeasuredWidth()與getWidth()的值就不再一樣了省有。
一定要注意的一點是:getMeasureWidth()方法在measure()過程結(jié)束后就可以獲取到了痒留,而getWidth()方法要在layout()過程結(jié)束后才能獲取到。再重申一遍4姥亍I焱贰!O象啊恤磷!
3.3 疑問:container自己什么時候被布局
前面我們說了弧轧,在派生自ViewGroup的container中,比如我們上面的MyLinLayout碗殷,在onLayout()中布局它所有的子控件精绎。那它自己什么時候被布局呢?
它當(dāng)然也有父控件锌妻,它的布局也是在父控件中由它的父控件完成的代乃,就這樣一層一層地向上由各自的父控件完成對自己的布局。直到所有控件的最頂層結(jié)點仿粹,在所有的控件的最頂部有一個ViewRoot搁吓,ViewRootImpl是View中的最高層級,屬于所有View的根(但ViewRootImpl不是View吭历,只是實現(xiàn)了ViewParent接口
)堕仔,實現(xiàn)了View和WindowManager之間的通信協(xié)議。那讓我們來看看它是怎么來做的吧晌区。
每個Activity中都包含一個Window對象摩骨,通常,Android中的Window是由PhoneWindow實現(xiàn)的朗若。而PhoneWindow又將一個DecorView設(shè)置為整個窗口的根View(DecorView是一個ViewGroup)恼五。
在它布局里,會調(diào)用一個layout()函數(shù)(不能被重載哭懈,代碼位于View.java):
/* final 標(biāo)識符 灾馒, 不能被重載 , 參數(shù)為每個視圖位于父視圖的坐標(biāo)軸
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
public final void layout(int l, int t, int r, int b) {
boolean changed = setFrame(l, t, r, b); //設(shè)置每個視圖位于父視圖的坐標(biāo)軸
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);//回調(diào)onLayout函數(shù) 遣总,設(shè)置每個子視圖的布局
mPrivateFlags &= ~LAYOUT_REQUIRED;
}
mPrivateFlags &= ~FORCE_LAYOUT;
在SetFrame(l,t,r,b)就是設(shè)置自己的位置睬罗,設(shè)置結(jié)束以后才會調(diào)用onLayout(changed, l, t, r, b)來設(shè)置內(nèi)部所有子控件的位置。
OK啦旭斥,到這里有關(guān)onMeasure()和onLayout()的內(nèi)容就講完啦容达,想必大家應(yīng)該也對整個布局流程有了一個清楚的認(rèn)識了,下面我們再看一個緊要的問題:如何得到自定義控件的左右間距margin值琉预。
4. 獲取子控件Margin的方法
4.1 獲取方法及示例
如果要自定義ViewGroup支持子控件的layout_margin參數(shù)董饰,則自定義的ViewGroup類必須重載generateLayoutParams()
函數(shù),并且在該函數(shù)中返回一個ViewGroup.MarginLayoutParams
派生類對象圆米,這樣才能使用margin參數(shù)卒暂。
需要特別注意的是,如果我們在onLayout()中根據(jù)margin來布局的話娄帖,那么我們在onMeasure()中計算container的大小時也祠,也要加上margin,不然會導(dǎo)致container太小近速,而控件顯示不全的問題诈嘿。
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)//注意這里返回的是MarginLayoutParams堪旧,而不是LayoutParams
}
override fun generateLayoutParams(p: LayoutParams?): LayoutParams {
return MarginLayoutParams(p)
}
/*
*如果要使用默認(rèn)的構(gòu)造方法,就生成layout_width="match_parent"奖亚、layout_height="match_parent"對應(yīng)的參數(shù)
*/
override fun generateDefaultLayoutParams(): LayoutParams {
return MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
...
for (i in 0 until childCount) {
//測量子控件
val child = getChildAt(i)
//measureChild 測量子控件
measureChild(child, widthMeasureSpec, heightMeasureSpec)
//獲得子控件的高度和寬度
// val childHeight = child.measuredHeight
// val childWidth = child.measuredWidth
val lp = child.layoutParams as ViewGroup.MarginLayoutParams
val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin
val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
//得到最大寬度淳梦,并且累加高度
height += childHeight
width = Math.max(childWidth, width)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var top = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
// val childWidth = child.measuredWidth
// val childHeight = child.measuredHeight
// child.layout(0, top, childWidth, top + childHeight)
val lp = child.layoutParams as ViewGroup.MarginLayoutParams//generateLayoutParams()返回的是MarginLayoutParams
val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin
val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
child.layout(lp.leftMargin, top + lp.topMargin, child.measuredWidth + lp.leftMargin, top + child.measuredHeight + lp.topMargin);
top += childHeight
}
}
4.2 原理
上面我們看了要重寫generateDefaultLayoutParams()函數(shù)才能獲取控件的margin間距。那為什么要重寫呢昔字?下面這句就為什么非要強轉(zhuǎn)呢爆袍?
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
下面我們來看看這么做的原因。
/**
*從指定的XML中獲取對應(yīng)的layout_width和layout_height值
*/
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);//默認(rèn)返回LayoutParams
}
/*
*如果要使用默認(rèn)的構(gòu)造方法作郭,就生成layout_width="wrap_content"陨囊、layout_height="wrap_content"對應(yīng)的參數(shù)
*/
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
首先,在container在初始化子控件時夹攒,會調(diào)用LayoutParams#generateLayoutParams(LayoutParams p)來為子控件生成對應(yīng)的布局屬性蜘醋,但默認(rèn)只是生成layout_width和layout_height所以對應(yīng)的布局參數(shù),即在正常情況下的generateLayoutParams()函數(shù)生成的LayoutParams實例是不能夠取到margin值的咏尝。即:
(1)generateLayoutParams()的默認(rèn)實現(xiàn)
//位于ViewGrop.java中
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
從上面的代碼中明顯可以看出压语,generateLayoutParams()調(diào)用LayoutParams()產(chǎn)生布局信息,而LayoutParams()最終調(diào)用setBaseAttributes()來獲得對應(yīng)的寬状土,高屬性无蜂。
這里是通過TypedArray對自定義的XML進(jìn)行值提取的過程。從這里也可以看到蒙谓,generateLayoutParams生成的LayoutParams屬性只有l(wèi)ayout_width和layout_height的屬性值。
(2)MarginLayoutParams實現(xiàn)
下面再來看看MarginLayoutParams的具體實現(xiàn)训桶,其實通過上面的過程累驮,大家也應(yīng)該想到,它也是通過TypeArray來解析自定義屬性來獲得用戶的定義值的舵揭。
public MarginLayoutParams(Context c, AttributeSet attrs) {
super();
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
int margin = a.getDimensionPixelSize(
com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
//第一部分:提取layout_margin的值并設(shè)置
if (margin >= 0) {
leftMargin = margin;
topMargin = margin;
rightMargin= margin;
bottomMargin = margin;
} else {
//第二部分:如果用戶沒有設(shè)置layout_margin谤专,而是單個設(shè)置的,那么就一個個提取
leftMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
UNDEFINED_MARGIN);
rightMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginRight,
UNDEFINED_MARGIN);
topMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginTop,
DEFAULT_MARGIN_RESOLVED);
startMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginStart,
DEFAULT_MARGIN_RELATIVE);
endMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
DEFAULT_MARGIN_RELATIVE);
}
a.recycle();
}
這里大家也可以看到為什么非要重寫generateLayoutParams()函數(shù)了午绳,就是因為默認(rèn)的generateLayoutParams()函數(shù)只會提取layout_width置侍、layout_height的值,只有MarginLayoutParams()才具有提取margin間距的功能拦焚!
二蜡坊、 繪制順序
Android 里面的繪制都是按順序的,先繪制的內(nèi)容會被后繪制的蓋住赎败。比如你在重疊的位置先畫圓再畫方秕衙,和先畫方再畫圓所呈現(xiàn)出來的結(jié)果肯定是不同的:
1 super.onDraw() 前 or 后?
前幾期我寫的自定義繪制僵刮,全都是直接繼承 View
類据忘,然后重寫它的 onDraw()
方法鹦牛,把繪制代碼寫在里面,就像這樣:
public class AppView extends View {
...
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
... // 自定義繪制代碼
}
...
}
這是自定義繪制最基本的形態(tài):繼承 View
類勇吊,在 onDraw()
中完全自定義它的繪制曼追。
在之前的樣例中,我把繪制代碼全都寫在了 super.onDraw()
的下面汉规。不過其實拉鹃,繪制代碼寫在 super.onDraw()
的上面還是下面都無所謂,甚至鲫忍,你把 super.onDraw()
這行代碼刪掉都沒關(guān)系膏燕,效果都是一樣的——因為在 View
這個類里,onDraw()
本來就是空實現(xiàn):
// 在 View.java 的源碼中悟民,onDraw() 是空的
// 所以直接繼承 View 的類坝辫,它們的 super.onDraw() 什么也不會做
public class View implements Drawable.Callback,
KeyEvent.Callback, AccessibilityEventSource {
...
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
...
}
然而,除了繼承 View
類射亏,自定義繪制更為常見的情況是近忙,繼承一個具有某種功能的控件,去重寫它的 onDraw()
智润,在里面添加一些繪制代碼及舍,做出一個「進(jìn)化版」的控件:
基于
EditText
,在它的基礎(chǔ)上增加了頂部的 Hint Text 和底部的字符計數(shù)窟绷。
而這種基于已有控件的自定義繪制锯玛,就不能不考慮 super.onDraw()
了:你需要根據(jù)自己的需求,判斷出你繪制的內(nèi)容需要蓋住控件原有的內(nèi)容還是需要被控件原有的內(nèi)容蓋住兼蜈,從而確定你的繪制代碼是應(yīng)該寫在 super.onDraw()
的上面還是下面攘残。
1.1 寫在 super.onDraw() 的下面
把繪制代碼寫在 super.onDraw()
的下面,由于繪制代碼會在原有內(nèi)容繪制結(jié)束之后才執(zhí)行为狸,所以繪制內(nèi)容就會蓋住控件原來的內(nèi)容歼郭。
這是最為常見的情況:為控件增加點綴性內(nèi)容。比如辐棒,在 Debug 模式下繪制出 ImageView 的圖像尺寸信息:
public class AppImageView extends ImageView {
...
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (DEBUG) {
// 在 debug 模式下繪制出 drawable 的尺寸信息
...
}
}
}
這招很好用的病曾,試過嗎?
當(dāng)然漾根,除此之外還有其他的很多用法泰涂,具體怎么用就取決于你的需求、經(jīng)驗和想象力了立叛。
1.2 寫在 super.onDraw() 的上面
如果把繪制代碼寫在 super.onDraw()
的上面负敏,由于繪制代碼會執(zhí)行在原有內(nèi)容的繪制之前,所以繪制的內(nèi)容會被控件的原內(nèi)容蓋住秘蛇。
相對來說其做,這種用法的場景就會少一些顶考。不過只是少一些而不是沒有,比如你可以通過在文字的下層繪制純色矩形來作為「強調(diào)色」:
public class AppTextView extends TextView {
...
protected void onDraw(Canvas canvas) {
... // 在 super.onDraw() 繪制文字之前妖泄,先繪制出被強調(diào)的文字的背景
super.onDraw(canvas);
}
}
2 dispatchDraw():繪制子 View 的方法
講了這幾期驹沿,到目前為止我只提到了 onDraw()
這一個繪制方法。但其實繪制方法不是只有一個的蹈胡,而是有好幾個渊季,其中 onDraw()
只是負(fù)責(zé)自身主體內(nèi)容繪制的。而有的時候罚渐,你想要的遮蓋關(guān)系無法通過 onDraw()
來實現(xiàn)却汉,而是需要通過別的繪制方法。
例如荷并,你繼承了一個 LinearLayout
合砂,重寫了它的 onDraw()
方法,在 super.onDraw()
中插入了你自己的繪制代碼源织,使它能夠在內(nèi)部繪制一些斑點作為點綴:
public class SpottedLinearLayout extends LinearLayout {
...
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
... // 繪制斑點
}
}
看起來沒問題對吧翩伪?
但是你會發(fā)現(xiàn),當(dāng)你添加了子 View 之后谈息,你的斑點不見了:
<SpottedLinearLayout
android:orientation="vertical"
... >
<ImageView ... />
<TextView ... />
</SpottedLinearLayout>
造成這種情況的原因是 Android 的繪制順序:在繪制過程中缘屹,每一個 ViewGroup 會先調(diào)用自己的 onDraw()
來繪制完自己的主體之后再去繪制它的子 View。對于上面這個例子來說侠仇,就是你的 LinearLayout
會在繪制完斑點后再去繪制它的子 View轻姿。那么在子 View 繪制完成之后,先前繪制的斑點就被子 View 蓋住了傅瞻。
具體來講踢代,這里說的「繪制子 View」是通過另一個繪制方法的調(diào)用來發(fā)生的,這個繪制方法叫做:dispatchDraw()
嗅骄。也就是說,在繪制過程中饼疙,每個 View 和 ViewGroup 都會先調(diào)用 onDraw()
方法來繪制主體溺森,再調(diào)用 dispatchDraw()
方法來繪制子 View。
注:雖然
View
和ViewGroup
都有dispatchDraw()
方法窑眯,不過由于 View 是沒有子 View 的屏积,所以一般來說dispatchDraw()
這個方法只對ViewGroup
(以及它的子類)有意義。
回到剛才的問題:怎樣才能讓 LinearLayout
的繪制內(nèi)容蓋住子 View 呢磅甩?只要讓它的繪制代碼在子 View 的繪制之后再執(zhí)行就好了炊林。
2.1 寫在 super.dispatchDraw() 的下面
只要重寫 dispatchDraw()
,并在 super.dispatchDraw()
的下面寫上你的繪制代碼卷要,這段繪制代碼就會發(fā)生在子 View 的繪制之后渣聚,從而讓繪制內(nèi)容蓋住子 View 了独榴。
public class SpottedLinearLayout extends LinearLayout {
...
// 把 onDraw() 換成了 dispatchDraw()
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
... // 繪制斑點
}
}
好萌的蝙蝠俠啊
2.2 寫在 super.dispatchDraw() 的上面
同理,把繪制代碼寫在 super.dispatchDraw()
的上面奕枝,這段繪制就會在 onDraw()
之后棺榔、 super.dispatchDraw()
之前發(fā)生,也就是繪制內(nèi)容會出現(xiàn)在主體內(nèi)容和子 View 之間隘道。而這個……
其實和前面 1.1 講的症歇,重寫 onDraw()
并把繪制代碼寫在 super.onDraw()
之后的做法,效果是一樣的谭梗。
能想明白為什么吧忘晤?圖就不上了。
3 繪制過程簡述
繪制過程中最典型的兩個部分是上面講到的主體和子 View激捏,但它們并不是繪制過程的全部设塔。除此之外,繪制過程還包含一些其他內(nèi)容的繪制缩幸。具體來講壹置,一個完整的繪制過程會依次繪制以下幾個內(nèi)容:
- 背景
- 主體(
onDraw()
) - 子 View(
dispatchDraw()
) - 滑動邊緣漸變和滑動條
- 前景
一般來說,一個 View(或 ViewGroup)的繪制不會這幾項全都包含表谊,但必然逃不出這幾項娇斩,并且一定會嚴(yán)格遵守這個順序。例如通常一個 LinearLayout
只有背景和子 View炮沐,那么它會先繪制背景再繪制子 View泛烙;一個 ImageView
有主體,有可能會再加上一層半透明的前景作為遮罩距辆,那么它的前景也會在主體之后進(jìn)行繪制余佃。需要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的跨算;之前其實也有爆土,不過只支持 FrameLayout
,而直到 6.0 才把這個支持放進(jìn)了 View
類里诸蚕。
這其中的第 2步势、3 兩步,前面已經(jīng)講過了背犯;第 1 步——背景坏瘩,它的繪制發(fā)生在一個叫 drawBackground()
的方法里,但這個方法是 private
的漠魏,不能重寫倔矾,你如果要設(shè)置背景,只能用自帶的 API 去設(shè)置(xml 布局文件的 android:background
屬性以及 Java 代碼的 View.setBackgroundXxx()
方法,這個每個人都用得很 6 了)哪自,而不能自定義繪制丰包;而第 4、5 兩步——滑動邊緣漸變和滑動條以及前景提陶,這兩部分被合在一起放在了 onDrawForeground()
方法里烫沙,這個方法是可以重寫的。
滑動邊緣漸變和滑動條可以通過 xml 的 android:scrollbarXXX
系列屬性或 Java 代碼的 View.setXXXScrollbarXXX()
系列方法來設(shè)置隙笆;前景可以通過 xml 的 android:foreground
屬性或 Java 代碼的 View.setForeground()
方法來設(shè)置锌蓄。而重寫 onDrawForeground()
方法,并在它的 super.onDrawForeground()
方法的上面或下面插入繪制代碼撑柔,則可以控制繪制內(nèi)容和滑動邊緣漸變瘸爽、滑動條以及前景的遮蓋關(guān)系。
4 onDrawForeground()
首先铅忿,再說一遍剪决,這個方法是 API 23 才引入的,所以在重寫這個方法的時候要確認(rèn)你的
minSdk
達(dá)到了 23檀训,不然低版本的手機裝上你的軟件會沒有效果柑潦。
在 onDrawForeground()
中,會依次繪制滑動邊緣漸變峻凫、滑動條和前景渗鬼。所以如果你重寫 onDrawForeground()
:
4.1 寫在 super.onDrawForeground() 的下面
如果你把繪制代碼寫在了 super.onDrawForeground()
的下面,繪制代碼會在滑動邊緣漸變荧琼、滑動條和前景之后被執(zhí)行譬胎,那么繪制內(nèi)容將會蓋住滑動邊緣漸變、滑動條和前景命锄。
public class AppImageView extends ImageView {
...
public void onDrawForeground(Canvas canvas) {
super.onDrawForeground(canvas);
... // 繪制「New」標(biāo)簽
}
}
<!-- 使用半透明的黑色作為前景堰乔,這是一種很常見的處理 -->
<AppImageView
...
android:foreground="#88000000" />
左上角的標(biāo)簽并沒有被黑色遮罩蓋住,而是保持了原有的顏色脐恩。
4.2 寫在 super.onDrawForeground() 的上面
如果你把繪制代碼寫在了 super.onDrawForeground()
的上面镐侯,繪制內(nèi)容就會在 dispatchDraw()
和 super.onDrawForeground()
之間執(zhí)行,那么繪制內(nèi)容會蓋住子 View驶冒,但被滑動邊緣漸變析孽、滑動條以及前景蓋住:
public class AppImageView extends ImageView {
...
public void onDrawForeground(Canvas canvas) {
... // 繪制「New」標(biāo)簽
super.onDrawForeground(canvas);
}
}
由于被半透明黑色遮罩蓋住只怎,左上角的標(biāo)簽明顯變暗了。
這種寫法怜俐,和前面 2.1 講的身堡,重寫 dispatchDraw()
并把繪制代碼寫在 super.dispatchDraw()
的下面的效果是一樣的:繪制內(nèi)容都會蓋住子 View,但被滑動邊緣漸變拍鲤、滑動條以及前景蓋住贴谎。
4.3 想在滑動邊緣漸變汞扎、滑動條和前景之間插入繪制代碼?
很簡單:不行擅这。
雖然這三部分是依次繪制的澈魄,但它們被一起寫進(jìn)了 onDrawForeground()
方法里,所以你要么把繪制內(nèi)容插在它們之前仲翎,要么把繪制內(nèi)容插在它們之后痹扇。而想往它們之間插入繪制,是做不到的溯香。
5 draw() 總調(diào)度方法
除了 onDraw()
dispatchDraw()
和 onDrawForeground()
之外鲫构,還有一個可以用來實現(xiàn)自定義繪制的方法: draw()
。
draw() 是繪制過程的總調(diào)度方法玫坛。一個 View 的整個繪制過程都發(fā)生在 draw()
方法里结笨。前面講到的背景、主體湿镀、子 View 炕吸、滑動相關(guān)以及前景的繪制,它們其實都是在 draw()
方法里的勉痴。
// View.java 的 draw() 方法的簡化版大致結(jié)構(gòu)(是大致結(jié)構(gòu)赫模,不是源碼哦):
public void draw(Canvas canvas) {
...
drawBackground(Canvas); // 繪制背景(不能重寫)
onDraw(Canvas); // 繪制主體
dispatchDraw(Canvas); // 繪制子 View
onDrawForeground(Canvas); // 繪制滑動相關(guān)和前景
...
}
從上面的代碼可以看出,onDraw()
dispatchDraw()
onDrawForeground()
這三個方法在 draw()
中被依次調(diào)用蚀腿,因此它們的遮蓋關(guān)系也就像前面所說的——dispatchDraw()
繪制的內(nèi)容蓋住 onDraw()
繪制的內(nèi)容嘴瓤;onDrawForeground()
繪制的內(nèi)容蓋住 dispatchDraw()
繪制的內(nèi)容。而在它們的外部莉钙,則是由 draw()
這個方法作為總的調(diào)度廓脆。所以,你也可以重寫 draw()
方法來做自定義的繪制磁玉。
5.1 寫在 super.draw() 的下面
由于 draw()
是總調(diào)度方法停忿,所以如果把繪制代碼寫在 super.draw()
的下面,那么這段代碼會在其他所有繪制完成之后再執(zhí)行蚊伞,也就是說席赂,它的繪制內(nèi)容會蓋住其他的所有繪制內(nèi)容。
它的效果和重寫 onDrawForeground()
时迫,并把繪制代碼寫在 super.onDrawForeground()
下面時的效果是一樣的:都會蓋住其他的所有內(nèi)容颅停。
當(dāng)然了,雖說它們效果一樣掠拳,但如果你既重寫
draw()
又重寫onDrawForeground()
癞揉,那么draw()
里的內(nèi)容還是會蓋住onDrawForeground()
里的內(nèi)容的。所以嚴(yán)格來講,它們的效果還是有一點點不一樣的喊熟。但這屬于抬杠……
5.2 寫在 super.draw() 的上面
同理柏肪,由于 draw()
是總調(diào)度方法,所以如果把繪制代碼寫在 super.draw()
的上面芥牌,那么這段代碼會在其他所有繪制之前被執(zhí)行烦味,所以這部分繪制內(nèi)容會被其他所有的內(nèi)容蓋住,包括背景壁拉。是的谬俄,背景也會蓋住它。
是不是覺得沒用扇商?覺得怎么可能會有誰想要在背景的下面繪制內(nèi)容凤瘦?別這么想,有的時候它還真的有用案铺。
例如我有一個 EditText
:
它下面的那條橫線蔬芥,是 EditText
的背景。所以如果我想給這個 EditText
加一個綠色的底控汉,我不能使用給它設(shè)置綠色背景色的方式笔诵,因為這就相當(dāng)于是把它的背景替換掉,從而會導(dǎo)致下面的那條橫線消失:
<EditText
...
android:background="#66BB6A" />
EditText
:我到底是個EditText
還是個TextView
姑子?傻傻分不清楚乎婿。
在這種時候,你就可以重寫它的 draw()
方法街佑,然后在 super.draw()
的上方插入代碼谢翎,以此來在所有內(nèi)容的底部涂上一片綠色:
public class SpottedLinearLayout extends LinearLayout {
...
// 把 onDraw() 換成了 dispatchDraw()
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
... // 繪制斑點
}
}
當(dāng)然,這種用法并不常見沐旨,事實上我也并沒有在項目中寫過這樣的代碼森逮。但我想說的是,我們作為工程師磁携,是無法預(yù)知將來會遇到怎樣的需求的褒侧。我們能做的只能是盡量地去多學(xué)習(xí)一些、多掌握一些谊迄,盡量地了解我們能夠做什么闷供、怎么做,然后在需求到來的時候统诺,就可以多一些自如歪脏,少一些束手無策。
注意
關(guān)于繪制方法粮呢,有兩點需要注意一下:
- 出于效率的考慮唾糯,
ViewGroup
默認(rèn)會繞過draw()
方法怠硼,換而直接執(zhí)行dispatchDraw()
,以此來簡化繪制流程移怯。所以如果你自定義了某個ViewGroup
的子類(比如LinearLayout
)并且需要在它的除dispatchDraw()
以外的任何一個繪制方法內(nèi)繪制內(nèi)容,你可能會需要調(diào)用View.setWillNotDraw(false)
這行代碼來切換到完整的繪制流程(是「可能」而不是「必須」的原因是这难,有些 ViewGroup 是已經(jīng)調(diào)用過setWillNotDraw(false)
了的舟误,例如ScrollView
)。 - 有的時候姻乓,一段繪制代碼寫在不同的繪制方法中效果是一樣的嵌溢,這時你可以選一個自己喜歡或者習(xí)慣的繪制方法來重寫。但有一個例外:如果繪制代碼既可以寫在
onDraw()
里蹋岩,也可以寫在其他繪制方法里赖草,那么優(yōu)先寫在onDraw()
,因為 Android 有相關(guān)的優(yōu)化剪个,可以在不需要重繪的時候自動跳過onDraw()
的重復(fù)執(zhí)行秧骑,以提升開發(fā)效率。享受這種優(yōu)化的只有onDraw()
一個方法扣囊。
總結(jié)
今天的內(nèi)容就是這些:使用不同的繪制方法乎折,以及在重寫的時候把繪制代碼放在 super.繪制方法()
的上面或下面不同的位置,以此來實現(xiàn)需要的遮蓋關(guān)系侵歇。下面用一張圖和一個表格總結(jié)一下:
嗯骂澄,上面這張圖在前面已經(jīng)貼過了,不用比較了完全一樣的惕虑。
另外別忘了上面提到的那兩個注意事項:
- 在
ViewGroup
的子類中重寫除dispatchDraw()
以外的繪制方法時坟冲,可能需要調(diào)用setWillNotDraw(false)
; - 在重寫的方法有多個選擇時溃蔫,優(yōu)先選擇
onDraw()
健提。
練習(xí):FlowLayout自適應(yīng)容器實現(xiàn)
XML布局
提取margin與onMeasure()重寫
提取margin
要提取margin,就一定要重寫generateLayoutParams
重寫onMeasure()——計算當(dāng)前FlowLayout所占的寬高
這里就要重寫onMeasure()函數(shù)酒唉,在其中計算所有當(dāng)前container所占的大小矩桂。
要做FlowLayout,首先涉及下面幾個問題:
- 何時換行
從效果圖中可以看到,F(xiàn)lowLayout的布局是一行行的痪伦,如果當(dāng)前行已經(jīng)放不下 下一個 控件侄榴,那就把這個控件移到下一行顯示。所以我們要有個變量來計算當(dāng)前行已經(jīng)占據(jù)的寬度网沾,以判斷剩下的空間是否還能容得下 下一個 控件癞蚕。
- 如何得到FlowLayout的寬度
FlowLayout的寬度是所有行寬度的最大值,所以我們要記錄下每一行的所占據(jù)的寬度值辉哥,進(jìn)而找到所有值中的最大值桦山。
- 如何得到FlowLayout的高度
很顯然攒射,F(xiàn)lowLayout的高度是每一行高度的總和,而每一行的高度則是取該行中所有控件高度的最大值恒水。
重寫onLayout()——布局所有子控件
代碼和注釋在com.richy.kotlindemo.customview.FlowLayout
中
class FlowLayout : ViewGroup {
override fun generateLayoutParams(p: ViewGroup.LayoutParams): ViewGroup.LayoutParams {
return ViewGroup.MarginLayoutParams(p)
}
override fun generateLayoutParams(attrs: AttributeSet): ViewGroup.LayoutParams {
return ViewGroup.MarginLayoutParams(context, attrs)
}
override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams {
return ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT)
}
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// (1)首先会放,剛進(jìn)來的時候是利用MeasureSpec獲取系統(tǒng)建議的數(shù)值的模式
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val measureWidth = View.MeasureSpec.getSize(widthMeasureSpec)
val measureHeight = View.MeasureSpec.getSize(heightMeasureSpec)
val measureWidthMode = View.MeasureSpec.getMode(widthMeasureSpec)
val measureHeightMode = View.MeasureSpec.getMode(heightMeasureSpec)
// (2)然后,是計算FlowLayout所占用的空間大小
var lineWidth = 0//記錄每一行的寬度
var lineHeight = 0//記錄每一行的高度
var height = 0//記錄整個FlowLayout所占高度
var width = 0//記錄整個FlowLayout所占寬度
//計算
val count = childCount
for (i in 0 until count) {
val child = getChildAt(i)
//這里一定要注意的是:在調(diào)用child.getMeasuredWidth()钉凌、child.getMeasuredHeight()之前咧最,
// 一定要調(diào)用measureChild(child,widthMeasureSpec,heightMeasureSpec);
// 在onMeasure()之后才能調(diào)用getMeasuredWidth()獲得值;同樣御雕,只有調(diào)用onLayout()后,getWidth()才能獲取值
measureChild(child, widthMeasureSpec, heightMeasureSpec)
val lp = child.layoutParams as ViewGroup.MarginLayoutParams
val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin
if (lineWidth + childWidth > measureWidth) {
//換行
//FlowLayout的寬度是所有行寬度的最大值酸纲,所以我們要記錄下每一行的所占據(jù)的寬度值捣鲸,進(jìn)而找到所有值中的最大值
width = Math.max(lineWidth, width)
height += lineHeight
//因為由于盛不下當(dāng)前控件,而將此控件調(diào)到下一行闽坡,所以將此控件的高度和寬度初始化給lineHeight栽惶、lineWidth
lineHeight = childHeight
lineWidth = childWidth
} else {
//累加
lineHeight = Math.max(lineHeight, childHeight)
lineWidth += childWidth
}
//最后一行是不會超出width范圍的,所以要單獨處理
if (i == count - 1) {
height += lineHeight
width = Math.max(width, lineWidth)
}
}
//當(dāng)屬性是MeasureSpec.EXACTLY時无午,那么它的高度就是確定的媒役,
// 只有當(dāng)是wrap_content時,根據(jù)內(nèi)部控件的大小來確定它的大小時宪迟,大小是不確定的酣衷,屬性是AT_MOST,
// 此時,就需要我們自己計算它的應(yīng)當(dāng)?shù)拇笮〈卧螅⒃O(shè)置進(jìn)去
setMeasuredDimension(if (measureWidthMode == View.MeasureSpec.EXACTLY) measureWidth else width,
if (measureHeightMode == View.MeasureSpec.EXACTLY) measureHeight else height)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var lineWidth = 0
var lineHeight = 0
var top = 0
var left = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
val lp = child.layoutParams as ViewGroup.MarginLayoutParams
//這樣做其實不好穿仪,這樣把margin當(dāng)做了padding處理了,包含在了child里面了
val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin
if (childWidth + lineWidth > measuredWidth) {
//如果換行,當(dāng)前控件將跑到下一行意荤,從最左邊開始啊片,所以left就是0,而top則需要加上上一行的行高玖像,才是這個控件的top點;
top += lineHeight
left = 0
//同樣紫谷,重新初始化lineHeight和lineWidth
lineHeight = childHeight
lineWidth = childWidth
} else {
//不換行
lineHeight = Math.max(lineHeight, childHeight)
lineWidth += childWidth
}
//計算childView的left,top,right,bottom
val lc = left + lp.leftMargin
val tc = top + lp.topMargin
val rc = lc + child.measuredWidth
val bc = tc + child.measuredHeight
child.layout(lc, tc, rc, bc)
//將left置為下一子控件的起始點
left += childWidth
}
}
}