眾所周知,自定義組件的第一步是繼承android.view.View類琳猫,然后重寫其中一些方法來實(shí)現(xiàn)自定義功能伟叛。在官網(wǎng)文檔說明自定義view的部分列出了一些方法。我理解是一些比較常用的需要復(fù)寫的方法脐嫂,因此本篇就來詳解下這些方法统刮,包括構(gòu)造器、onDraw雹锣、onMeasure网沾、事件響應(yīng)等。View類中的方法和域是非常多的蕊爵,其他的以后找機(jī)會(huì)再詳解辉哥。
View的構(gòu)造器
View (Context context)
Simple constructor to use when creating a view from code.
也就是說,這個(gè)構(gòu)造函數(shù)可以在代碼新建view的時(shí)候使用攒射,而無法在xml中使用醋旦。下面來自定義一個(gè)只有此構(gòu)造函數(shù)的view,然后在復(fù)寫onDraw(之后細(xì)講)來寫行字:
class MyView extends View {
public MyView(Context context) {
super(context);
}
@Override
public void onDraw(Canvas c){
Paint p = new Paint();
p.setColor(Color.BLUE);
p.setTextSize(50);
c.drawText("自定義view",50,50,p);
}
}
之后在代碼中生成一個(gè)MyView并加入當(dāng)前界面:
ViewGroup.LayoutParams p = findViewById(R.id.container).getLayoutParams();
p.height = ViewGroup.LayoutParams.WRAP_CONTENT;
p.width = ViewGroup.LayoutParams.MATCH_PARENT;
this.addContentView(new MyView(this),p);
效果如下:
而如果將onDraw中的內(nèi)容去掉会放,界面會(huì)變?yōu)榭瞻姿瞧搿6偃缭趚ml中使用這個(gè)自定義view,在運(yùn)行時(shí)會(huì)報(bào):
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.tgtpp.themandsytle/com.tgtpp.themandsytle.MainActivity}: android.view.InflateException: Binary XML file line #21: Binary XML file line #21: Error inflating class com.tgtpp.themandsytle.MainActivity.MyView
因此僅有這個(gè)構(gòu)造函數(shù)咧最,那么這個(gè)View默認(rèn)沒有任何樣式捂人,只能通過編寫代碼來令其展示內(nèi)容與樣式,且只能在代碼中調(diào)用矢沿。
View (Context context, AttributeSet attrs)
Constructor that is called when inflating a view from XML. This is called when a view is being constructed from an XML file, supplying attributes that were specified in the XML file. This version uses a default style of 0, so the only attribute values applied are those in the Context's Theme and the given AttributeSet.
使用這個(gè)構(gòu)造函數(shù)后滥搭,便可在xml中使用這個(gè)自定義view。為MyView增加構(gòu)造函數(shù):
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attributeSet){
super(context, attributeSet);
}
@Override
public void onDraw(Canvas c){
super.onDraw(c);
Paint p = new Paint();
p.setColor(Color.BLUE);
p.setTextSize(50);
c.drawText("自定義view",50,50,p);
}
}
然后在xml中引用此類:
<com.tgtpp.themandsytle.MyView
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
此時(shí)已經(jīng)可以在android studio的xml的design界面看到這個(gè)view的預(yù)覽了捣鲸,令自定義view可以用于xml的好處就是可以方便的設(shè)置屬性瑟匆。在xml中為此View添加background:
<com.tgtpp.themandsytle.MyView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#00ffff"
/>
效果如下:
可見成功添加了背景色,但是也可以注意到栽惶,雖然layout_height設(shè)置為wrap_content愁溜,但是高度并非是想象中包圍文字的疾嗅。想達(dá)到此效果,還應(yīng)更詳細(xì)地重寫其他函數(shù)冕象。而至于新增的AttributeSet參數(shù)代承,看了下相關(guān)的使用還是比較復(fù)雜的,應(yīng)該是為了可以將xml中設(shè)置的屬性傳遞給View交惯,一般來似乎不需要處理次泽。因此之后有時(shí)間再細(xì)看這個(gè)。
View (Context context, AttributeSet attrs, int defStyleAttr)
Perform inflation from XML and apply a class-specific base style from a theme attribute. This constructor of View allows subclasses to use their own base style when they are inflating. For example, a Button class's constructor would call this version of the super class constructor and supply R.attr.buttonStyle for defStyleAttr; this allows the theme's button style to modify all of the base view attributes (in particular its background) as well as the Button class's attributes.
既然官方文檔里舉了Button的例子席爽,那么就來看下Button的源碼:
@RemoteView
public class Button extends TextView {
public Button(Context context) {
this(context, null);
}
public Button(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.buttonStyle);
}
public Button(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public Button(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public CharSequence getAccessibilityClassName() {
return Button.class.getName();
}
}
可以看到是在第二個(gè)構(gòu)造函數(shù)中,向父類傳遞了com.android.internal.R.attr.buttonStyle作為第三個(gè)參數(shù)啊片。如果看過我上一篇博文的只锻,可能會(huì)對(duì)buttonStyle有些印象:在應(yīng)用的theme中重新設(shè)定buttonStyle,可以使得全局button的樣式改變紫谷。參考Button實(shí)現(xiàn)方式齐饮,將com.android.internal.R.attr.buttonStyle傳入自定義View:
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attributeSet){
super(context, attributeSet, com.android.internal.R.attr.buttonStyle);
}
@Override
public void onDraw(Canvas c){
super.onDraw(c);
}
}
然而卻報(bào)了錯(cuò):Error:(22, 60) 錯(cuò)誤: 程序包c(diǎn)om.android.internal.R不存在
,網(wǎng)上查詢說這是個(gè)隱藏的類笤昨,看來不能直接使用祖驱。由于buttonStyle是一個(gè)屬性,因此我們也自定義一個(gè)屬性:
<declare-styleable name="MyView">
<attr name="defaultStyleAttr" format="reference" />
</declare-styleable>
這里順便插一句瞒窒,我這里declare-styleable的name寫的是我自定義View的名稱捺僻,android studio提示和官網(wǎng)相關(guān)教程都是寫的對(duì)應(yīng)自定義類的名稱。事實(shí)上不對(duì)應(yīng)view的名稱也是可以正常編譯崇裁,在本篇case中也能正常運(yùn)行匕坯。其影響就是,在xml中使用自定義類時(shí)拔稳,無法在其中使用自定義的這個(gè)屬性葛峻。反之,和view對(duì)應(yīng)起來巴比,就可以通過app:屬性名引用到:
<com.tgtpp.themandsytle.MyView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:defaultStyleAttr=""
/>
回到本次主題术奖,屬性定義好,在我們的類中使用:
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attributeSet){
super(context, attributeSet, R.attr.defaultStyleAttr);
}
@Override
public void onDraw(Canvas c){
super.onDraw(c);
}
}
目前屬性定義了轻绞,還需要找個(gè)地方賦值:
- 官方說明提到了theme采记,那么就先嘗試在應(yīng)用主題中賦值,這里暫時(shí)借用button的默認(rèn)樣式Widget.Button:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="defaultStyleAttr">@android:style/Widget.Button</item>
</style>
在xml中使用自定義界面并設(shè)置合適的長(zhǎng)寬:
<com.tgtpp.themandsytle.MyView
android:layout_width="50dp"
android:layout_height="50dp"
/>
界面效果如下铲球,成功展示了在主題中設(shè)置的默認(rèn)樣式:
- 不在theme中設(shè)置挺庞,而是在使用view的時(shí)候設(shè)置此屬性:
<com.tgtpp.themandsytle.MyView
android:layout_width="50dp"
android:layout_height="50dp"
app:defaultStyleAttr="@android:style/Widget.Button"
/>
然而卻報(bào)錯(cuò):Failed to find style 'defaultStyleAttr' in current theme
〖诓。看來必須要在主題中設(shè)置這個(gè)屬性才可以选侨。
總結(jié)下:View的第三個(gè)構(gòu)造函數(shù)的第三個(gè)參數(shù)掖鱼,接收一個(gè)在當(dāng)前主題中指定的樣式,以此方式令所有View的實(shí)例具有相同的默認(rèn)樣式援制。若想自定義戏挡,需在自定義屬性后,在當(dāng)前主題中為此自定義屬性賦值晨仑,然后將此屬性的ID傳入View的構(gòu)造函數(shù)的第三個(gè)參數(shù)褐墅。這樣自定義View便可如Button等具有默認(rèn)樣式。
最后嘗試下如果只有第三個(gè)構(gòu)造函數(shù)會(huì)怎樣洪己。結(jié)果報(bào)錯(cuò):
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.tgtpp.themandsytle/com.tgtpp.themandsytle.MainActivity}: android.view.InflateException: Binary XML file line #22: Binary XML file line #22: Error inflating class com.tgtpp.themandsytle.MyView
看來希望可以正確inflate自定義View必須要有前兩個(gè)構(gòu)造函數(shù)妥凳,第三個(gè)構(gòu)造函數(shù)是類似Button那樣使用的。
View (Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
Perform inflation from XML and apply a class-specific base style from a theme attribute or style resource. This constructor of View allows subclasses to use their own base style when they are inflating.
也就是直接傳入一個(gè)style的id答捕,使得自定義View使用這個(gè)樣式逝钥。這里隨便找一個(gè)樣式R.style.Widget_AppCompat_Button傳入,注意到這個(gè)構(gòu)造函數(shù)只支持api 21以上:
public class MyView extends View {
public MyView(Context context) {
super(context);
}
@TargetApi(21)
public MyView(Context context, AttributeSet attributeSet){
super(context, attributeSet, 0, R.style.Widget_AppCompat_Button);
}
@Override
public void onDraw(Canvas c){
super.onDraw(c);
}
}
然后在xml中使用MyView:
<com.tgtpp.themandsytle.MyView
android:layout_width="50dp"
android:layout_height="50dp"
/>
效果如下:
優(yōu)先級(jí)
至此我們可以看到拱镐,View的構(gòu)造函數(shù)的后三個(gè)參數(shù)都可以決定此view的樣式艘款,但是它們之間是有優(yōu)先級(jí)的,如官網(wǎng)所說:
When determining the final value of a particular attribute, there are four inputs that come into play:
1.Any attribute values in the given AttributeSet.
2.The style resource specified in the AttributeSet (named "style").
3.The default style specified by defStyleAttr.
4.The default style specified by defStyleRes.
5.The base values in this theme.
Each of these inputs is considered in-order, with the first listed taking precedence over the following ones. In other words, if in the AttributeSet you have supplied <Button * textColor="#ff000000"> , then the button's text will always be black, regardless of what is specified in any of the styles.
思考
那么如果利用這四個(gè)構(gòu)造函數(shù)優(yōu)雅地自定義view呢沃琅?我想可以這樣:先自定義一個(gè)默認(rèn)樣式哗咆,將id傳遞個(gè)第四個(gè)defStyleRes參數(shù),這樣view就有了默認(rèn)樣式益眉。然后自定義attr晌柬,并將此id傳遞給第三個(gè)參數(shù)defStyleAttr,這樣如果其他人使用這個(gè)自定義view呜叫,需要根據(jù)主題更改樣式空繁,就可以直接在theme中重定義此attr即可。使用第四個(gè)參數(shù)的好處是在一些簡(jiǎn)單的應(yīng)用下朱庆,不需再編寫theme盛泡。可惜如果需要支持低版本api娱颊,是無法這樣做的傲诵。
Drawing
onDraw(android.graphics.Canvas)
這個(gè)接口提供了一個(gè)Canvas,也就是一個(gè)畫布箱硕,可以在其上繪制想要繪制的內(nèi)容拴竹。為了不發(fā)散太多,就不細(xì)講這個(gè)類了剧罩。上文有一個(gè)寫字的例子可以參考下∷ò荩現(xiàn)在來看下這邊繪制的內(nèi)容和View及樣式的關(guān)系。依舊使用R.style.Widget_AppCompat_Button作為默認(rèn)樣式,現(xiàn)在在onDraw中隨意畫個(gè)圖片:
@Override
public void onDraw(Canvas c){
super.onDraw(c);
Bitmap b = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher);
c.drawBitmap(b,0,0,null);
}
在xml里調(diào)大MyView幕与,并設(shè)置下margin:
<com.tgtpp.themandsytle.MyView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_margin="30dp"
/>
效果如下:
可以看到挑势,設(shè)置的樣式還在,圖片也成功地繪制在了其上啦鸣,因此二者應(yīng)該是互不影響潮饱,系統(tǒng)先繪制樣式,之后在其上再繪制onDraw內(nèi)容诫给。而繪制的坐標(biāo)是基于MyView的香拉。在xml中縮小MyView的長(zhǎng)寬到30dp,效果如下:
注意到繪制的圖片并沒有隨之縮小中狂,因此onDraw中的繪制不會(huì)像設(shè)置的style那樣自動(dòng)適配view凫碌,還需手動(dòng)進(jìn)行相關(guān)設(shè)置。
Layout
void onSizeChanged (int w, int h, int oldw, int oldh)
當(dāng)?shù)谝淮未_定當(dāng)前view的大小或者大小改變時(shí)吃型,會(huì)調(diào)用到此函數(shù)证鸥。w、h表示當(dāng)前長(zhǎng)寬勤晚,oldw、oldh表示之前長(zhǎng)寬泉褐,第一次確定大小時(shí)這兩個(gè)值為0赐写。如果只是想簡(jiǎn)單控制view的繪制內(nèi)容,用這個(gè)即可膜赃。這邊幾個(gè)參數(shù)的單位都是像素挺邀。
void onLayout (boolean changed, int left, int top, int right, int bottom)
當(dāng)?shù)谝淮未_定當(dāng)前view的大小或者大小改變時(shí),會(huì)調(diào)用到此函數(shù)跳座。與onSizeChanged不同的是端铛,這個(gè)函數(shù)提供的left\top\right\bottom分別是相對(duì)于父view的左上和右下的坐標(biāo)。比如前文MyView長(zhǎng)寬為30dp疲眷,magin為30dp禾蚕,在dpi為480的情況下,傳入的left\top\right\bottom分別是90\90\180\180狂丝,單位為像素换淆。
void onMeasure (int widthMeasureSpec, int heightMeasureSpec)
想要更精確地控制,則可使用此函數(shù)几颜。兩個(gè)參數(shù)是“packed”倍试,即一個(gè)整數(shù)表示很多值,需要解析才能獲得有意義的值蛋哭。
View.MeasureSpec
使用此類來解析onMeasure中的兩個(gè)參數(shù)县习。可以解析出兩個(gè)內(nèi)容:size和mode。size就是長(zhǎng)寬的值躁愿,mode描述了view的parent對(duì)其的限制叛本,包括AT_MOST\EXACTLY\UNSPECIFIED(在某個(gè)范圍內(nèi)想多大多大\指定了具體值\沒有任何限制)。
針對(duì)前文MyView長(zhǎng)寬為30dp的情況攘已,調(diào)用onMeasure并解析炮赦,方式就是將onMeasure中獲得的參數(shù)傳入View.MeasureSpec的getSize、getMode等函數(shù)中:
@Override
public void onMeasure (int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
Log.i("TEST", "width:size,mode,string:"+View.MeasureSpec.getSize(widthMeasureSpec)+","+View.MeasureSpec.getMode(widthMeasureSpec)+","+View.MeasureSpec.toString(widthMeasureSpec));
Log.i("TEST", "height:size,mode,string:"+View.MeasureSpec.getSize(heightMeasureSpec)+","+View.MeasureSpec.getMode(heightMeasureSpec)+","+View.MeasureSpec.toString(heightMeasureSpec));
}
結(jié)果如下:
Event processing
這邊事件處理其實(shí)包括了對(duì)于多種硬件設(shè)備的處理样勃。對(duì)于目前占絕大多數(shù)的觸屏智能機(jī)吠勘,處理點(diǎn)擊事件使用:
boolean onTouchEvent (MotionEvent event)
而對(duì)于那種老式有硬件鍵盤的設(shè)備,提供了對(duì)按鍵的響應(yīng)峡眶,并且文檔中特別說明剧防,此響應(yīng)并不一定對(duì)軟鍵盤起作用,不要用它們處理軟鍵盤的點(diǎn)擊事件:
boolean onKeyDown (int keyCode, KeyEvent event)
boolean onKeyUp (int keyCode, KeyEvent event)
這里還有對(duì)“trackball”事件的處理辫樱,也就是那種滾輪樣設(shè)備:
boolean onTrackballEvent (MotionEvent event)
Focus
void onFocusChanged (boolean gainFocus, int direction, Rect previouslyFocusedRect)
focus就是類似那種輸入框出現(xiàn)的時(shí)候會(huì)直接有輸入提示在那邊峭拘,這就是獲得了焦點(diǎn)。在onCreate中使用requestFocus令MyView獲得焦點(diǎn)狮暑,然后打印onFocusChanaged的三個(gè)參數(shù):
@Override
public void onFocusChanged (boolean gainFocus,
int direction,
Rect previouslyFocusedRect){
super.onFocusChanged(gainFocus,direction,previouslyFocusedRect);
Log.i("TEST", "gainFocurs,direction,rect:"+gainFocus+","+direction+","+previouslyFocusedRect);
}
然而奇怪的是鸡挠,打印結(jié)果總是gainFocus先是true,然后馬上變?yōu)閒alse搬男,而previouslyFocusedRect一直為null:
06-23 18:36:21.482 30740-30740/com.tgtpp.themandsytle I/TEST: gainFocurs,direction,rect:true,130,null
06-23 18:36:21.487 30740-30740/com.tgtpp.themandsytle I/TEST: gainFocurs,direction,rect:false,0,null
嘗試在xml中為MyView添加android:focusable="true"
拣展,沒有效果;添加android:focusableInTouchMode="true"
后缔逛,focus不再變?yōu)閒alse备埃。應(yīng)該是因?yàn)樵趖ouch模式下不能requestFocus,所以會(huì)自動(dòng)置focus為false褐奴。而previouslyFocusedRect多次嘗試按脚,一直為null,這個(gè)問題之后有空再研究敦冬。
void onWindowFocusChanged (boolean hasWindowFocus)
經(jīng)試驗(yàn)辅搬,在應(yīng)用被完全展現(xiàn)出來的時(shí)候會(huì)被調(diào)用且hasWindowFocus為true;縮小或消失會(huì)被調(diào)用且hasWindowFocus為false匪补。
Attaching
當(dāng)view和window相連之前和之后調(diào)用如下方法伞辛,一般說明view已經(jīng)擁有或失去了繪制的空間:
void onAttachedToWindow ()
void onDetachedFromWindow ()
當(dāng)windows可見性變化時(shí)調(diào)用如下方法:
void onDetachedFromWindow ()