[Android技術(shù)專題]自定義View

一、前言

文中涉及到很多自己的理解,能力有限,有問題的地方還請(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ì)有很大地幫助。

四、參考資料

Creating Custom Views

推翻自己和過往,重學(xué)自定義View

Android LayoutInflater原理分析,帶你一步步深入了解View(一)

Android視圖繪制流程完全解析,帶你一步步深入了解View(二)

Android視圖狀態(tài)及重繪流程分析,帶你一步步深入了解View(三)

Android自定義View的實(shí)現(xiàn)方法,帶你一步步深入了解View(四)

Android 深入理解Android中的自定義屬性

公共技術(shù)點(diǎn)之 View 事件傳遞

Android 觸摸及手勢(shì)操作GestureDetector

通過Spannable對(duì)象設(shè)置textview的樣式

Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView

Android視圖SurfaceView的實(shí)現(xiàn)原理分析

Android硬件加速

Android手寫優(yōu)化-平滑的簽名效果實(shí)現(xiàn)

Android手寫優(yōu)化-更為平滑的簽名效果實(shí)現(xiàn)

五、優(yōu)質(zhì)開源項(xiàng)目

awesome-android-ui

Android 開源項(xiàng)目分類匯總

Android平臺(tái)下的原生Markdown解析器

MarkdownEditors

Android開源彈幕引擎

DraweeTextView

Android圖文混排控件MixtureTextView

markers

Android系統(tǒng)應(yīng)用源碼中的各種自定義控件

六、忠告

千萬別一言不合就自定義,能夠用Android基礎(chǔ)控件解決的問題就盡量用基礎(chǔ)控件,其次是用基礎(chǔ)控件的組合,如果是確實(shí)有必要自定義才考慮自定義。自定義的控件既需要耗費(fèi)較長(zhǎng)的開發(fā)時(shí)間,又不一定能保證有基礎(chǔ)控件那么高的效率(基礎(chǔ)控件都是谷歌優(yōu)化過了的)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容