基礎(chǔ)知識
View的構(gòu)造函數(shù)
1. View(Context)
- 在Java代碼里面new的時(shí)候調(diào)用。
2. View(Context, AttributeSet)
- 在.xml里聲明的時(shí)候調(diào)用,AttributeSet是從.xml中解析出來的屬性集合。
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/icon" />
- 上面.xml中的layout_width, layout_height 以及 src是從哪里來的?它們可不是憑空產(chǎn)生的;實(shí)際上是通過<declare-styleable>把這些屬性明確的聲明為系統(tǒng)需要處理的東西。比如,src就是在這里定義的:
<declare-styleable name="ImageView">
<!-- Sets a drawable as the content of this ImageView. -->
<attr name="src" format="reference|color" />
</declare-styleable>
- 每個(gè)declare-styleable產(chǎn)生一個(gè)R.styleable.[name],外加每個(gè)屬性的R.styleable.[name]_[attribute] 。比如,上面的代碼產(chǎn)生R.styleable.ImageView和R.styleable.ImageView_src。
- 這些資源是什么東西呢?R.styleable.[name]是所有屬性資源的數(shù)組,系統(tǒng)使用它來查找屬性值。每個(gè)R.styleable.[name]_[attribute]只不過是這個(gè)數(shù)組的索引罷了,所以你可以一次性取出所有屬性,然后按索引分別查詢每個(gè)的值。
- xml中屬性是以AttributeSet的形式通過構(gòu)造方法傳遞給View的,但通常我們不直接使用AttributeSet。而是使用Theme.obtainStyledAttributes()。這是因?yàn)樵嫉膶傩酝ǔP枰煤蛻?yīng)用樣式。比如,如果你在XML中定義了style=@style/MyStyle,這個(gè)方法先獲取MyStyle,然后把它的屬性混合進(jìn)去。最終obtainStyledAttributes() 返回一個(gè)TypedArray,你可以用它來獲取屬性值。這個(gè)過程簡化之后就像這樣:
public ImageView(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ImageView, 0, 0);
Drawable src = ta.getDrawable(R.styleable.ImageView_src);
setImageDrawable(src);
ta.recycle();
}
- 這里我們向obtainStyledAttributes()傳遞了兩個(gè)參數(shù)。第一個(gè)參數(shù)是AttributeSet attrs,即xml中的屬性;]第二個(gè)參數(shù)是R.styleable.ImageView數(shù)組,它告訴這個(gè)方法我們想取哪個(gè)屬性的值,這里表示要獲取ImageView屬性的值;第三和第四個(gè)參數(shù)是兩個(gè)資源引用defStyleAttr和defStyleRes,將在第三和第四個(gè)構(gòu)造方法中進(jìn)行說明。
- 當(dāng)獲得了TypedArray之后,我們就可以獲取單個(gè)屬性了。我們需要使用R.styleable.ImageView_src來正確索引數(shù)組中的src屬性。
3. View(Context, AttributeSet, defStyleAttr)
- defStyleAttr參數(shù):默認(rèn)的Style,指它在當(dāng)前Application或Activity所用的Theme中的默認(rèn)Style,為某個(gè)類型的View定義這個(gè)類的基礎(chǔ)樣式,如果我們不在構(gòu)造方法傳入指定我們自定義的樣式則將使用Andoid系統(tǒng)默認(rèn)的控件樣式,指定時(shí)需要間接的通過theme,如下所示:
- 在Theme(styles.xml)中設(shè)置樣式
<resources>
<style name="Theme">
<item name="mStyle">@style/CustomStyle</item>
</style>
<!--具體樣式-->
<style name="CustomStyle" >
<item name="android:background">@android:color/black</item>
</style>
</resource>
- 在構(gòu)造方法中使用
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.view, R.attr.mStyle, 0);
4. View(Context, AttributeSet, defStyleAttr, defStyleRes)
- defStyleRes參數(shù):它只是一個(gè)用于指定樣式的style資源(@style/Widget.TextView)。比defStyleAttr簡單,不需要間接的通過theme。在API 21添加的。因此除非你的minSdkVersion為21,否則不要使用它。
它們是串聯(lián)的,如果你調(diào)用了一個(gè),所有的都會(huì)通過super被調(diào)用。串聯(lián)還意味著你只需重寫你需要的構(gòu)造函數(shù)。一般來說,你只需實(shí)現(xiàn)前兩個(gè)(一個(gè)用于代碼,一個(gè)用于XML inflation)。
View視圖結(jié)構(gòu)
對于多View的視圖,結(jié)構(gòu)是樹形結(jié)構(gòu):最頂層是ViewGroup,ViewGroup下可能有多個(gè)ViewGroup或View,如下圖:

