[Android技術(shù)專題]自定義View從入門到上天

一碗降、前言

標(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)目

六尚辑、忠告

千萬(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):

open_dev
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市撵幽,隨后出現(xiàn)的幾起案子灯荧,更是在濱河造成了極大的恐慌,老刑警劉巖盐杂,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逗载,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡链烈,警方通過(guò)查閱死者的電腦和手機(jī)厉斟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)强衡,“玉大人擦秽,你說(shuō)我怎么就攤上這事′銮冢” “怎么了感挥?”我有些...
    開(kāi)封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)越败。 經(jīng)常有香客問(wèn)我触幼,道長(zhǎng),這世上最難降的妖魔是什么究飞? 我笑而不...
    開(kāi)封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任置谦,我火速辦了婚禮堂鲤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘媒峡。我一直安慰自己瘟栖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布谅阿。 她就那樣靜靜地躺著半哟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪签餐。 梳的紋絲不亂的頭發(fā)上镜沽,一...
    開(kāi)封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音贱田,去河邊找鬼缅茉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛男摧,可吹牛的內(nèi)容都是我干的蔬墩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼耗拓,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼拇颅!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起乔询,我...
    開(kāi)封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤樟插,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后竿刁,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體黄锤,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年食拜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鸵熟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡负甸,死狀恐怖流强,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情呻待,我是刑警寧澤打月,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站蚕捉,受9級(jí)特大地震影響奏篙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鱼冀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一报破、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧千绪,春花似錦充易、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至瑞妇,卻和暖如春稿静,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辕狰。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工改备, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蔓倍。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓悬钳,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親偶翅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子默勾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,509評(píng)論 25 707
  • 一、前言 文中涉及到很多自己的理解聚谁,能力有限母剥,有問(wèn)題的地方還請(qǐng)指正。 很多人把自定義View想得復(fù)雜了形导,以為有多高...
    passiontim閱讀 594評(píng)論 0 6
  • View簡(jiǎn)介 1.View原理及其子類介紹 View是Android UI組件的基類环疼,ViewGroup是容納UI...
    叛逆的青春不回頭閱讀 1,337評(píng)論 0 15
  • 關(guān)于java8的lamba 在github 上 看到比較好的介紹, 有一些自己不清楚的,寫在這里 主要是anony...
    patrick002閱讀 497評(píng)論 0 1
  • 聽(tīng)音樂(lè)的孩子內(nèi)心都是純凈的朵耕。經(jīng)常聽(tīng)到萌在隔壁放著音樂(lè)寫作業(yè)秦爆,有時(shí)會(huì)情不自禁的唱起來(lái),歡快的憔披、婉轉(zhuǎn)的等限,都很好聽(tīng),不會(huì)...
    觀蘭閱讀 138評(píng)論 0 0