自定義View,也可以稱(chēng)為自定義控件,通過(guò)自定義View可以使得控件實(shí)現(xiàn)各種定制的效果。

  實(shí)現(xiàn)自定義View,需要掌握View的底層工作原理,比如View的測(cè)量過(guò)程、布局流程以及繪制流程,除此之外,還需要掌握View常見(jiàn)的回調(diào)方法。而對(duì)于那些具有滑動(dòng)效果的自定義View,我們還需要處理View的滑動(dòng),如果遇到滑動(dòng)沖突則需要處理相應(yīng)的滑動(dòng)沖突。

  下面是View的常見(jiàn)回調(diào)方法:

  • 構(gòu)造方法

  • onAttach

  • onVisibilityChanged

  • onDetach

  • onFinishInflate

  • onSizeChanged

  • onMeasure

  • onLayout

  • onTouchEvent

  自定義控件的實(shí)現(xiàn)手段可簡(jiǎn)要分為四種類(lèi):

  • 繼承View重寫(xiě)onDraw方法,這種方法主要是用于實(shí)現(xiàn)一些不規(guī)則的效果,采用這種方式需要自己支持wrap_content,并且處理padding。

  • 繼承ViewGroup派生特殊的Layout,這種方法主要是用于實(shí)現(xiàn)自定義的布局,當(dāng)某種效果看起來(lái)像是幾個(gè)View組合在一起時(shí),可以采用這種方法來(lái)實(shí)現(xiàn)。采用這種方法是需要合理處理ViewGroup的測(cè)量和布局這兩個(gè)過(guò)程,并同時(shí)處理子元素的測(cè)量和布局過(guò)程。

  • 繼承特定的View,用于拓展已有的View的功能。

  • 繼承特定的ViewGroup(如LinearLayout、RelativeLayout),其適用情形和方法2 類(lèi)似。

  在自定義View中需要的注意點(diǎn):

  應(yīng)當(dāng)遵守Android標(biāo)準(zhǔn)控件的規(guī)范(如命名、可配置、事件處理、狀態(tài)保存及恢復(fù)等)

  • 命名表意明確

  • 控件屬性可以在XML中配置

  • 讓View支持wrap_content和padding(下文會(huì)具體講到)

  • 在View中盡量不使用Handler,因?yàn)閂iew中自帶post系列的方法。

  • 自定義View的內(nèi)存泄漏問(wèn)題(如果有線程或者動(dòng)畫(huà),需要及時(shí)停止)

  • View的滑動(dòng)沖突(在View帶有滑動(dòng)嵌套的情形,需要處理好滑動(dòng)沖突)

  • 具有一定的交互性,如按下、點(diǎn)擊等

  • 自定義View內(nèi)部實(shí)現(xiàn)狀態(tài)保存和恢復(fù)的機(jī)制

  • 兼容性

  下面主要從View的基本知識(shí)、View的繪制過(guò)程講一下View的工作原理。

1.從Activity中的View結(jié)構(gòu)講起

  每個(gè)Activity都含有一個(gè)Window對(duì)象,而這個(gè)Window對(duì)象一般都是PhoneWindow。PhoneWindow將以DecorView設(shè)置為整個(gè)應(yīng)用窗口的根View。DecorView作為窗口界面的頂層視圖,封裝了一些窗口操作的通用方法。可以這么說(shuō),DecorView將要顯示的具體內(nèi)容呈現(xiàn)在了PhoneWindow中,這里面的所有的View的監(jiān)聽(tīng)事件都是通過(guò)WindowManagerService來(lái)接收的,并通過(guò)Activity對(duì)象來(lái)回調(diào)相應(yīng)的onClickListenr。

  在顯示上,將屏幕分成兩部分,一個(gè)是TitleView,另一個(gè)是ContentView,這個(gè)ContentView想必大家都很熟悉,它是一個(gè)ID為content的FrameLayout,activity_main就是設(shè)置在這樣一個(gè)FrameLayout中。