注意:無論是measure過程、layout過程還是draw過程,永遠(yuǎn)都是從View樹的根節(jié)點(diǎn)開始測量或計(jì)算(即從樹的頂端開始),一層一層、一個(gè)分支一個(gè)分支地進(jìn)行(即樹形遞歸),最終計(jì)算整個(gè)View樹中各個(gè)View,最終確定整個(gè)View樹的相關(guān)屬性。
Android坐標(biāo)系
- Android的坐標(biāo)系定義為:
屏幕的左上角為坐標(biāo)原點(diǎn)
向右為x軸增大方向
向下為y軸增大方向 -
具體如下圖:
Android屏幕坐標(biāo)系.png
View位置(坐標(biāo))描述
- View的位置由4個(gè)頂點(diǎn)決定的(如下圖A、B、C、D)
- 4個(gè)頂點(diǎn)的位置描述分別由4個(gè)值決定(View的位置是相對于父控件而言的):
Top:子View上邊界到父view上邊界的距離
Left:子View左邊界到父view左邊界的距離
Bottom:子View下邊距到父View上邊界的距離
Right:子View右邊界到父view左邊界的距離

View位置獲取方式
- View的位置是通過view.getxxx()函數(shù)進(jìn)行獲取(以Top為例):
// 獲取Top位置
public final int getTop() {
return mTop;
}
// 其余如下:
getLeft(); //獲取子View左上角距父View左側(cè)的距離
getBottom(); //獲取子View右下角距父View頂部的距離
getRight(); //獲取子View右下角距父View左側(cè)的距離
- 與MotionEvent中g(shù)et...()和getRaw...()的區(qū)別
//get() :觸摸點(diǎn)相對于其所在組件坐標(biāo)系的坐標(biāo)
event.getX();
event.getY();
//getRaw() :觸摸點(diǎn)相對于屏認(rèn)坐標(biāo)系的坐標(biāo)
event.getRawX();
event.getRawY();
具體如下圖:

Android的角度(angle)與弧度(radian)
-
角度和弧度都是描述角的一種度量單位,區(qū)別如下圖:
角度和弧度區(qū)別.png -
在默認(rèn)的屏幕坐標(biāo)系中角度增大方向?yàn)轫槙r(shí)針,與數(shù)學(xué)坐標(biāo)系中角度增大方向剛好相反。
屏幕角度增大方向.png
Android中顏色相關(guān)內(nèi)容
-
Android支持的顏色模式:
Android顏色模式.png -
以ARGB8888為例介紹顏色定義:
ARGB8888.png
定義顏色的方式
- 在java中定義顏色
//java中使用Color類定義顏色
int color = Color.GRAY; //灰色
//Color類中使用ARGB值表示顏色
int color = Color.argb(127, 255, 0, 0); //半透明紅色
//使用十六進(jìn)制定義顏色
int color = 0xaaff0000; //帶有透明度的紅色
//Android中Color工具類 parseColor解析顏色字符串
int color = Color.parseColor("#FFFFFF")//白色
- 在xml文件中定義顏色
/res/values/color.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
//定義了紅色(沒有alpha(透明)通道)
<color name="red">#ff0000</color>
//定義了藍(lán)色(沒有alpha(透明)通道)
<color name="green">#00ff00</color>
</resources>
- 在xml文件(不局限與布局xml文件,其它xml文件也可以用這種定義方式)中以"#"開頭定義顏色,后面跟十六進(jìn)制的值,有如下幾種定義方式:
#f00 //低精度 - 不帶透明通道紅色 == #ff0000
#af00 //低精度 - 帶透明通道紅色 == #aaff0000
#ff0000 //高精度 - 不帶透明通道紅色
#aaff0000 //高精度 - 帶透明通道紅色
引用顏色的方式
- 在java文件中引用color.xml中定義的顏色:
int color = getResources().getColor(R.color.mycolor);
- 在xml文件(layout或style)中引用或者創(chuàng)建顏色:
<!--在style文件中引用-->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/red</item>
</style>
<!--在layout文件中引用在/res/values/color.xml中定義的顏色-->
android:background="@color/red"
<!--在layout文件中創(chuàng)建并使用顏色-->
android:background="#ff0000"
View的繪制流程
- View及ViewGroup基本相同,只是在ViewGroup中不僅要繪制自己還是繪制其中的子控件,而View則只需要繪制自己就可以了,所以我們這里就以ViewGroup為例來講述整個(gè)繪制流程。
- 繪制流程分為三步:測量、布局、繪制 ,分別對應(yīng):onMeasure()、onLayout()、onDraw() 。
- 其中,他們?nèi)齻€(gè)的作用分別如下:
onMeasure():測量自己的大小,為正式布局提供建議。(注意,只是建議,至于用不用,要看onLayout);
onLayout():使用layout()函數(shù)對所有子控件布局;
onDraw():根據(jù)布局的位置繪圖。
- 布局繪畫前涉及兩個(gè)過程:測量過程和布局過程。測量過程通過measure方法實(shí)現(xiàn),是View樹自頂向下的遍歷,每個(gè)View在循環(huán)過程中將尺寸細(xì)節(jié)往下傳遞,當(dāng)測量過程完成之后,所有的View都存儲(chǔ)了自己的尺寸。第二個(gè)過程則是通過方法layout來實(shí)現(xiàn)的,也是自頂向下的;在這個(gè)過程中,每個(gè)父View負(fù)責(zé)通過計(jì)算好的尺寸放置它的子View。
onMeasure()
- 首先,看一下onMeasure()的聲明:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
- 這里我們主要關(guān)注傳進(jìn)來的兩個(gè)參數(shù):int widthMeasureSpec, int heightMeasureSpec。
- 與這兩個(gè)參數(shù)有關(guān)的是兩個(gè)問題:意義和組成。即他們是怎么來的,表示什么意思;還有,他們是組成方式是怎樣的。 我們先說他們的意義: 他們是父類傳遞過來給當(dāng)前view的一個(gè)建議值,即想把當(dāng)前view的尺寸設(shè)置為寬widthMeasureSpec,高h(yuǎn)eightMeasureSpec。
- 有關(guān)他們的組成,我們就直接轉(zhuǎn)到MeasureSpec部分。
MeasureSpec
- 雖然表面上看起來他們是int類型的數(shù)字,其實(shí)他們是由mode+size兩部分組成的。
widthMeasureSpec和heightMeasureSpec轉(zhuǎn)化成二進(jìn)制數(shù)字表示,他們都是32位的。前兩位代表mode(測量模式),后面30位才是他們的實(shí)際數(shù)值(size)。
- 模式分類
它有三種模式:
- UNSPECIFIED(未指定),父元素不對子元素施加任何束縛,子元素可以得到任意想要的大?。?/li>
- EXACTLY(完全),父元素決定自元素的確切大小,子元素將被限定在給定的邊界里而忽略它本身大??;
- AT_MOST(至多),子元素至多達(dá)到指定大小的值。
- widthMeasureSpec和heightMeasureSpec各自都有它對應(yīng)的模式,XML布局和模式有如下對應(yīng)關(guān)系:
- wrap_content-> MeasureSpec.AT_MOST
- match_parent -> MeasureSpec.EXACTLY
- 具體值 -> MeasureSpec.EXACTLY
- MeasureSpec.UNSPECIFIED一般自定義View用不到,系統(tǒng)內(nèi)部ListView、ScrollView就是用的該模式。
- 模式提取
三種測量模式對應(yīng)的二進(jìn)制值分別是:
UNSPECIFIED=00000000000000000000000000000000
EXACTLY =01000000000000000000000000000000
AT_MOST =10000000000000000000000000000000
最前面兩位代表模式,分別對應(yīng)十進(jìn)制的0,1,2;
- widthMeasureSpec和heightMeasureSpec是由模式和數(shù)值組成的,而且二進(jìn)制的前兩位代表模式,后30位代表數(shù)值。
- 我們先想想,如果我們自己來提取widthMeasureSpec和heightMeasureSpec中的模式和數(shù)值是怎么提取呢?
- 首先想到的肯定是通過掩碼(MASK)和與運(yùn)算去掉不需要的部分而得到對應(yīng)的模式或數(shù)值。
說到這大家可能會(huì)迷茫,我們寫段代碼來提取模式部分吧:
//對應(yīng)11000000000000000000000000000000;總共32位,前兩位是1
int MODE_MASK = 0xc0000000;
//提取模式
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
//提取數(shù)值
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
- 相信大家看了代碼就應(yīng)該清楚模式和數(shù)值提取的方法了吧,主要用到了MASK的與、非運(yùn)算將不需要的位數(shù)替換成0,保留相應(yīng)的位數(shù)。
- 上面我們自已實(shí)現(xiàn)了模式和數(shù)值的提取。但在強(qiáng)大的andorid面前,肯定有提供提取模式和數(shù)值的類。這個(gè)類就是MeasureSpec,下面兩個(gè)函數(shù)就可以實(shí)現(xiàn)這個(gè)功能:
MeasureSpec.getMode(int spec) //獲取MODE
MeasureSpec.getSize(int spec) //獲取數(shù)值
- 通過下面的代碼就可以分別獲取widthMeasureSpec和heightMeasureSpec的模式和數(shù)值。
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
- 其實(shí)大家通過查看源碼可以知道,我們上面模式提取的代碼就是MeasureSpec.getMode()和MeasureSpec.getSize()的實(shí)現(xiàn)。
- onMeasure()是用來測量當(dāng)前控件大小的,給onLayout()提供數(shù)值參考,需要特別注意的是:測量完成以后通過setMeasuredDimension(int,int)設(shè)置給系統(tǒng)。
- 我們可以給View設(shè)置LayoutParams,在View測量的時(shí)候,系統(tǒng)會(huì)將LayoutParams在父容器的約束下轉(zhuǎn)換成對應(yīng)的MeasureSpec,然后再根據(jù)這個(gè)MeasureSpec來確定View測量后的寬高。
- 子view的大小由父view的MeasureSpec值和子view的LayoutParams屬性共同決定。
- 實(shí)例
當(dāng)模式是MeasureSpec.EXACTLY時(shí),我們就不必要設(shè)定我們計(jì)算的大小了,因?yàn)檫@個(gè)大小是用戶指定的,我們不應(yīng)更改。但當(dāng)模式是MeasureSpec.AT_MOST時(shí),也就是說用戶將布局設(shè)置成了wrap_content,我們就需要將大小設(shè)定為我們計(jì)算的數(shù)值,因?yàn)橛脩舾緵]有設(shè)置具體值是多少,需要我們自己計(jì)算。即,假如width和height是我們經(jīng)過計(jì)算的控件所占的寬度和高度。那在onMeasure()中使用setMeasuredDimension()最后設(shè)置時(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)過計(jì)算,控件所占的寬和高分別對應(yīng)width和height
//計(jì)算過程,我們會(huì)在onLayout中細(xì)講
…
setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height);
}
onLayout()
- onLayout()是實(shí)現(xiàn)所有子控件布局的函數(shù),它自己的布局由它的父容器進(jìn)行,直到所有控件的最頂層結(jié)點(diǎn)ViewRoot,它會(huì)通過SetFrame(l,t,r,b)設(shè)置自己的位置。
- 我們先看看ViewGroup的onLayout()函數(shù)的默認(rèn)行為是什么。
在ViewGroup.java中
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
- 是一個(gè)抽象方法,說明凡是派生自ViewGroup的類都必須自己去實(shí)現(xiàn)這個(gè)方法(View的話是可以不實(shí)現(xiàn)的,View也沒實(shí)現(xiàn)該方法的必要)。像LinearLayout、RelativeLayout等布局,都是重寫了這個(gè)方法,然后在內(nèi)部按照各自的規(guī)則對子視圖進(jìn)行布局的。
實(shí)例
-
下面我們就舉個(gè)例子來看一下有關(guān)onMeasure()和onLayout()的具體使用。
下面是效果圖:
自定義View.png - XML布局
<?xml version="1.0" encoding="utf-8"?>
<com.lg.www.animblog.MyLinLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ff00ff"
tools:context=".MainActivity">
<TextView android:text="第一個(gè)VIEW"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView android:text="第二個(gè)VIEW"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView android:text="第三個(gè)VIEW"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</com.lg.www.animblog.MyLinLayout>
- MyLinLayout實(shí)現(xiàn)
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
public class MyLinLayout extends ViewGroup {
public MyLinLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
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);
int height = 0;
int width = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
//測量子控件
measureChild(child, widthMeasureSpec, heightMeasureSpec);
//獲得子控件的高度和寬度
int childHeight = child.getMeasuredHeight();
int childWidth = child.getMeasuredWidth();
/*因?yàn)槲覀兪谴怪迸帕衅鋬?nèi)部所有的View,
所以容器所占寬度應(yīng)該是各個(gè)TextVIew中的最大寬度,所占高度應(yīng)該是所有控件的高度和。*/
width = Math.max(childWidth, width);
height += childHeight;
}
setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
int childHeight = child.getMeasuredHeight();
int childWidth = child.getMeasuredWidth();
child.layout(0, top, childWidth, top + childHeight);
//垂直排列,上頂點(diǎn)依次累加
top += childHeight;
}
}
}
getMeasuredWidth()與getWidth()
- 這里引出了一個(gè)很容易被混淆的問題:getMeasuredWidth()與getWidth()的區(qū)別。
- 他們的值大部分時(shí)間都是相同的,但意義確是根本不一樣的,我們就來簡單分析一下。
- 區(qū)別主要體現(xiàn)在下面幾點(diǎ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)來定義了各個(gè)子控件所應(yīng)在的位置:
int childHeight = child.getMeasuredHeight();
int childWidth = child.getMeasuredWidth();
child.layout(0, top, childWidth, top + childHeight);
從代碼中可以看到,我們使用child.layout(0, top, childWidth, top + childHeight);來布局控件的位置,其中g(shù)etWidth()的取值就是這里的右坐標(biāo)減去左坐標(biāo)的寬度;因?yàn)槲覀冞@里的寬度是直接使用的child.getMeasuredWidth()的值,當(dāng)然會(huì)導(dǎo)致getMeasuredWidth()與getWidth()的值是一樣的。如果我們在調(diào)用layout()的時(shí)候傳進(jìn)去的寬度值不與getMeasuredWidth()相同,那必然getMeasuredWidth()與getWidth()的值就不再一樣了。
onDraw()


感謝
深入理解Android View的構(gòu)造函數(shù)
View的第三個(gè)構(gòu)造函數(shù)的第三個(gè)參數(shù)defStyle
自定義View基礎(chǔ) - 最易懂的自定義View原理系列(1)
自定義控件三部曲視圖篇(一)——測量與布局





