作者:guolin
來源:CSDN
原文:https://blog.csdn.net/guolin_blog/article/details/12921889
版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請附上博文鏈接!
作為對View進行學(xué)習的第三篇文章,本文將講解一下視圖狀態(tài)以及重繪方面的知識。
相信大家在平時使用View的時候都會發(fā)現(xiàn)它是有狀態(tài)的,比如說有一個按鈕,普通狀態(tài)下是一種效果,但是當手指按下的時候就會變成另外一種效果,這樣才會給人產(chǎn)生一種點擊了按鈕的感覺。當然了,這種效果相信幾乎所有的Android程序員都知道該如何實現(xiàn),但是我們既然是深入了解View,那么自然也應(yīng)該知道它背后的實現(xiàn)原理應(yīng)該是什么樣的,今天就讓我們來一起探究一下吧。
1. 視圖狀態(tài)
視圖狀態(tài)的種類非常多,一共有十幾種類型,不過多數(shù)情況下我們只會使用到其中的幾種,因此這里我們也就只去分析最常用的幾種視圖狀態(tài)。
-
enabled
表示當前視圖是否可用??梢哉{(diào)用setEnable()方法來改變視圖的可用狀態(tài),傳入true表示可用,傳入false表示不可用。它們之間最大的區(qū)別在于,不可用的視圖是無法響應(yīng)onTouch事件的。
-
focused
表示當前視圖是否獲得到焦點。通常情況下有兩種方法可以讓視圖獲得焦點,即通過鍵盤的上下左右鍵切換視圖,以及調(diào)用requestFocus()方法。而現(xiàn)在的Android手機幾乎都沒有鍵盤了,因此基本上只可以使用requestFocus()這個辦法來讓視圖獲得焦點了。而requestFocus()方法也不能保證一定可以讓視圖獲得焦點,它會有一個布爾值的返回值,如果返回true說明獲得焦點成功,返回false說明獲得焦點失敗。一般只有視圖在focusable和focusable in touch mode同時成立的情況下才能成功獲取焦點,比如說EditText。
-
window_focused
表示當前視圖是否處于正在交互的窗口中,這個值由系統(tǒng)自動決定,應(yīng)用程序不能進行改變。
-
selected
表示當前視圖是否處于選中狀態(tài)。一個界面當中可以有多個視圖處于選中狀態(tài),調(diào)用setSelected()方法能夠改變視圖的選中狀態(tài),傳入true表示選中,傳入false表示未選中。
-
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文件,在里面編寫如下代碼:
<?xml version="1.0" encoding="utf-8"?>
<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è)置為某個按鈕的背景圖,如下所示:
<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:layout_width="60dp"
android:layout_height="40dp"
android:id="@+id/compose"
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()方法,代碼如下所示:
protected void drawableStateChanged() {
Drawable d = mBGDrawable;
if (d != null && d.isStateful()) {
d.setState(getDrawableState());
}
}
在這里的第一步,首先是將mBGDrawable賦值給一個Drawable對象,那么這個mBGDrawable是什么呢?觀察setBackgroundResource()方法中的代碼,如下所示:
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),代碼如下所示:
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)進行更新,代碼如下所示:
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()方法,那么我們趕快看一下吧:
@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(xué)的繪制流程的,很顯然,背景圖的繪制是在draw()方法中完成的,那么為什么selectDrawable()方法能夠控制背景圖的改變呢?這就要研究一下視圖重繪的流程了。
2. 視圖重繪
雖然視圖會在Activity加載完成之后自動繪制到屏幕上,但是我們完全有理由在與Activity進行交互的時候要求動態(tài)更新視圖,比如改變視圖的狀態(tài)、以及顯示或隱藏某個控件等。那在這個時候,之前繪制出的視圖其實就已經(jīng)過期了,此時我們就應(yīng)該對視圖進行重繪。
調(diào)用視圖的setVisibility()、setEnabled()、setSelected()等方法時都會導(dǎo)致視圖重繪,而如果我們想要手動地強制讓視圖進行重繪,可以調(diào)用invalidate()方法來實現(xiàn)。當然了,setVisibility()、setEnabled()、setSelected()等方法的內(nèi)部其實也是通過調(diào)用invalidate()方法來實現(xiàn)的,那么就讓我們來看一看invalidate()方法的代碼是什么樣的吧。
View的源碼中會有數(shù)個invalidate()方法的重載和一個invalidateDrawable()方法,當然它們的原理都是相同的,因此我們只分析其中一種,代碼如下所示:
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()方法中,代碼如下所示:
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()方法了,代碼如下所示:
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
invalidateChild(null, dirty);
return null;
}
這里的代碼非常簡單,僅僅是去調(diào)用了invalidateChild()方法而已,那我們再跟進去瞧一瞧吧:
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ù)跟進:
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()方法的代碼吧,如下所示:
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()方法,這不就是我們在前面一篇文章中學(xué)到的視圖繪制的入口嗎?雖然經(jīng)過了很多輾轉(zhuǎn)的調(diào)用,但是可以確定的是,調(diào)用視圖的invalidate()方法后確實會走到performTraversals()方法中,然后重新執(zhí)行繪制流程。之后的流程就不需要再進行描述了吧,可以參考 跟著郭大俠一步步深入了解View第二篇之Android視圖繪制流程完全解析 這一篇文章。
了解了這些之后,我們再回過頭來看看剛才的selectDrawable()方法中到底做了什么才能夠控制背景圖的改變,代碼如下所示:
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()方法,這個方法中的代碼如下所示:
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的類定義其實你就知道了,如下所示:
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的理解變得更加深刻了。
附:第一篇:Android LayoutInflater原理分析
附:第二篇:Android視圖繪制流程完全解析
作者:guolin
來源:CSDN
原文:https://blog.csdn.net/guolin_blog/article/details/17045157
版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請附上博文鏈接!