在App市場(chǎng)中蔗牡,我們經(jīng)秤毕担可以看到許多的非常炫的頁(yè)面,他們?cè)O(shè)計(jì)精美注重細(xì)節(jié)蛋逾,用戶體驗(yàn)非常好集晚。而這種頁(yè)面的開(kāi)發(fā),通常Android自帶的原生控件是無(wú)法滿足的区匣,所以就需要我們根據(jù)不同的需求進(jìn)行自定義View偷拔。而根據(jù)設(shè)計(jì)和功能需求不同,我們通常有三種方法來(lái)實(shí)現(xiàn)自定義View:
- 組合:將不同控件組合在一起形成新的控件亏钩;
- 擴(kuò)展:在現(xiàn)有控件的基礎(chǔ)上莲绰,進(jìn)行擴(kuò)展;
- 重寫(xiě):現(xiàn)有控件無(wú)法滿足姑丑,通過(guò)重寫(xiě)來(lái)實(shí)現(xiàn)全新的控件蛤签;
本文以常見(jiàn)的自定義文本輸入框?yàn)槔樱窒韺?shí)現(xiàn)方式以及相關(guān)的自定義控件知識(shí)點(diǎn)栅哀。
需求 - 復(fù)雜自定義輸入框
整個(gè)控件包含三部分:標(biāo)題欄震肮,輸入框,提示信息欄留拾。要求該輸入框上面包含一個(gè)文本控件顯示標(biāo)題戳晌,下面包含一個(gè)文本控件顯示提示信息,合起來(lái)是一個(gè)完整的控件痴柔,并有多個(gè)新添加屬性沦偎,能夠?yàn)橛脩籼峁ML配置方式,也可以Java代碼配置。如圖所示輸入框豪嚎,能夠?qū)Σ煌瑺顟B(tài)有不同的顯示:
-
正常狀態(tài)下搔驼,灰色邊框,且為圓角矩形侈询;
-
得到焦點(diǎn)時(shí)舌涨,藍(lán)色邊框,且為圓角矩形妄荔;
-
校驗(yàn)輸入內(nèi)容泼菌,發(fā)現(xiàn)有錯(cuò)誤時(shí),紅色邊框啦租,圓角矩形,且有感嘆號(hào)提示圖標(biāo)用來(lái)提醒用戶荒揣;
分析
- 輸入框:該輸入框包含多個(gè)狀態(tài)篷角,但分析可知,類似Android原生的EditText控件系任,且該控件現(xiàn)有功能無(wú)法滿足多種狀態(tài)的要求恳蹲,因?yàn)椋梢?strong>擴(kuò)展EditText俩滥,在原生控件的基礎(chǔ)上進(jìn)行擴(kuò)展嘉蕾,增加功能,修改UI顯示效果霜旧;
- 整體:包含三部分:標(biāo)題 + 輸入框 + 提示信息错忱,即TextView + 擴(kuò)展EditText + TextView,且要作為一個(gè)整體提供給用戶使用挂据,姑且將此控件成為CustomInputView以清。即幾個(gè)基本控件組合在一起行成新的控件,這種方式通常需要繼承一個(gè)合適的ViewGroup崎逃,然后添加指定功能控件掷倔,形成新的控件,且可以指定可配置屬性个绍,增強(qiáng)可配置性勒葱;
一、自定義View實(shí)現(xiàn)構(gòu)造函數(shù)
(一) 實(shí)現(xiàn):繼承View并自定義輸入框的構(gòu)造函數(shù)
保證自定義view不管通過(guò)哪種方式創(chuàng)建都可以走到相應(yīng)的邏輯
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//do something
}
通過(guò)繼承View或者合適的布局(比如這里實(shí)現(xiàn)自定義輸入框巴柿,可以直接繼承EditText凛虽;或者考慮到包含三部分標(biāo)題+控件+提示信息,可以直接繼承線性布局LinearLayout)篮洁,并實(shí)現(xiàn)View的構(gòu)造函數(shù)涩维,之后就可以對(duì)其進(jìn)行改造,實(shí)現(xiàn)我們想要的自定義效果。但是其中有四個(gè)構(gòu)造函數(shù)瓦阐,他們分別什么意義呢蜗侈?我們這里又為什么只實(shí)現(xiàn)了三個(gè)呢?
(二) 原理:四個(gè)構(gòu)造函數(shù)
- 用Java代碼創(chuàng)建View睡蟋,如果只用這個(gè)構(gòu)造函數(shù)聲明踏幻,該View沒(méi)有任何參數(shù),基本是個(gè)空View對(duì)象戳杀;
public View(Context context)
- 從XML中創(chuàng)建View该面,且參數(shù)attr是在XML中配置的參數(shù);
public View(Context context, AttributeSet attrs)
- 從XML中創(chuàng)建View信卡,且有自定義屬性時(shí)調(diào)用隔缀。系統(tǒng)默認(rèn)只會(huì)調(diào)用前兩個(gè)構(gòu)造函數(shù),至于第三個(gè)構(gòu)造函數(shù)的調(diào)用,通常是在構(gòu)造函數(shù)中主動(dòng)調(diào)用的(例如,在第二個(gè)構(gòu)造函數(shù)中調(diào)用第三個(gè)構(gòu)造函數(shù));
public View(Context context, AttributeSet attrs, int defStyleAttr)
- 從XML中創(chuàng)建View傍菇,且有自定義屬性猾瘸,且需要在SDK21以上才能使用;
public View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
知道了不同的構(gòu)造函數(shù)的含義后丢习,那么我們自定義View時(shí)牵触,應(yīng)該重寫(xiě)哪個(gè)構(gòu)造函數(shù)呢?首先我們要區(qū)分不同構(gòu)造函數(shù)的調(diào)用時(shí)機(jī)咐低,一共四個(gè)構(gòu)造函數(shù)揽思,第一個(gè)是Java代碼創(chuàng)建時(shí)調(diào)用;后三個(gè)都是XML創(chuàng)建见擦,其中第二個(gè)比較好理解钉汗,即attr參數(shù)就是XML中配置的參數(shù);那么后兩個(gè)構(gòu)造函數(shù)又有什么區(qū)別呢锡宋?他們都是與主題相關(guān)儡湾,從而使得一些View即使不對(duì)其進(jìn)行任何配置,也有一些默認(rèn)屬性执俩,所以徐钠,在自定義View時(shí),如果不需要View隨著主題變化而變化役首,有前兩個(gè)構(gòu)造函數(shù)就夠了尝丐。
(三) 原理:View的屬性和主題
不同View的形態(tài)不同,是因?yàn)槠渑渲玫膶傩圆煌獍拢赩iew中有很多屬性爹袁,如color,background等矮固,這些屬性可以在不同位置進(jìn)行配置:(1)可以直接寫(xiě)在XML文件中失息;(2)可以在XML中以style形式定義譬淳;(3)theme主題中定義;(4)defStyleAttr盹兢;(5)defStyleRes邻梆;且他們的優(yōu)先級(jí)為:
XML直接定義 > XML中style引用 > defStyleAttr > defStyleRes > theme直接定義
- defStyleAttr:只要在主題中對(duì)這個(gè)屬性賦值,該View就會(huì)自動(dòng)應(yīng)用這個(gè)屬性的值绎秒。在給這個(gè)屬性賦值時(shí)浦妄,在xml中一般使用@style/xxx形式;
- defStyleRes:只有在第三個(gè)參數(shù)defStyleAttr為0见芹,或者主題中沒(méi)有找到這個(gè)defStyleAttr屬性的賦值時(shí)剂娄,才可以啟用。而且這個(gè)參數(shù)不再是Attr了玄呛,而是真正的style阅懦。其實(shí)這也是一種低級(jí)別的“默認(rèn)主題”,即在主題未聲明屬性值時(shí)把鉴,我們可以主動(dòng)的給一個(gè)style故黑,使用這個(gè)構(gòu)造函數(shù)定義出的View,其主題就是這個(gè)定義的defStyleRes庭砍。
具體關(guān)于優(yōu)先級(jí)驗(yàn)證的例子見(jiàn)這篇博客
二、自定義屬性
(一) 實(shí)現(xiàn)步驟1:編寫(xiě)styleable和item等標(biāo)簽元素
通過(guò)declare-styleable標(biāo)簽為其配置自定義屬性混埠,在res/values/attrs.xml文件中編寫(xiě)styleable和item等標(biāo)簽元素:
<resources>
<declare-styleable name="CustomView">
<attr name="custom_attr1" format="string" />
<attr name="custom_attr2" format="boolean" />
<attr name="custom_attr3" format="integer" />
<attr name="custom_attr4" format="dimension" />
</declare-styleable>
<attr name="custom_attr5" format="string" />
</resources>
聲明了一個(gè)自定義屬性集MyCustomView,其中包含了custom_attr1,custom_att2,custom_attr3,custom_attr4四個(gè)屬性.同時(shí),我們還聲明了一個(gè)獨(dú)立的屬性custom_attr5怠缸;
(二) 實(shí)現(xiàn)步驟2:在XML布局文件中使用
- 在根布局引用命名空間
xmlns:app="http://schemas.android.com/apk/res-auto"
- 在布局文件中使用自定義view
<com.example.myapplication.CustomView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:custom_attr1="test"
app:custom_attr2="true"
app:custom_attr3="1"
app:custom_attr4="1dp"
app:custom_attr5="base"/>
(三) 實(shí)現(xiàn)步驟3:在CustomView的構(gòu)造方法中通過(guò)TypedArray獲取
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
String testStr = ta.getString(R.styleable.CustomView_custom_attr1);
boolean testBool = ta.getBoolean(R.styleable.CustomView_custom_attr2, false);
ta.recycle();
通過(guò)以上四個(gè)步驟,我們就為自定義view定義了自定義屬性钳宪,且可以通過(guò)XML進(jìn)行配置揭北,并讀取到配置的屬性值,并對(duì)其進(jìn)行操作吏颖。下面是其中的一些原理:
(四) 原理:AttributeSet與TypedArray
- AttributeSet:包含該View聲明的所有的屬性的集合搔体。可以通過(guò)getAttributeName()方法獲取所有屬性的key半醉,getAttributeValue()方法獲取所有屬性的value疚俱;例如:
<com.example.myapplication.CustomView
android:layout_width="100dp"
android:layout_height="100dp"
app:custom_attr1="test"/>
解析出來(lái)的key和value值為:
attrName = layout_width , attrVal = 100.0dip
attrName = layout_height , attrVal = 200.0dip
attrName = text , attrVal = test
- TypedArray:簡(jiǎn)化解析屬性的工作。如果布局中的屬性的值是引用類型(比如:@dimen/dp100)缩多,AttributeSet解析出來(lái)的結(jié)果是
@數(shù)字
的字符串呆奕,即id。如果使用AttributeSet去獲得最終的字符串衬吆,那么需要第一步拿到id梁钾,第二步再去解析id。而TypedArray正是幫我們簡(jiǎn)化了這個(gè)過(guò)程逊抡。例如:
<com.example.myapplication.CustomView
android:layout_width="@dimen/dp100"
android:layout_height="100dp"
app:custom_attr1="@string/test"/>
解析出來(lái)的key和value值為:
attrName = layout_width , attrVal = @2130065234
attrName = layout_height , attrVal = 100.0dip
attrName = text , attrVal = @2131211809
如果用AttributeSet解析像素值姆泻,代碼為:
int widthDimenId = attrs.getAttributeResourceValue(0, -1);
int width = getResources().getDimension(widthDimenId);
結(jié)論:在View的構(gòu)造方法中,可以通過(guò)AttributeSet去獲得自定義屬性的值,但是比較麻煩拇勃,而TypedArray可以很方便的獲取四苇。
(五) 原理:declare-styleable
- styleale的出現(xiàn)系統(tǒng)可以為我們完成很多常量(int[]數(shù)組,下標(biāo)常量)等的編寫(xiě)潜秋,簡(jiǎn)化開(kāi)發(fā)工作蛔琅;
- attr中的屬性不可以重復(fù)定義,可以一次定義峻呛,多次使用罗售。可以聲明一個(gè)parent钩述,父類style寨躁,其他style繼承該父類使用,其中定義和使用的區(qū)別:
(1)定義:<attr name="testAttr" format="integer" />
(2)使用:<attr name="testAttr"/>
結(jié)論:Android會(huì)根據(jù)其在R.java中生成一些常量方便我們使用(aapt干的)牙勘,本質(zhì)上职恳,可以不聲明declare-styleable,僅僅聲明所需的屬性即可方面,但是比較麻煩放钦,而declare-styleable可以使我們方便的獲取。
具體關(guān)于自定義屬性驗(yàn)證的例子見(jiàn)這篇博客
三恭金、設(shè)置不同樣式對(duì)應(yīng)不同狀態(tài)
(一) 實(shí)現(xiàn):一個(gè)文件實(shí)現(xiàn)不同狀態(tài)的樣式
- 第一種方式:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!--可編輯狀態(tài)操禀,失焦時(shí):灰色-->
<item android:state_enabled="true" android:state_focused="false">
<shape android:shape="rectangle">
<stroke android:width="@dimen/dp1" android:color="@color/grey">
</shape>
</item>
<!--可編輯狀態(tài),且獲得焦點(diǎn)時(shí):藍(lán)色-->
<item android:state_enabled="true" android:state_focused="true">
<shape android:shape="rectangle">
<stroke android:width="@dimen/dp1" android:color="@color/blue">
</shape>
</item>
</selector>
- 第二種方式:
或者也可以將其中不同狀態(tài)對(duì)應(yīng)的item抽成一個(gè)文件横腿,以防如果其他控件使用可以直接調(diào)用颓屑,代碼如下:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!--可編輯狀態(tài),失焦時(shí):灰色-->
<item android:drawable="@drawable/custom_drawable1" android:state_enabled="true" android:state_focused="false" />
<!--可編輯狀態(tài)耿焊,且獲得焦點(diǎn)時(shí):藍(lán)色-->
<item android:drawable="@drawable/custom_drawable2" android:state_enabled="true" android:state_focused="true" />
</selector>
其中揪惦,custom_drawable1.xml的代碼為:(custom_drawable2類似)
<shape android:shape="rectangle">
<stroke android:width="@dimen/dp1" android:color="@color/grey">
</shape>
(二) 原理:selector選擇器
定義資源文件xml時(shí),使用selector標(biāo)簽罗侯,可以添加一個(gè)或多個(gè)item子標(biāo)簽器腋,而相應(yīng)的狀態(tài)是在item標(biāo)簽中定義的。定義的xml文件可以作為兩種資源使用:drawable和color:
- 作為drawable資源使用時(shí)歇父,一般和shape一樣放于drawable目錄下蒂培,item必須指定android:drawable屬性;使用的例子見(jiàn)上面代碼(
(一) 實(shí)現(xiàn):一個(gè)文件實(shí)現(xiàn)不同狀態(tài)的樣式
) - 作為color資源使用時(shí)榜苫,則放于color目錄下护戳,item必須指定android:color屬性;使用例子見(jiàn)下面:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 當(dāng)前窗口失去焦點(diǎn)時(shí) -->
<item android:color="@android:color/black" android:state_window_focused="false" />
<!-- 不可用時(shí) -->
<item android:color="@android:color/background_light" android:state_enabled="false" />
<!-- 按壓時(shí) -->
<item android:color="@android:color/holo_blue_light" android:state_pressed="true" />
<!-- 被選中時(shí) -->
<item android:color="@android:color/holo_green_dark" android:state_selected="true" />
<!-- 被激活時(shí) -->
<item android:color="@android:color/holo_green_light" android:state_activated="true" />
<!-- 默認(rèn)時(shí) -->
<item android:color="@android:color/white" />
</selector>
其中垂睬,注意:
- android:drawable屬性除了引用@drawable資源媳荒,也可以引用@color顏色值抗悍;但android:color只能引用@color;
- item是從上往下匹配的钳枕,如果匹配到一個(gè)item那它就將采用這個(gè)item缴渊,而不是采用最佳匹配的規(guī)則;所以設(shè)置默認(rèn)的狀態(tài)鱼炒,一定要寫(xiě)在最后衔沼,如果寫(xiě)在前面,則后面所有的item都不會(huì)起作用昔瞧;
總結(jié)
根據(jù)以上介紹指蚁,可以簡(jiǎn)單寫(xiě)出一個(gè)標(biāo)題+輸入框+提示信息的布局了,且可以自定義屬性值自晰,主要代碼如下:
public class CustomView extends LinearLayout {
private TextView title;
private TextView description;
private EditText input;
//custom property
private String customAttr1;
private Boolean customAttr2;
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initViews(context);
initProperties(context, attrs);
}
private void initViews(Context context) {
setOrientation(VERTICAL);
title = new TextView(context);
addView(title);
input = new EditText(context);
input.setBackgroundResource(R.drawable.custom_input_selector);
input.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if(somethingWrong()) {
input.setBackgroundResource(R.drawable.custom_input_error);
} else {
input.setBackgroundResource(R.drawable.custom_input_selector);
}
//或者可以使用三目運(yùn)算符
//input.setBackgroundResource(somethingWrong()? R.drawable.custom_input_error : R.drawable.custom_input_selector);
}
});
addView(input);
description = new EditText(context);
addView(description);
}
private void initProperties(Context context, @Nullable AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
setCustomAttr1(ta.getString(R.styleable.CustomView_custom_attr1));
setCustomAttr2(ta.getBoolean(R.styleable.CustomView_custom_attr2, false));
ta.recycle();
}
public void setCustomAttr1(String attr) {
customAttr1 = attr;
}
public void setCustomAttr2(boolean attr) {
customAttr2 = attr;
}
public String getCustomAttr1() {
return customAttr1;
}
public Boolean getCustomAttr2() {
return customAttr2;
}
}
實(shí)用的常用Tips
- 給ImageView設(shè)置水波紋效果:
android:background="?android:attr/selectableItemBackground"
- 可以利用ContextThemeWrapper引入style來(lái)修改控件樣式凝化,能夠方便的將自定義樣式寫(xiě)入style,減少代碼酬荞,如:
ContextThemeWrapper wrapper = new ContextThemeWrapper(context, R.style.CustomStyle);
CustomView customView = new CustomView(wrapper);
但要注意搓劫,慎用這種方式,ContextThemeWrapper會(huì)改變當(dāng)前theme混巧,并改變此后再使用的context枪向,有可能會(huì)影響較大。
- 設(shè)置當(dāng)前自定義控件的寬度和高度
customView.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.WRAP_CONTENT);
//或者
customView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.WRAP_CONTENT));
- 調(diào)用UI線程延遲執(zhí)行
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//doSomething
}
},100); //延遲100毫秒執(zhí)行
參考文獻(xiàn)
Android View 四個(gè)構(gòu)造函數(shù)詳解
Android 深入理解Android中的自定義屬性