第3章 Android 控件架構(gòu)與自定義控件詳解
1.Android控件架構(gòu)
控件大致分為兩類,ViewGroup控件和View控件。如圖在UI界面架構(gòu)中,每個Activity都包含一個Window對象,Window對象通常由PhoneWindow實現(xiàn)。PhoneWindow將一個DecorView設(shè)置為整個應用窗口的根View,DecorView作為窗口的頂層視圖,里面的所有View的監(jiān)聽事件都通過WindowManagerService來接收,并通過Activity對象來回調(diào)相應的onClickListener。
其中,DecorView包含兩部分:TitleView和ContentView。
ContentView實際是一個FrameLayout,所以設(shè)置requestWindowFeature
(Window.FEATURE_NOTITLE)一定要在調(diào)用setContentView()方法之前,才能生效。
當程序在onCreate()方法中調(diào)用setContentView()方法后,ActivityManagerService會回調(diào)onResume()方法,此時系統(tǒng)才會將整個DecorView添加到PhoneWindow中,并讓其顯示出來,從而完成界面的繪制。

擴展: Activity界面繪制過程詳解
2.View的測量
Android的View的測量時通過MeasureSpec類實現(xiàn),它是32位int值,高兩位為測量模式,低30位為測量的大小,有三種測量模式:
- 精確值模式(EXACTLY),如將控件layout_width或layout_height設(shè)置為具體數(shù)值或match_parent;
- 最大值模式(AT_MOST),控件layout_width或layout_height設(shè)置為wrap_content,控件大小隨內(nèi)容變化,不超過父控件允許的最大;
- UNSPECIFIED,不指定控件大小測量模式,View要多大就多大,通常在自定義View時使用;
View默認的onMeasure()方法只支持EXACTLY模式,所以自定義控件不重寫onMeasure(),就只能使用EXACTLY模式(具體值和match_parent屬性),要支持wrap_content屬性就必須重寫onMeasure()方法。重寫onMeasure方法最終工作就是把測量后的寬高值作為參數(shù)設(shè)置在setMeasuredDimension方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//計算width和height
setMeasuredDimension(width, height);
}
例如,ScrollView嵌套ListView或GridView,想要設(shè)置它們不出現(xiàn)滾動條,可以重寫它們的onMeasure方法,設(shè)置測量模式為AT_MOST,如下
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(
Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
3.View的繪制
View的onDraw()方法包含一個參數(shù)Canvas對象,使用這個Canvas對象就可以直接繪圖了。但是通常情況下,Canvas對象的創(chuàng)建需要傳入?yún)?shù)Bitmap,為什么呢?
因為傳進去的Bitmap與通過這個Bitmap創(chuàng)建的Canvas畫布是緊緊聯(lián)系在一起的,這個Bitmap用來存儲所有繪制在Canvas上的像素信息,當使用Bitmap創(chuàng)建Canvas之后,后面調(diào)用所有的Canvas.drawXXX方法都發(fā)生在這個Bitmap上。當View需要重繪時,更新bitmap就可以了
4.ViewGroup的測量與繪制
當ViewGroup大小為wrap_content時,需要遍歷子View,調(diào)用measure方法獲取每一個子View測量結(jié)果,然后將子View放到合適位置,也就是Layout的過程;自定義ViewGroup,通常需要重寫onLayout()方法來控制字View顯示邏輯,需要支持wrap_content時,還需要重寫onMeasure方法。
ViewGroup通常不需要繪制,因為它本身沒有需要繪制的東西,如果不指定ViewGroup的背景顏色,那么ViewGroup的onDraw方法都不會被調(diào)用。但是,ViewGroup會調(diào)用dispatchDraw方法來繪制其子view,其過程同樣是通過遍歷所有子view并調(diào)用子view的繪制方法來完成繪制工作的。
5.自定義View
自定義View時,通常會重寫onDraw()方法來繪制View的顯示內(nèi)容,如果需要使用到wrap_content,還必須重寫onMeasure()方法,另外,通過自定義attrs屬性,可以設(shè)置新的屬性配置值。自定義View時,比較重要的回調(diào)方法:
- onFinishInflate():從XML加載組件后回調(diào);
- onSizeChanged():組件大小改變時回調(diào);
- onMeasure():回調(diào)該方法來進行測量;
- onLayout():回調(diào)該方法來確定顯示的位置;
- onTouchEvent():監(jiān)聽到觸摸事件時回調(diào);
三種自定義控件的實現(xiàn)方式:
(1). 對現(xiàn)成控件進行擴展
一般可以在onDraw()方法對原生控件進行擴展
@Override
protected void onDraw(Canvas canvas) {
//在回調(diào)父類方法之前實現(xiàn)自己的邏輯,對TextView來說就是在繪制文本之前
super.onDraw(canvas);
//在回調(diào)父類方法之后實現(xiàn)自己的邏輯,對TextView來說就是在繪制文本之后
}
如書中,利用Android的繪圖機制實現(xiàn)動態(tài)文字閃動的效果。利用Paint對象的Shader渲染器,通過設(shè)置不斷變化的LinearGradient,并使用帶有該屬性的Paint對象來繪制要顯示的文字,首先在onSizeChanged()方法進行對象的初始化,并根據(jù)View的寬度設(shè)置一個LinearGradient漸變渲染器,如下:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh){
super.onSizeChanged(w, h, oldw, oldh){
if(mViewWidth==0){
mViewWidth=getMeasuredWidth();
if(mViewWidth>0){
mPaint=getPaint(); //關(guān)鍵,獲取當前TextView的Paint對象,設(shè)置LinearGradient屬性
mLinearGradient=new LinearGradient(0, 0, mViewWidth, 0, new
int[]{Color.BLUE, 0xffffffff, Color.BLUE}, null, Shader.TileMode.CLAMP);
mPaint.setShader(mLinearGradient);
mGradientMatrix=new Matrix();
}
}
}
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
if(mGradientMatrix!=null){
mTraslate+=mViewWidth/5;
if(mTranslate>2*mViewWidth){
mTranslate=-mViewWidth;
}
mGradientMatrix.setTranslate(mTranslate,0);
mLinearGradient.setLocalMatrix(mGradientMatrix);
postInvalidateDelayed(100);
}
}
(2). 通過組合來實現(xiàn)新的控件
這種方式可以用來創(chuàng)建復用性比較強的控件,通常會繼承一個合適的ViewGroup,再添加指定的功能,組合成復合控件,比如頂部標題欄自定義的TopBar
先定義屬性,在attrs.xml文件,添加自定義的屬性,文字、顏色、大小等
<declare-styleable name="TopBar">
<attr name="title" format="string"/>
<attr name="titleTextSize" format="dimension"
<attr name="leftBackground" format="reference|color"
……
再組合控件,然后定義和調(diào)用接口,布局中引用(注意要使用完整的包名),最后接口調(diào)用……
(3). 重寫View來實現(xiàn)全新控件
自定義View難于繪制控件和實現(xiàn)交互,通常需要繼承View類,重寫onDraw(), onMeasure()等方法實現(xiàn)繪制邏輯,重寫onTouchEvent()等觸控事件來實現(xiàn)交互邏輯……
自定義控件資料擴展:
TextView預渲染研究
Android自定義控件其實很簡單
AndroidTips-View
The end