一、前言
文中涉及到很多自己的理解,能力有限,有問題的地方還請(qǐng)指正。
很多人把自定義View想得復(fù)雜了,以為有多高深,主要還是沒有實(shí)踐過,沒有足夠的自信;但也有很多人把自定義View想得簡(jiǎn)單了,以為摸清View的幾個(gè)關(guān)鍵回調(diào)、知道自定義屬性和Android的消息分發(fā)機(jī)制就算是老司機(jī)了,其實(shí)對(duì)于自定義View來講,設(shè)計(jì)、排版、效率都是很費(fèi)腦筋的,我在github上到現(xiàn)在都沒發(fā)現(xiàn)一個(gè)像樣的圖文混排自定義View。
常見的Android自定義View主要有兩種類型:
組合控件:通過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)在很多都是通過webview加載網(wǎng)頁(yè)實(shí)現(xiàn)了)、詞典取詞控件、圖表控件、個(gè)性化進(jìn)度條、彈幕顯示控件、Markdown控件、IDE代碼編輯控件等。
按照上面這種方式分只是便于理解,很多時(shí)候有些控件既有組合,又需要復(fù)寫所繼承類的回調(diào)方法。
二、自定義View的價(jià)值
能夠做到基礎(chǔ)控件無法做到的效果,為應(yīng)用的表現(xiàn)增色;
在多個(gè)應(yīng)用并行開發(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在主線程中繪制,在有無硬件加速的情況下都能工作(沒有硬件加速的情況下,canvas的有些方法會(huì)失效);
SurfaceView:繼承自View,繪制和顯示效率高,因?yàn)閾碛歇?dú)立的繪圖表面,UI在一個(gè)獨(dú)立的線程中進(jìn)行繪制,不會(huì)占用主線程的資源。SurfaceView的使用和普通的View不一樣,需要結(jié)合SurfaceHodler一起使用。因?yàn)楹退拗鞔翱诓皇枪蚕硗粋€(gè)繪圖表面的原因,筆者在實(shí)際使用SurfaceView的過程中發(fā)現(xiàn)對(duì)其做動(dòng)畫操作會(huì)達(dá)不到想要的效果(一坨黑色);
TextureView:繼承自View,與SurfaceView相比,TextureView不會(huì)創(chuàng)建一個(gè)單獨(dú)的繪圖表面,這使得它可以像一般的View一樣執(zhí)行一些變換操作,比如移動(dòng)、動(dòng)畫等等,但TextureView必須在硬件加速開啟的窗口中才能正常工作;
View的三大核心方法onMeasure、onLayout、onDraw
onMeasure:用于測(cè)量視圖的大?。?/p>
onLayout:用于給視圖進(jìn)行布局;
onDraw:用于對(duì)視圖進(jìn)行繪制;
自定義屬性
對(duì)于自定義View的一些屬性設(shè)置,除了可以在自定義View中提供公開接口外,還可以通過自定義屬性,在對(duì)自定義View布局時(shí)就指定,這樣可以簡(jiǎn)化用戶使用控件的復(fù)雜度,實(shí)現(xiàn)自定義屬性的步驟如下:
在自定義View工程的res/values文件夾下新建一個(gè)attrs.xml的文件,在里面定義自定義屬性的ID、屬性和屬性對(duì)應(yīng)的類型,eg:
在自定義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.DictViewtextSize){ textSize = a.getDimensionPixelSize(attr, textSize); }else if(attr == com.test.dict.R.styleable.DictViewtextColor){ 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.DictViewwidth){ setWidth(a.getDimensionPixelSize(attr, mWidth)); }else if(attr == com.test.dict.R.styleable.DictViewheight){ setHeight(a.getDimensionPixelSize(attr, mHeight)); } } a.recycle();
對(duì)自定義屬性的解析需要注意兩點(diǎn):
1.TypedArray使用完成后一定要調(diào)用其recycle方法,否則會(huì)有內(nèi)存泄露的問題;
2.如果自定義View在一個(gè)單獨(dú)的module中(不屬于主工程),對(duì)attr的獲取不能使用switch-case語(yǔ)句,要用if...else,具體原因之前有介紹過,詳見:在Android library中不能使用switch-case語(yǔ)句訪問資源ID的原因分析及解決方案
完成自定義屬性的定義后,就可以在布局自定義View的過程中使用自定義屬性了,具體步驟如下:
在xml布局文件的根標(biāo)簽或者需要使用自定義屬性的標(biāo)簽中指定自定義屬性的命名空間,其中這里的dictview就是命名空間,是可以隨意指定的:
xmlns:dictview="http://schemas.android.com/apk/res-auto"
在自定義View的布局中使用自定義屬性,所有自定義屬性的設(shè)置都是在指定的命名空間下的,因?yàn)槭亲远x,所以不能用android這個(gè)命名空間:
雙緩沖
在移動(dòng)設(shè)備中很容易出現(xiàn)效率問題,對(duì)于效率問題的處理,主要方法是時(shí)間換空間或者空間換時(shí)間;自定義View可能存在顯示的效率問題,可以通過雙緩沖來解決這個(gè)問題,雙緩沖就是用空間換時(shí)間的典型例子,同一個(gè)View在內(nèi)存中創(chuàng)建了兩份同樣大小的內(nèi)存,一份用于繪制,一份用于顯示,繪制是繪制在Bitmap上,顯示就是將這張bitmap顯示在畫布上。
硬件加速
在Android設(shè)備中,硬件加速默認(rèn)是開啟的,有些應(yīng)用出于內(nèi)存占用(開啟硬件加速會(huì)占用更多的內(nèi)存)和應(yīng)用特征的考慮(沒什么動(dòng)畫,基本沒有涉及到需要GPU的操作),會(huì)在AndroidManifest.xml中關(guān)掉硬件加速,這會(huì)導(dǎo)致自定義View時(shí),canvas的某些方法不能正常使用,為了讓自定義View達(dá)到更好的表現(xiàn)效果,建議不要關(guān)掉有用到自定義View界面的硬件加速(因?yàn)樵赩iew層面只能關(guān)閉硬件加速,無法開啟硬件加速,所以只能控制Activity和Window層面的硬件加速)。
圖文混排:
涉及到圖文混排的自定義View,一定要將排版和顯示這兩件事情分開,因?yàn)榕虐婧臅r(shí)但不涉及到UI的更新,可以在線程中處理,但顯示必須要更新UI,所以在onDraw方法里面盡量不要做耗時(shí)和邏輯處理,只純粹做顯示操作。對(duì)于排版可以考慮異步,或者先完成排版,后續(xù)只需要直接顯示即可,這得具體問題具體分析。
同時(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í)通過MotionEvent回調(diào)到上層,底層1秒鐘可能傳了幾百個(gè)點(diǎn),但onTouchEvent方法中接收到的可能只有幾十個(gè)點(diǎn),如果需要更為平滑地點(diǎn)信息,可以借助MotionEvent的getHistorySize方法獲取底層上報(bào)的更多點(diǎn)信息,關(guān)于getHistorySize的解釋,請(qǐng)參見參考資料中對(duì)平滑手寫簽名效果的介紹。
SpannableString
使用過SpannableString的都知道,可以通過它將同一串字符中的不同文字做不同的處理,比如某些文字的顏色、字體、背景色、大小等有變化,都可以通過它來設(shè)置,熟練掌握SpannableString對(duì)于靈活自定義View會(huì)有很大地幫助。
四、參考資料
Android LayoutInflater原理分析,帶你一步步深入了解View(一)
Android視圖繪制流程完全解析,帶你一步步深入了解View(二)
Android視圖狀態(tài)及重繪流程分析,帶你一步步深入了解View(三)
Android自定義View的實(shí)現(xiàn)方法,帶你一步步深入了解View(四)
Android 觸摸及手勢(shì)操作GestureDetector
通過Spannable對(duì)象設(shè)置textview的樣式
Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView
Android視圖SurfaceView的實(shí)現(xiàn)原理分析
Android手寫優(yōu)化-平滑的簽名效果實(shí)現(xiàn)
Android手寫優(yōu)化-更為平滑的簽名效果實(shí)現(xiàn)
五、優(yōu)質(zhì)開源項(xiàng)目
Android系統(tǒng)應(yīng)用源碼中的各種自定義控件
六、忠告
千萬別一言不合就自定義,能夠用Android基礎(chǔ)控件解決的問題就盡量用基礎(chǔ)控件,其次是用基礎(chǔ)控件的組合,如果是確實(shí)有必要自定義才考慮自定義。自定義的控件既需要耗費(fèi)較長(zhǎng)的開發(fā)時(shí)間,又不一定能保證有基礎(chǔ)控件那么高的效率(基礎(chǔ)控件都是谷歌優(yōu)化過了的)。