一碗降、前言
標(biāo)題起得屌了點(diǎn)隘竭,文章只能給大家?guī)?lái)理論知識(shí),能不能上天還是得各位親自實(shí)踐讼渊。文中涉及到很多自己的理解动看,能力有限,有問(wèn)題的地方還請(qǐng)指正爪幻,文中的參考資料都是精心篩選的菱皆,非常值得閱讀。
很多人把自定義View想得復(fù)雜了挨稿,以為有多高深仇轻,主要還是沒(méi)有實(shí)踐過(guò),沒(méi)有足夠的自信奶甘;但也有很多人把自定義View想得簡(jiǎn)單了篷店,以為摸清View的幾個(gè)關(guān)鍵回調(diào)、知道自定義屬性和Android的消息分發(fā)機(jī)制就算是老司機(jī)了臭家,其實(shí)對(duì)于自定義View來(lái)講疲陕,設(shè)計(jì)方淤、排版、效率都是很費(fèi)腦筋的蹄殃,我在github上到現(xiàn)在都沒(méi)發(fā)現(xiàn)一個(gè)像樣的圖文混排自定義View携茂。
常見(jiàn)的Android自定義View主要有兩種類型:
組合控件:通過(guò)Android的基礎(chǔ)控件(TextView、CheckBox窃爷、Button邑蒋、ProgressBar等)組合而成,比如試題控件(TextView+VideoGroup)按厘、下拉刷新医吊、瀑布流控件、帶左/右滑功能的控件逮京、視頻控件等卿堂,這種自定義View的難點(diǎn)在于程序的邏輯處理;
完全自定義控件:繼承自View懒棉、TextureView或SurfaceView草描,然后重寫核心的回調(diào)方法,以View為例策严,按需復(fù)寫其構(gòu)造穗慕、onMeasure、onLayout妻导、onTouchEvent逛绵、onDraw、onAttachedToWindow倔韭、onDetachedFromWindow等方法术浪,這種自定義View的難點(diǎn)在于程序的設(shè)計(jì)、效率優(yōu)化和排版寿酌,比如輸入法中的手寫控件胰苏、圖文混排控件(現(xiàn)在很多都是通過(guò)webview加載網(wǎng)頁(yè)實(shí)現(xiàn)了)、詞典取詞控件醇疼、圖表控件硕并、個(gè)性化進(jìn)度條、彈幕顯示控件秧荆、Markdown控件鲤孵、IDE代碼編輯控件等。
按照上面這種方式分只是便于理解辰如,很多時(shí)候有些控件既有組合普监,又需要復(fù)寫所繼承類的回調(diào)方法。
二、自定義View的價(jià)值
能夠做到基礎(chǔ)控件無(wú)法做到的效果凯正,為應(yīng)用的表現(xiàn)增色毙玻;
在多個(gè)應(yīng)用并行開(kāi)發(fā)的團(tuán)隊(duì),將公用的交互效果提取成自定義控件廊散,方便復(fù)用桑滩,減少不必要的重復(fù)勞動(dòng);
將控件的內(nèi)部邏輯封裝在自定義View中允睹,便于應(yīng)用內(nèi)解耦运准;
三、有必要了解的核心知識(shí)點(diǎn)
View缭受、SurfaceView胁澳、TextureView的區(qū)別
View:普通的View,與宿主窗口共享同一個(gè)繪圖表面米者,UI在主線程中繪制韭畸,在有無(wú)硬件加速的情況下都能工作(沒(méi)有硬件加速的情況下,canvas的有些方法會(huì)失效)蔓搞;
SurfaceView:繼承自View胰丁,繪制和顯示效率高,因?yàn)閾碛歇?dú)立的繪圖表面喂分,UI在一個(gè)獨(dú)立的線程中進(jìn)行繪制锦庸,不會(huì)占用主線程的資源。SurfaceView的使用和普通的View不一樣蒲祈,需要結(jié)合SurfaceHodler一起使用甘萧。因?yàn)楹退拗鞔翱诓皇枪蚕硗粋€(gè)繪圖表面的原因,筆者在實(shí)際使用SurfaceView的過(guò)程中發(fā)現(xiàn)對(duì)其做動(dòng)畫操作會(huì)達(dá)不到想要的效果(一坨黑色)讳嘱;
TextureView:繼承自View,與SurfaceView相比酿愧,TextureView不會(huì)創(chuàng)建一個(gè)單獨(dú)的繪圖表面沥潭,這使得它可以像一般的View一樣執(zhí)行一些變換操作,比如移動(dòng)嬉挡、動(dòng)畫等等钝鸽,但TextureView必須在硬件加速開(kāi)啟的窗口中才能正常工作;
View的三大核心方法onMeasure庞钢、onLayout拔恰、onDraw
onMeasure:用于測(cè)量視圖的大小基括;
onLayout:用于給視圖進(jìn)行布局颜懊;
onDraw:用于對(duì)視圖進(jìn)行繪制;
自定義屬性
對(duì)于自定義View的一些屬性設(shè)置,除了可以在自定義View中提供公開(kāi)接口外河爹,還可以通過(guò)自定義屬性匠璧,在對(duì)自定義View布局時(shí)就指定,這樣可以簡(jiǎn)化用戶使用控件的復(fù)雜度咸这,實(shí)現(xiàn)自定義屬性的步驟如下:
-
在自定義View工程的res/values文件夾下新建一個(gè)attrs.xml的文件夷恍,在里面定義自定義屬性的ID、屬性和屬性對(duì)應(yīng)的類型媳维,eg:
<declare-styleable name="DictView" >
<attr name="textSize" format="dimension" />
<attr name="textColor" format="reference|color" /><attr name="typeface"> <enum name="normal" value="0" /> <enum name="sans" value="1" /> <enum name="serif" value="2" /> <enum name="monospace" value="3" /> </attr> <attr name="width" format="dimension" /> <attr name="height" format="dimension" />
</declare-styleable>
-
在自定義View帶attrs參數(shù)的構(gòu)造方法中解析自定義屬性值:
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DictView, defStyle, 0);
int n = a.getIndexCount();
for(int i = 0; i < n; i++)
{
int attr = a.getIndex(i);
if(attr == com.test.dict.R.styleable.DictView_textSize){
textSize = a.getDimensionPixelSize(attr, textSize);
}else if(attr == com.test.dict.R.styleable.DictView_textColor){
textColor = a.getColorStateList(attr);
}else if(attr == com.test.dict.R.styleable.DictView_typeface){
typefaceIndex = a.getInt(attr, typefaceIndex);
}else if(attr == com.test.dict.R.styleable.DictView_width){
setWidth(a.getDimensionPixelSize(attr, mWidth));
}else if(attr == com.test.dict.R.styleable.DictView_height){
setHeight(a.getDimensionPixelSize(attr, mHeight));
}
}
a.recycle();
對(duì)自定義屬性的解析需要注意兩點(diǎn):
1.TypedArray使用完成后一定要調(diào)用其recycle方法酿雪,否則會(huì)有內(nèi)存泄露的問(wèn)題;
2.如果自定義View在一個(gè)單獨(dú)的module中(不屬于主工程)侄刽,對(duì)attr的獲取不能使用switch-case語(yǔ)句指黎,要用if...else,具體原因之前有介紹過(guò)唠梨,詳見(jiàn):在Android library中不能使用switch-case語(yǔ)句訪問(wèn)資源ID的原因分析及解決方案
完成自定義屬性的定義后袋励,就可以在布局自定義View的過(guò)程中使用自定義屬性了,具體步驟如下:
-
在xml布局文件的根標(biāo)簽或者需要使用自定義屬性的標(biāo)簽中指定自定義屬性的命名空間当叭,其中這里的dictview就是命名空間茬故,是可以隨意指定的:
xmlns:dictview="http://schemas.android.com/apk/res-auto"
-
在自定義View的布局中使用自定義屬性,所有自定義屬性的設(shè)置都是在指定的命名空間下的蚁鳖,因?yàn)槭亲远x磺芭,所以不能用android這個(gè)命名空間:
<com.test.dictview.DictView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="@string/hello_world" dictview:textColor="@android:color/white" dictview:typeface="sans" />
雙緩沖
在移動(dòng)設(shè)備中很容易出現(xiàn)效率問(wèn)題,對(duì)于效率問(wèn)題的處理醉箕,主要方法是時(shí)間換空間或者空間換時(shí)間钾腺;自定義View可能存在顯示的效率問(wèn)題,可以通過(guò)雙緩沖來(lái)解決這個(gè)問(wèn)題讥裤,雙緩沖就是用空間換時(shí)間的典型例子放棒,同一個(gè)View在內(nèi)存中創(chuàng)建了兩份同樣大小的內(nèi)存,一份用于繪制己英,一份用于顯示间螟,繪制是繪制在Bitmap上,顯示就是將這張bitmap顯示在畫布上损肛。
硬件加速
在Android設(shè)備中厢破,硬件加速默認(rèn)是開(kāi)啟的,有些應(yīng)用出于內(nèi)存占用(開(kāi)啟硬件加速會(huì)占用更多的內(nèi)存)和應(yīng)用特征的考慮(沒(méi)什么動(dòng)畫治拿,基本沒(méi)有涉及到需要GPU的操作)摩泪,會(huì)在AndroidManifest.xml中關(guān)掉硬件加速,這會(huì)導(dǎo)致自定義View時(shí)劫谅,canvas的某些方法不能正常使用见坑,為了讓自定義View達(dá)到更好的表現(xiàn)效果嚷掠,建議不要關(guān)掉有用到自定義View界面的硬件加速(因?yàn)樵赩iew層面只能關(guān)閉硬件加速,無(wú)法開(kāi)啟硬件加速鳄梅,所以只能控制Activity和Window層面的硬件加速)叠国。
圖文混排:
涉及到圖文混排的自定義View,一定要將排版和顯示這兩件事情分開(kāi)戴尸,因?yàn)榕虐婧臅r(shí)但不涉及到UI的更新粟焊,可以在線程中處理,但顯示必須要更新UI孙蒙,所以在onDraw方法里面盡量不要做耗時(shí)和邏輯處理项棠,只純粹做顯示操作。對(duì)于排版可以考慮異步挎峦,或者先完成排版香追,后續(xù)只需要直接顯示即可,這得具體問(wèn)題具體分析坦胶。
同時(shí)顯示也有技巧透典,為了節(jié)省內(nèi)存,可以考慮做緩存顿苇,一個(gè)控件可能不只一頁(yè)內(nèi)容峭咒,可以在內(nèi)存中緩存當(dāng)前頁(yè)和當(dāng)前頁(yè)的前、后兩頁(yè)纪岁,當(dāng)滑動(dòng)時(shí)凑队,始終按照這種策略更新緩存內(nèi)容就可以了,這樣既達(dá)到了節(jié)省內(nèi)存幔翰、又提高效率的目的漩氨。
getHistorySize
對(duì)于有涉及到觸摸操作的自定義View(比如手寫控件),是在onTouchEvent方法中接收觸摸消息的遗增,但限于Android系統(tǒng)和設(shè)備本身的情況叫惊,底層上報(bào)的點(diǎn)信息不一定能夠?qū)崟r(shí)通過(guò)MotionEvent回調(diào)到上層,底層1秒鐘可能傳了幾百個(gè)點(diǎn)做修,但onTouchEvent方法中接收到的可能只有幾十個(gè)點(diǎn)霍狰,如果需要更為平滑地點(diǎn)信息,可以借助MotionEvent的getHistorySize方法獲取底層上報(bào)的更多點(diǎn)信息缓待,關(guān)于getHistorySize的解釋蚓耽,請(qǐng)參見(jiàn)參考資料中對(duì)平滑手寫簽名效果的介紹渠牲。
SpannableString
使用過(guò)SpannableString的都知道旋炒,可以通過(guò)它將同一串字符中的不同文字做不同的處理,比如某些文字的顏色签杈、字體瘫镇、背景色鼎兽、大小等有變化,都可以通過(guò)它來(lái)設(shè)置铣除,熟練掌握SpannableString對(duì)于靈活自定義View會(huì)有很大地幫助谚咬。
四、參考資料
五泽铛、優(yōu)質(zhì)開(kāi)源項(xiàng)目
Android系統(tǒng)應(yīng)用源碼中的各種自定義控件
六尚辑、忠告
千萬(wàn)別一言不合就自定義,能夠用Android基礎(chǔ)控件解決的問(wèn)題就盡量用基礎(chǔ)控件盔腔,其次是用基礎(chǔ)控件的組合杠茬,如果是確實(shí)有必要自定義才考慮自定義。自定義的控件既需要耗費(fèi)較長(zhǎng)的開(kāi)發(fā)時(shí)間弛随,又不一定能保證有基礎(chǔ)控件那么高的效率(基礎(chǔ)控件都是谷歌優(yōu)化過(guò)了的)瓢喉。
更多原創(chuàng)文章和優(yōu)質(zhì)資源請(qǐng)關(guān)注公眾號(hào):