自定義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 所示:
圖1
圖2
View的繪制流程:
圖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ò)程:
參考源碼:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
圖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è)量,如下圖所示:
圖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