如下圖1 和圖2 所示:

 移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

                 圖1

 移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

                 圖2

 

View的繪制流程:

 移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

              圖3

  如上圖所示,performTraversals會(huì)依次調(diào)用performMeasure、performLayout、performDraw三個(gè)方法,這三個(gè)方法分別完成頂級(jí)View的measure、layout、draw這三大流程。

  其中在performMeasure中又會(huì)調(diào)用measure,接著在measure中調(diào)用onMeasure方法,在onMeasure中會(huì)對(duì)所有的子元素進(jìn)行measure過(guò)程,這個(gè)時(shí)候measure流程就從父容器傳遞到子元素中了,即完成依次measure操作,接著子元素進(jìn)行同樣的measure過(guò)程,如此方法直至完成整個(gè)View樹(shù)的遍歷。同理,performLayout和performDraw的傳遞流程和performmeasure是類(lèi)似的(performDraw的傳遞過(guò)程是在draw方法中的dispatchDraw完成的,并無(wú)實(shí)質(zhì)區(qū)別)。

       measure過(guò)程決定了View的寬高,measure完成后,可以通過(guò)getMeasuredWidth和getMeasuredHeight方法來(lái)獲取到View測(cè)量后的寬高,在幾乎所有的情況下它都等同于View的最終高度,但特殊情況除外。Layout過(guò)程確定了View的四個(gè)頂點(diǎn)的坐標(biāo)和實(shí)際的View的寬高,完成以后,可以通過(guò)getTop、getBottom、getLeft、getRight來(lái)得到四個(gè)頂點(diǎn)的位置,并可以通過(guò)getWidth和getHeight來(lái)得到View的最終寬高。Draw過(guò)程決定了View的顯示,只有draw方法完成后,View的內(nèi)容才會(huì)顯示在屏幕上。

2.如何完成測(cè)量過(guò)程呢?

  Android系統(tǒng)提供了一個(gè)MeasureSpec類(lèi),通過(guò)它可以幫助我們測(cè)量View。MeasureSpec是一個(gè)32位的int值,其中高2位為測(cè)量的模式,低30位為測(cè)量的大小。

  EXACTLY:精確值模式, 當(dāng)我們將空間的layout_width或者layout_height屬性指定為具體值時(shí),或者指定為match_parent屬性時(shí),系統(tǒng)使用的是EXACTLY。

  AT_MOST:最大值模式,當(dāng)空間的layout_width屬性或者layout_height屬性為wrap_content時(shí),控件大小一般隨著空間的子控件或者內(nèi)容的變化而變化,此時(shí),控件的尺寸只要不超過(guò)父控件允許的最大尺寸即可。

  UNSPECIFIED:不指定其測(cè)量大小,通常情況下在繪制自定義View時(shí)才會(huì)使用它。

在view的測(cè)量過(guò)程中,系統(tǒng)會(huì)將LayoutParams在父容器的約束下轉(zhuǎn)換為對(duì)應(yīng)的MeasureSpec,然后根據(jù)這個(gè)MeasureSpec來(lái)確定View測(cè)量后的寬高。MeasureSpec由父容器和LayoutParams共同決定。

  對(duì)于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams決定

  對(duì)于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams決定

       當(dāng)View的LayoutParams采用精確值時(shí),不管父容器的MeasureSpec是什么,View的MeasureSpec模式都是EXACTLY,并且大小遵循LayoutParms的大小。

       當(dāng)View的寬高是match_parent模式,view的MeasureSpec模式遵循父容器的MeasureSpec模式。

       當(dāng)View的寬高是wrap_content,不管父容器的模式是EXACTLY還是AT_MOST,View的模式都是AT_MOST并且大小不超過(guò)父容器的剩余空間。

   下面分別簡(jiǎn)要講一下View的measure過(guò)程和ViewGroup的measure過(guò)程。

1)View的measure過(guò)程:

  參考源碼:

