在前面一篇文章中,我?guī)е蠹乙黄饛脑创a的層面上分析了視圖的繪制流程,了解了視圖繪制流程中onMeasure、onLayout、onDraw這三個最重要步驟的工作原理,那么今天我們將繼續(xù)對View進行深入探究,學習一下視圖狀態(tài)以及重繪方面的知識。如果你還沒有看過我前面一篇文章,可以先去閱讀 Android視圖繪制流程完全解析,帶你一步步深入了解View(二) ?。
相信大家在平時使用View的時候都會發(fā)現(xiàn)它是有狀態(tài)的,比如說有一個按鈕,普通狀態(tài)下是一種效果,但是當手指按下的時候就會變成另外一種效果,這樣才會給人產(chǎn)生一種點擊了按鈕的感覺。當然了,這種效果相信幾乎所有的Android程序員都知道該如何實現(xiàn),但是我們既然是深入了解View,那么自然也應(yīng)該知道它背后的實現(xiàn)原理應(yīng)該是什么樣的,今天就讓我們來一起探究一下吧。
一、視圖狀態(tài)
視圖狀態(tài)的種類非常多,一共有十幾種類型,不過多數(shù)情況下我們只會使用到其中的幾種,因此這里我們也就只去分析最常用的幾種視圖狀態(tài)。
1. enabled
表示當前視圖是否可用??梢哉{(diào)用setEnable()方法來改變視圖的可用狀態(tài),傳入true表示可用,傳入false表示不可用。它們之間最大的區(qū)別在于,不可用的視圖是無法響應(yīng)onTouch事件的。
2. focused
表示當前視圖是否獲得到焦點。通常情況下有兩種方法可以讓視圖獲得焦點,即通過鍵盤的上下左右鍵切換視圖,以及調(diào)用requestFocus()方法。而現(xiàn)在的Android手機幾乎都沒有鍵盤了,因此基本上只可以使用requestFocus()這個辦法來讓視圖獲得焦點了。而requestFocus()方法也不能保證一定可以讓視圖獲得焦點,它會有一個布爾值的返回值,如果返回true說明獲得焦點成功,返回false說明獲得焦點失敗。一般只有視圖在focusable和focusable in touch mode同時成立的情況下才能成功獲取焦點,比如說EditText。
3. window_focused
表示當前視圖是否處于正在交互的窗口中,這個值由系統(tǒng)自動決定,應(yīng)用程序不能進行改變。
4. selected
表示當前視圖是否處于選中狀態(tài)。一個界面當中可以有多個視圖處于選中狀態(tài),調(diào)用setSelected()方法能夠改變視圖的選中狀態(tài),傳入true表示選中,傳入false表示未選中。
5. pressed
表示當前視圖是否處于按下狀態(tài)。可以調(diào)用setPressed()方法來對這一狀態(tài)進行改變,傳入true表示按下,傳入false表示未按下。通常情況下這個狀態(tài)都是由系統(tǒng)自動賦值的,但開發(fā)者也可以自己調(diào)用這個方法來進行改變。
我們可以在項目的drawable目錄下創(chuàng)建一個selector文件,在這里配置每種狀態(tài)下視圖對應(yīng)的背景圖片。比如創(chuàng)建一個compose_bg.xml文件,在里面編寫如下代碼:
[html] view plaincopy
< selector ?xmlns:android ="http://schemas.android.com/apk/res/android" > ???? ????< item ?android:drawable ="@drawable/compose_pressed" ?android:state_pressed ="true" > </ item > ?? ????< item ?android:drawable ="@drawable/compose_pressed" ?android:state_focused ="true" > </ item > ?? ????< item ?android:drawable ="@drawable/compose_normal" > </ item > ?? ?? </ selector > ??
這段代碼就表示,當視圖處于正常狀態(tài)的時候就顯示compose_normal這張背景圖,當視圖獲得到焦點或者被按下的時候就顯示compose_pressed這張背景圖。
創(chuàng)建好了這個selector文件后,我們就可以在布局或代碼中使用它了,比如將它設(shè)置為某個按鈕的背景圖,如下所示:
[html] view plaincopy
<? xml ?version ="1.0" ?encoding ="utf-8" ?> ??< LinearLayout ?xmlns:android ="http://schemas.android.com/apk/res/android" ??????android:layout_width ="match_parent" ?? ????android:layout_height ="match_parent" ?? ????android:orientation ="vertical" ?> ?? ?????? ????< Button ??? ????????android:id ="@+id/compose" ?? ????????android:layout_width ="60dp" ?? ????????android:layout_height ="40dp" ?? ????????android:layout_gravity ="center_horizontal" ?? ????????android:background ="@drawable/compose_bg" ?? ????????/> ?? ?????? </ LinearLayout > ??
現(xiàn)在運行一下程序,這個按鈕在普通狀態(tài)和按下狀態(tài)的時候就會顯示不同的背景圖片,如下圖所示:
這樣我們就用一個非常簡單的方法實現(xiàn)了按鈕按下的效果,但是它的背景原理到底是怎樣的呢?這就又要從源碼的層次上進行分析了。
我們都知道,當手指按在視圖上的時候,視圖的狀態(tài)就已經(jīng)發(fā)生了變化,此時視圖的pressed狀態(tài)是true。每當視圖的狀態(tài)有發(fā)生改變的時候,就會回調(diào)View的drawableStateChanged()方法,代碼如下所示:
[java] view plaincopy
protected ?void ?drawableStateChanged()?{??????Drawable?d?=?mBGDrawable;?? ????if ?(d?!=?null ?&&?d.isStateful())?{?? ????????d.setState(getDrawableState());?? ????}?? }??
在這里的第一步,首先是將mBGDrawable賦值給一個Drawable對象,那么這個mBGDrawable是什么呢?觀察setBackgroundResource()方法中的代碼,如下所示:
[java] view plaincopy
public ?void ?setBackgroundResource(int ?resid)?{??????if ?(resid?!=?0 ?&&?resid?==?mBackgroundResource)?{?? ????????return ;?? ????}?? ????Drawable?d=?null ;?? ????if ?(resid?!=?0 )?{?? ????????d?=?mResources.getDrawable(resid);?? ????}?? ????setBackgroundDrawable(d);?? ????mBackgroundResource?=?resid;?? }??
可以看到,在第7行調(diào)用了Resource的getDrawable()方法將resid轉(zhuǎn)換成了一個Drawable對象,然后調(diào)用了setBackgroundDrawable()方法并將這個Drawable對象傳入,在setBackgroundDrawable()方法中會將傳入的Drawable對象賦值給mBGDrawable。
而我們在布局文件中通過android:background屬性指定的selector文件,效果等同于調(diào)用setBackgroundResource()方法。也就是說drawableStateChanged()方法中的mBGDrawable對象其實就是我們指定的selector文件。
接下來在drawableStateChanged()方法的第4行調(diào)用了getDrawableState()方法來獲取視圖狀態(tài),代碼如下所示:
[java] view plaincopy
public ?final ?int []?getDrawableState()?{??????if ?((mDrawableState?!=?null )?&&?((mPrivateFlags?&?DRAWABLE_STATE_DIRTY)?==?0 ))?{?? ????????return ?mDrawableState;?? ????}?else ?{?? ????????mDrawableState?=?onCreateDrawableState(0 );?? ????????mPrivateFlags?&=?~DRAWABLE_STATE_DIRTY;?? ????????return ?mDrawableState;?? ????}?? }??
在這里首先會判斷當前視圖的狀態(tài)是否發(fā)生了改變,如果沒有改變就直接返回當前的視圖狀態(tài),如果發(fā)生了改變就調(diào)用onCreateDrawableState()方法來獲取最新的視圖狀態(tài)。視圖的所有狀態(tài)會以一個整型數(shù)組的形式返回。
在得到了視圖狀態(tài)的數(shù)組之后,就會調(diào)用Drawable的setState()方法來對狀態(tài)進行更新,代碼如下所示:
[java] view plaincopy
public ?boolean ?setState(final ?int []?stateSet)?{??????if ?(!Arrays.equals(mStateSet,?stateSet))?{?? ????????mStateSet?=?stateSet;?? ????????return ?onStateChange(stateSet);?? ????}?? ????return ?false ;?? }??
這里會調(diào)用Arrays.equals()方法來判斷視圖狀態(tài)的數(shù)組是否發(fā)生了變化,如果發(fā)生了變化則調(diào)用onStateChange()方法,否則就直接返回false。但你會發(fā)現(xiàn),Drawable的onStateChange()方法中其實就只是簡單返回了一個false,并沒有任何的邏輯處理,這是為什么呢?這主要是因為mBGDrawable對象是通過一個selector文件創(chuàng)建出來的,而通過這種文件創(chuàng)建出來的Drawable對象其實都是一個StateListDrawable實例,因此這里調(diào)用的onStateChange()方法實際上調(diào)用的是StateListDrawable中的onStateChange()方法,那么我們趕快看一下吧:
[java] view plaincopy
@Override ??protected ?boolean ?onStateChange(int []?stateSet)?{??????int ?idx?=?mStateListState.indexOfStateSet(stateSet);?? ????if ?(DEBUG)?android.util.Log.i(TAG,?"onStateChange?" ?+?this ?+?"?states?" ?? ????????????+?Arrays.toString(stateSet)?+?"?found?" ?+?idx);?? ????if ?(idx?<?0 )?{?? ????????idx?=?mStateListState.indexOfStateSet(StateSet.WILD_CARD);?? ????}?? ????if ?(selectDrawable(idx))?{?? ????????return ?true ;?? ????}?? ????return ?super .onStateChange(stateSet);?? }??
可以看到,這里會先調(diào)用indexOfStateSet()方法來找到當前視圖狀態(tài)所對應(yīng)的Drawable資源下標,然后在第9行調(diào)用selectDrawable()方法并將下標傳入,在這個方法中就會將視圖的背景圖設(shè)置為當前視圖狀態(tài)所對應(yīng)的那張圖片了。
那你可能會有疑問,在前面一篇文章中我們說到,任何一個視圖的顯示都要經(jīng)過非??茖W的繪制流程的,很顯然,背景圖的繪制是在draw()方法中完成的,那么為什么selectDrawable()方法能夠控制背景圖的改變呢?這就要研究一下視圖重繪的流程了。
二、視圖重繪
雖然視圖會在Activity加載完成之后自動繪制到屏幕上,但是我們完全有理由在與Activity進行交互的時候要求動態(tài)更新視圖,比如改變視圖的狀態(tài)、以及顯示或隱藏某個控件等。那在這個時候,之前繪制出的視圖其實就已經(jīng)過期了,此時我們就應(yīng)該對視圖進行重繪。
調(diào)用視圖的setVisibility()、setEnabled()、setSelected()等方法時都會導致視圖重繪,而如果我們想要手動地強制讓視圖進行重繪,可以調(diào)用invalidate()方法來實現(xiàn)。當然了,setVisibility()、setEnabled()、setSelected()等方法的內(nèi)部其實也是通過調(diào)用invalidate()方法來實現(xiàn)的,那么就讓我們來看一看invalidate()方法的代碼是什么樣的吧。
View的源碼中會有數(shù)個invalidate()方法的重載和一個invalidateDrawable()方法,當然它們的原理都是相同的,因此我們只分析其中一種,代碼如下所示:
[java] view plaincopy
void ?invalidate(boolean ?invalidateCache)?{??????if ?(ViewDebug.TRACE_HIERARCHY)?{?? ????????ViewDebug.trace(this ,?ViewDebug.HierarchyTraceType.INVALIDATE);?? ????}?? ????if ?(skipInvalidate())?{?? ????????return ;?? ????}?? ????if ?((mPrivateFlags?&?(DRAWN?|?HAS_BOUNDS))?==?(DRAWN?|?HAS_BOUNDS)?||?? ????????????(invalidateCache?&&?(mPrivateFlags?&?DRAWING_CACHE_VALID)?==?DRAWING_CACHE_VALID)?||?? ????????????(mPrivateFlags?&?INVALIDATED)?!=?INVALIDATED?||?isOpaque()?!=?mLastIsOpaque)?{?? ????????mLastIsOpaque?=?isOpaque();?? ????????mPrivateFlags?&=?~DRAWN;?? ????????mPrivateFlags?|=?DIRTY;?? ????????if ?(invalidateCache)?{?? ????????????mPrivateFlags?|=?INVALIDATED;?? ????????????mPrivateFlags?&=?~DRAWING_CACHE_VALID;?? ????????}?? ????????final ?AttachInfo?ai?=?mAttachInfo;?? ????????final ?ViewParent?p?=?mParent;?? ????????if ?(!HardwareRenderer.RENDER_DIRTY_REGIONS)?{?? ????????????if ?(p?!=?null ?&&?ai?!=?null ?&&?ai.mHardwareAccelerated)?{?? ????????????????p.invalidateChild(this ,?null );?? ????????????????return ;?? ????????????}?? ????????}?? ????????if ?(p?!=?null ?&&?ai?!=?null )?{?? ????????????final ?Rect?r?=?ai.mTmpInvalRect;?? ????????????r.set(0 ,?0 ,?mRight?-?mLeft,?mBottom?-?mTop);?? ????????????p.invalidateChild(this ,?r);?? ????????}?? ????}?? }??
在這個方法中首先會調(diào)用skipInvalidate()方法來判斷當前View是否需要重繪,判斷的邏輯也比較簡單,如果View是不可見的且沒有執(zhí)行任何動畫,就認為不需要重繪了。之后會進行透明度的判斷,并給View添加一些標記位,然后在第22和29行調(diào)用
ViewParent的invalidateChild()方法,這里的ViewParent其實就是當前視圖的父視圖,因此會調(diào)用到ViewGroup的invalidateChild()方法中,代碼如下所示:
[java] view plaincopy
public ?final ?void ?invalidateChild(View?child,?final ?Rect?dirty)?{??????ViewParent?parent?=?this ;?? ????final ?AttachInfo?attachInfo?=?mAttachInfo;?? ????if ?(attachInfo?!=?null )?{?? ????????final ?boolean ?drawAnimation?=?(child.mPrivateFlags?&?DRAW_ANIMATION)?==?DRAW_ANIMATION;?? ????????if ?(dirty?==?null )?{?? ????????????......?? ????????}?else ?{?? ????????????......?? ????????????do ?{?? ????????????????View?view?=?null ;?? ????????????????if ?(parent?instanceof ?View)?{?? ????????????????????view?=?(View)?parent;?? ????????????????????if ?(view.mLayerType?!=?LAYER_TYPE_NONE?&&?? ????????????????????????????view.getParent()?instanceof ?View)?{?? ????????????????????????final ?View?grandParent?=?(View)?view.getParent();?? ????????????????????????grandParent.mPrivateFlags?|=?INVALIDATED;?? ????????????????????????grandParent.mPrivateFlags?&=?~DRAWING_CACHE_VALID;?? ????????????????????}?? ????????????????}?? ????????????????if ?(drawAnimation)?{?? ????????????????????if ?(view?!=?null )?{?? ????????????????????????view.mPrivateFlags?|=?DRAW_ANIMATION;?? ????????????????????}?else ?if ?(parent?instanceof ?ViewRootImpl)?{?? ????????????????????????((ViewRootImpl)?parent).mIsAnimating?=?true ;?? ????????????????????}?? ????????????????}?? ????????????????if ?(view?!=?null )?{?? ????????????????????if ?((view.mViewFlags?&?FADING_EDGE_MASK)?!=?0 ?&&?? ????????????????????????????view.getSolidColor()?==?0 )?{?? ????????????????????????opaqueFlag?=?DIRTY;?? ????????????????????}?? ????????????????????if ?((view.mPrivateFlags?&?DIRTY_MASK)?!=?DIRTY)?{?? ????????????????????????view.mPrivateFlags?=?(view.mPrivateFlags?&?~DIRTY_MASK)?|?opaqueFlag;?? ????????????????????}?? ????????????????}?? ????????????????parent?=?parent.invalidateChildInParent(location,?dirty);?? ????????????????if ?(view?!=?null )?{?? ????????????????????Matrix?m?=?view.getMatrix();?? ????????????????????if ?(!m.isIdentity())?{?? ????????????????????????RectF?boundingRect?=?attachInfo.mTmpTransformRect;?? ????????????????????????boundingRect.set(dirty);?? ????????????????????????m.mapRect(boundingRect);?? ????????????????????????dirty.set((int )?boundingRect.left,?(int )?boundingRect.top,?? ????????????????????????????????(int )?(boundingRect.right?+?0 .5f),?? ????????????????????????????????(int )?(boundingRect.bottom?+?0 .5f));?? ????????????????????}?? ????????????????}?? ????????????}?while ?(parent?!=?null );?? ????????}?? ????}?? }??
可以看到,這里在第10行進入了一個while循環(huán),當ViewParent不等于空的時候就會一直循環(huán)下去。在這個while循環(huán)當中會不斷地獲取當前布局的父布局,并調(diào)用它的invalidateChildInParent()方法,在ViewGroup的
invalidateChildInParent()方法中主要是來計算需要重繪的矩形區(qū)域,這里我們先不管它,當循環(huán)到最外層的根布局后,就會調(diào)用ViewRoot的invalidateChildInParent()方法了,代碼如下所示:
[java] view plaincopy
public ?ViewParent?invalidateChildInParent(final ?int []?location,?final ?Rect?dirty)?{??????invalidateChild(null ,?dirty);?? ????return ?null ;?? }??
這里的代碼非常簡單,僅僅是去調(diào)用了invalidateChild()方法而已,那我們再跟進去瞧一瞧吧:
[java] view plaincopy
public ?void ?invalidateChild(View?child,?Rect?dirty)?{??????checkThread();?? ????if ?(LOCAL_LOGV)?Log.v(TAG,?"Invalidate?child:?" ?+?dirty);?? ????mDirty.union(dirty);?? ????if ?(!mWillDrawSoon)?{?? ????????scheduleTraversals();?? ????}?? }??
這個方法也不長,它在第6行又調(diào)用了scheduleTraversals()這個方法,那么我們繼續(xù)跟進:
[java] view plaincopy
public ?void ?scheduleTraversals()?{??????if ?(!mTraversalScheduled)?{?? ????????mTraversalScheduled?=?true ;?? ????????sendEmptyMessage(DO_TRAVERSAL);?? ????}?? }??
可以看到,這里調(diào)用了sendEmptyMessage()方法,并傳入了一個DO_TRAVERSAL參數(shù)。了解Android異步消息處理機制的朋友們都會知道,任何一個Handler都可以調(diào)用sendEmptyMessage()方法來發(fā)送消息,并且在handleMessage()方法中接收消息,而如果你看一下ViewRoot的類定義就會發(fā)現(xiàn),它是繼承自Handler的,也就是說這里調(diào)用
sendEmptyMessage()方法出的消息,會在ViewRoot的handleMessage()方法中接收到。那么趕快看一下handleMessage()方法的代碼吧,如下所示:
[java] view plaincopy
public ?void ?handleMessage(Message?msg)?{??????switch ?(msg.what)?{?? ????case ?DO_TRAVERSAL:?? ????????if ?(mProfile)?{?? ????????????Debug.startMethodTracing("ViewRoot" );?? ????????}?? ????????performTraversals();?? ????????if ?(mProfile)?{?? ????????????Debug.stopMethodTracing();?? ????????????mProfile?=?false ;?? ????????}?? ????????break ;?? ????......?? }??
熟悉的代碼出現(xiàn)了!這里在第7行調(diào)用了performTraversals()方法,這不就是我們在前面一篇文章中學到的視圖繪制的入口嗎?雖然經(jīng)過了很多輾轉(zhuǎn)的調(diào)用,但是可以確定的是,調(diào)用視圖的invalidate()方法后確實會走到performTraversals()方法中,然后重新執(zhí)行繪制流程。之后的流程就不需要再進行描述了吧,可以參考 Android視圖繪制流程完全解析,帶你一步步深入了解View(二) ?這一篇文章。
了解了這些之后,我們再回過頭來看看剛才的selectDrawable()方法中到底做了什么才能夠控制背景圖的改變,代碼如下所示:
[java] view plaincopy
public ?boolean ?selectDrawable(int ?idx)?{??????if ?(idx?==?mCurIndex)?{?? ????????return ?false ;?? ????}?? ????final ?long ?now?=?SystemClock.uptimeMillis();?? ????if ?(mDrawableContainerState.mExitFadeDuration?>?0 )?{?? ????????if ?(mLastDrawable?!=?null )?{?? ????????????mLastDrawable.setVisible(false ,?false );?? ????????}?? ????????if ?(mCurrDrawable?!=?null )?{?? ????????????mLastDrawable?=?mCurrDrawable;?? ????????????mExitAnimationEnd?=?now?+?mDrawableContainerState.mExitFadeDuration;?? ????????}?else ?{?? ????????????mLastDrawable?=?null ;?? ????????????mExitAnimationEnd?=?0 ;?? ????????}?? ????}?else ?if ?(mCurrDrawable?!=?null )?{?? ????????mCurrDrawable.setVisible(false ,?false );?? ????}?? ????if ?(idx?>=?0 ?&&?idx?<?mDrawableContainerState.mNumChildren)?{?? ????????Drawable?d?=?mDrawableContainerState.mDrawables[idx];?? ????????mCurrDrawable?=?d;?? ????????mCurIndex?=?idx;?? ????????if ?(d?!=?null )?{?? ????????????if ?(mDrawableContainerState.mEnterFadeDuration?>?0 )?{?? ????????????????mEnterAnimationEnd?=?now?+?mDrawableContainerState.mEnterFadeDuration;?? ????????????}?else ?{?? ????????????????d.setAlpha(mAlpha);?? ????????????}?? ????????????d.setVisible(isVisible(),?true );?? ????????????d.setDither(mDrawableContainerState.mDither);?? ????????????d.setColorFilter(mColorFilter);?? ????????????d.setState(getState());?? ????????????d.setLevel(getLevel());?? ????????????d.setBounds(getBounds());?? ????????}?? ????}?else ?{?? ????????mCurrDrawable?=?null ;?? ????????mCurIndex?=?-1 ;?? ????}?? ????if ?(mEnterAnimationEnd?!=?0 ?||?mExitAnimationEnd?!=?0 )?{?? ????????if ?(mAnimationRunnable?==?null )?{?? ????????????mAnimationRunnable?=?new ?Runnable()?{?? ????????????????@Override ?public ?void ?run()?{?? ????????????????????animate(true );?? ????????????????????invalidateSelf();?? ????????????????}?? ????????????};?? ????????}?else ?{?? ????????????unscheduleSelf(mAnimationRunnable);?? ????????}?? ????????animate(true );?? ????}?? ????invalidateSelf();?? ????return ?true ;?? }??
這里前面的代碼我們可以都不管,關(guān)鍵是要看到在第54行一定會調(diào)用invalidateSelf()方法,這個方法中的代碼如下所示:
[java] view plaincopy
public ?void ?invalidateSelf()?{??????final ?Callback?callback?=?getCallback();?? ????if ?(callback?!=?null )?{?? ????????callback.invalidateDrawable(this );?? ????}?? }??
可以看到,這里會先調(diào)用getCallback()方法獲取Callback接口的回調(diào)實例,然后再去調(diào)用回調(diào)實例的invalidateDrawable()方法。那么這里的回調(diào)實例又是什么呢?觀察一下View的類定義其實你就知道了,如下所示:
[java] view plaincopy
public ?class ?View?implements ?Drawable.Callback,?Drawable.Callback2,?KeyEvent.Callback,??AccessibilityEventSource?{?? ????......?? }??
View類正是實現(xiàn)了
Callback接口,所以剛才其實調(diào)用的就是View中的invalidateDrawable()方法,之后就會按照我們前面分析的流程執(zhí)行重繪邏輯,所以視圖的背景圖才能夠得到改變的。
另外需要注意的是,invalidate()方法雖然最終會調(diào)用到performTraversals()方法中,但這時measure和layout流程是不會重新執(zhí)行的,因為視圖沒有強制重新測量的標志位,而且大小也沒有發(fā)生過變化,所以這時只有draw流程可以得到執(zhí)行。而如果你希望視圖的繪制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而應(yīng)該調(diào)用requestLayout()了。這個方法中的流程比invalidate()方法要簡單一些,但中心思想是差不多的,這里也就不再詳細進行分析了。
這樣的話,我們就將視圖狀態(tài)以及重繪的工作原理都搞清楚了,相信大家對View的理解變得更加深刻了。感興趣的朋友可以繼續(xù)閱讀
總結(jié)
以上是生活随笔 為你收集整理的Android视图状态及重绘流程分析,带你一步步深入了解View(三) 的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔 推薦給好友。