移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {         

     setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),    

     getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}

移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

                                 圖4

  由上述源碼可知,在調(diào)用onMeasure方法時(shí)會(huì)調(diào)用setMeasuredDimension方法,在這個(gè)方法中會(huì)傳入其寬高。由此可知,在自定義View中,需要重新定義view的寬和高。

View類(lèi)默認(rèn)的onMeasure方法只支持EXACTLY模式,如果在自定義控件的時(shí)候不重寫(xiě)onMeasure方法,就只能使用EXACTLY模式??丶梢韵鄳?yīng)你指定的具體寬高值或者match_parent屬性,如果要讓自定義View支持wrap_content屬性,則必須要重寫(xiě)onMeasure方法,否則在布局中使用wrap_content就相當(dāng)于使用match_parent)

 

2)ViewGroup的measure過(guò)程:

       對(duì)于ViewGroup而言,除了完成自己的measure過(guò)程,還要遍歷去調(diào)用所有子元素的measure方法,各個(gè)子元素再遞歸執(zhí)行這個(gè)過(guò)程。

       ViewGroup是一個(gè)抽象類(lèi),它沒(méi)有重寫(xiě)View的onMeasure方法,但它提供了一個(gè)measureChildren的方法,在measureChildren方法中它會(huì)遍歷ViewGroup中的子元素,并調(diào)用measureChild方法,對(duì)子元素進(jìn)行measure。measureChild的思想就是取出子元素的LayoutParams,然后通過(guò)getChildMeasureSpec來(lái)創(chuàng)建子元素的measureSpec,最后將子元素的measureSpec傳遞給measure方法就能完成測(cè)量,如下圖所示:

 移動(dòng)開(kāi)發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開(kāi)發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

                                                      圖5

 

  正如前面提到的ViewGroup是一個(gè)抽象類(lèi),它沒(méi)有重寫(xiě)onMeasure方法,其測(cè)量過(guò)程中的onMeasure需要其子類(lèi)去具體實(shí)現(xiàn)。如LinearLayout、RelativeLayout。不同的ViewGroup子類(lèi)的布局特性不同,這也導(dǎo)致其測(cè)量細(xì)節(jié)不同。

  下面簡(jiǎn)要了解一下LinearLyaout和RelativeLayout的onMeasure實(shí)現(xiàn)

1)LinearLayout的Measure實(shí)現(xiàn):

LinearLayout的布局方向有兩種,所以LinearLayout會(huì)根據(jù)mOrientation來(lái)分別調(diào)用measureVertical或者是measureHorizontal。以水平布局為例,

遍歷所有的view,跳過(guò)為null或者屬性為View.GONE的,加上分割線寬度mDividerWidth和左右margin,計(jì)算所有View的childWidth之和mTotalLength,統(tǒng)計(jì)所有View的weight和totalWeight,并且對(duì)子view進(jìn)行測(cè)量。

 

2)RelativeLayout的Measure實(shí)現(xiàn):

  當(dāng)?shù)谝淮螆?zhí)行onMeasure或者requestLayout后,需要調(diào)用sortChildren方法,根據(jù)添加順序?qū)λ械淖觱iew進(jìn)行排序,橫著一次,豎著一次,然后對(duì)兩個(gè)序列進(jìn)行檢查,通過(guò)依賴(lài)圖靜態(tài)類(lèi)中的getSortedViews方法根據(jù)依賴(lài)關(guān)系進(jìn)行排序。

  之后在onMeasure中,對(duì)子view進(jìn)行遍歷,即對(duì)兩個(gè)序列進(jìn)行分別遍歷。

  首先是橫向遍歷,調(diào)用mSortedHorizontalChildren,獲取RelativeLayout.layoutParams,并依次調(diào)用方法,計(jì)算控件的橫向位置及mLeft和mRight,然后橫向測(cè)量子View,接下去根據(jù)前面的結(jié)果很想擺放子View,如果此時(shí)父RelativeLayout的寬度是WRAP_CONTENT,會(huì)在此時(shí)對(duì)寬高進(jìn)行修正。

  橫向完畢后進(jìn)行垂直排列的View序列進(jìn)行上述操在,步驟大致相同,在此處會(huì)對(duì)子view進(jìn)行measure時(shí)就會(huì)正確的測(cè)量,之后的操作就是對(duì)父RelativeLayout的寬高等屬性進(jìn)行再次修正。

  從上面的分析中,一個(gè)最明顯的不同就是RelativeLayout在進(jìn)行measure過(guò)程中需要進(jìn)行兩次遍歷,而LinearLayout則只需要一次遍歷過(guò)程。

      此外,需要注意的是,在某些極端情況下,系統(tǒng)可能需要調(diào)用多次measure才能確定最終的測(cè)量寬高,在這種情況下,在onMeasure方法中拿到的測(cè)量高很可能是不準(zhǔn)確的。所以最好在onLayout方法中獲取View的高寬。

 

3.如何獲取View的寬和高

(1)調(diào)用onWindowFocusChanged方法(焦點(diǎn)變化),這個(gè)時(shí)候View已經(jīng)初始化完畢,這個(gè)時(shí)候去獲取View的寬高是沒(méi)有問(wèn)題的。然而當(dāng)頻繁進(jìn)行onResume和onPause,onWindowFocusChanged方法也會(huì)被頻繁調(diào)用。

(2)調(diào)用view.post(runnable)

         通過(guò)post將一個(gè)Runnable投遞到消息隊(duì)列的尾部,然后等待Looper調(diào)用此Runnable,view也已經(jīng)初始化好了。

(3)ViewTreeObserver

         使用ViewTreeObserver的眾多回調(diào)可以使用這個(gè)功能,如OnGlobalLayoutListener,當(dāng)View樹(shù)的狀態(tài)發(fā)生改變或者View樹(shù)的View的可見(jiàn)性發(fā)生改變時(shí),OnGlobalLayoutListener會(huì)被回調(diào),需要注意的是,伴隨著View樹(shù)狀態(tài)的改變,onGlobalLayoutListener會(huì)被回調(diào)多次。

(4)View.measure(int widthMeasureSpec,int heightMeasureSpec)

  • match_parent 不能

  •  具體值和wrap_content可以。

4.Layout過(guò)程

         Layout過(guò)程用于ViewGroup確定子元素的位置,當(dāng)ViewGroup的位置被確定后,它在onLayout中會(huì)遍歷所有的子元素,并調(diào)用其layout方法,在layout方法中onLayout方法又會(huì)被調(diào)用。

         layout方法首先通過(guò)setFrame方法倆設(shè)置view的四個(gè)頂點(diǎn)的位置,接著調(diào)用onLayout方法,確定子元素的位置。

         由于onLayout的實(shí)現(xiàn)同樣與布局有關(guān),因此View和ViewGroup均沒(méi)有實(shí)現(xiàn)onLayout方法。

5.draw過(guò)程

  • 將View繪制到屏幕上,大概的幾個(gè)步驟
    1.繪制背景background.draw(canvas)
    2.繪制自己(onDraw)
    3.繪制children(dispatchDraw)
    4.繪制裝飾(onDrawScrollBars)

  • View的繪制過(guò)程是通過(guò)dispatchDraw來(lái)實(shí)現(xiàn)的,它會(huì)遍歷所有子元素的draw方法。

  • 如果一個(gè)View不需要繪制任何內(nèi)容,那么設(shè)置setWillNotDraw為true后,系統(tǒng)會(huì)進(jìn)行相應(yīng)的優(yōu)化;ViewGroup默認(rèn)為true,如果我們的自定義ViewGroup需要通過(guò)onDraw來(lái)繪制內(nèi)容的時(shí)候,需要顯示的關(guān)閉它。

http://www.cnblogs.com/hustzhb/p/6926010.html