1. 程式人生 > >Android檢視狀態及重繪流程分析,帶你一步步深入瞭解View(三)

Android檢視狀態及重繪流程分析,帶你一步步深入瞭解View(三)

在前面一篇文章中,我帶著大家一起從原始碼的層面上分析了檢視的繪製流程,瞭解了檢視繪製流程中onMeasure、onLayout、onDraw這三個最重要步驟的工作原理,那麼今天我們將繼續對View進行深入探究,學習一下檢視狀態以及重繪方面的知識。如果你還沒有看過我前面一篇文章,可以先去閱讀 Android檢視繪製流程完全解析,帶你一步步深入瞭解View(二) 。

相信大家在平時使用View的時候都會發現它是有狀態的,比如說有一個按鈕,普通狀態下是一種效果,但是當手指按下的時候就會變成另外一種效果,這樣才會給人產生一種點選了按鈕的感覺。當然了,這種效果相信幾乎所有的Android程式設計師都知道該如何實現,但是我們既然是深入瞭解View,那麼自然也應該知道它背後的實現原理應該是什麼樣的,今天就讓我們來一起探究一下吧。

一、檢視狀態

檢視狀態的種類非常多,一共有十幾種類型,不過多數情況下我們只會使用到其中的幾種,因此這裡我們也就只去分析最常用的幾種檢視狀態。

1. enabled

表示當前檢視是否可用。可以呼叫setEnable()方法來改變檢視的可用狀態,傳入true表示可用,傳入false表示不可用。它們之間最大的區別在於,不可用的檢視是無法響應onTouch事件的。

2. focused

表示當前檢視是否獲得到焦點。通常情況下有兩種方法可以讓檢視獲得焦點,即通過鍵盤的上下左右鍵切換檢視,以及呼叫requestFocus()方法。而現在的Android手機幾乎都沒有鍵盤了,因此基本上只可以使用requestFocus()這個辦法來讓檢視獲得焦點了。而requestFocus()方法也不能保證一定可以讓檢視獲得焦點,它會有一個布林值的返回值,如果返回true說明獲得焦點成功,返回false說明獲得焦點失敗。一般只有檢視在focusable和focusable in touch mode同時成立的情況下才能成功獲取焦點,比如說EditText。

3. window_focused

表示當前檢視是否處於正在互動的視窗中,這個值由系統自動決定,應用程式不能進行改變。

4. selected

表示當前檢視是否處於選中狀態。一個介面當中可以有多個檢視處於選中狀態,呼叫setSelected()方法能夠改變檢視的選中狀態,傳入true表示選中,傳入false表示未選中。

5. pressed

表示當前檢視是否處於按下狀態。可以呼叫setPressed()方法來對這一狀態進行改變,傳入true表示按下,傳入false表示未按下。通常情況下這個狀態都是由系統自動賦值的,但開發者也可以自己呼叫這個方法來進行改變。

我們可以在專案的drawable目錄下建立一個selector檔案,在這裡配置每種狀態下檢視對應的背景圖片。比如建立一個compose_bg.xml檔案,在裡面編寫如下程式碼:

<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>
這段程式碼就表示,當檢視處於正常狀態的時候就顯示compose_normal這張背景圖,當檢視獲得到焦點或者被按下的時候就顯示compose_pressed這張背景圖。

建立好了這個selector檔案後,我們就可以在佈局或程式碼中使用它了,比如將它設定為某個按鈕的背景圖,如下所示:

<?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>

現在執行一下程式,這個按鈕在普通狀態和按下狀態的時候就會顯示不同的背景圖片,如下圖所示:


這樣我們就用一個非常簡單的方法實現了按鈕按下的效果,但是它的背景原理到底是怎樣的呢?這就又要從原始碼的層次上進行分析了。

我們都知道,當手指按在檢視上的時候,檢視的狀態就已經發生了變化,此時檢視的pressed狀態是true。每當檢視的狀態有發生改變的時候,就會回撥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行呼叫了Resource的getDrawable()方法將resid轉換成了一個Drawable物件,然後呼叫了setBackgroundDrawable()方法並將這個Drawable物件傳入,在setBackgroundDrawable()方法中會將傳入的Drawable物件賦值給mBGDrawable。

而我們在佈局檔案中通過android:background屬性指定的selector檔案,效果等同於呼叫setBackgroundResource()方法。也就是說drawableStateChanged()方法中的mBGDrawable物件其實就是我們指定的selector檔案。

接下來在drawableStateChanged()方法的第4行呼叫了getDrawableState()方法來獲取檢視狀態,程式碼如下所示:

public final int[] getDrawableState() {
    if ((mDrawableState != null) && ((mPrivateFlags & DRAWABLE_STATE_DIRTY) == 0)) {
        return mDrawableState;
    } else {
        mDrawableState = onCreateDrawableState(0);
        mPrivateFlags &= ~DRAWABLE_STATE_DIRTY;
        return mDrawableState;
    }
}

在這裡首先會判斷當前檢視的狀態是否發生了改變,如果沒有改變就直接返回當前的檢視狀態,如果發生了改變就呼叫onCreateDrawableState()方法來獲取最新的檢視狀態。檢視的所有狀態會以一個整型陣列的形式返回。

在得到了檢視狀態的陣列之後,就會呼叫Drawable的setState()方法來對狀態進行更新,程式碼如下所示:

public boolean setState(final int[] stateSet) {
    if (!Arrays.equals(mStateSet, stateSet)) {
        mStateSet = stateSet;
        return onStateChange(stateSet);
    }
    return false;
}
這裡會呼叫Arrays.equals()方法來判斷檢視狀態的陣列是否發生了變化,如果發生了變化則呼叫onStateChange()方法,否則就直接返回false。但你會發現,Drawable的onStateChange()方法中其實就只是簡單返回了一個false,並沒有任何的邏輯處理,這是為什麼呢?這主要是因為mBGDrawable物件是通過一個selector檔案創建出來的,而通過這種檔案創建出來的Drawable物件其實都是一個StateListDrawable例項,因此這裡呼叫的onStateChange()方法實際上呼叫的是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);
}

可以看到,這裡會先呼叫indexOfStateSet()方法來找到當前檢視狀態所對應的Drawable資源下標,然後在第9行呼叫selectDrawable()方法並將下標傳入,在這個方法中就會將檢視的背景圖設定為當前檢視狀態所對應的那張圖片了。

那你可能會有疑問,在前面一篇文章中我們說到,任何一個檢視的顯示都要經過非常科學的繪製流程的,很顯然,背景圖的繪製是在draw()方法中完成的,那麼為什麼selectDrawable()方法能夠控制背景圖的改變呢?這就要研究一下檢視重繪的流程了。

二、檢視重繪

雖然檢視會在Activity載入完成之後自動繪製到螢幕上,但是我們完全有理由在與Activity進行互動的時候要求動態更新檢視,比如改變檢視的狀態、以及顯示或隱藏某個控制元件等。那在這個時候,之前繪製出的檢視其實就已經過期了,此時我們就應該對檢視進行重繪。

呼叫檢視的setVisibility()、setEnabled()、setSelected()等方法時都會導致檢視重繪,而如果我們想要手動地強制讓檢視進行重繪,可以呼叫invalidate()方法來實現。當然了,setVisibility()、setEnabled()、setSelected()等方法的內部其實也是通過呼叫invalidate()方法來實現的,那麼就讓我們來看一看invalidate()方法的程式碼是什麼樣的吧。

View的原始碼中會有數個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);
        }
    }
}
在這個方法中首先會呼叫skipInvalidate()方法來判斷當前View是否需要重繪,判斷的邏輯也比較簡單,如果View是不可見的且沒有執行任何動畫,就認為不需要重繪了。之後會進行透明度的判斷,並給View新增一些標記位,然後在第22和29行呼叫ViewParent的invalidateChild()方法,這裡的ViewParent其實就是當前檢視的父檢視,因此會呼叫到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迴圈,當ViewParent不等於空的時候就會一直迴圈下去。在這個while迴圈當中會不斷地獲取當前佈局的父佈局,並呼叫它的invalidateChildInParent()方法,在ViewGroup的invalidateChildInParent()方法中主要是來計算需要重繪的矩形區域,這裡我們先不管它,當迴圈到最外層的根佈局後,就會呼叫ViewRoot的invalidateChildInParent()方法了,程式碼如下所示:
    public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
        invalidateChild(null, dirty);
        return null;
    }
這裡的程式碼非常簡單,僅僅是去呼叫了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行又呼叫了scheduleTraversals()這個方法,那麼我們繼續跟進:
public void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        sendEmptyMessage(DO_TRAVERSAL);
    }
}
可以看到,這裡呼叫了sendEmptyMessage()方法,並傳入了一個DO_TRAVERSAL引數。瞭解Android非同步訊息處理機制的朋友們都會知道,任何一個Handler都可以呼叫sendEmptyMessage()方法來發送訊息,並且在handleMessage()方法中接收訊息,而如果你看一下ViewRoot的類定義就會發現,它是繼承自Handler的,也就是說這裡呼叫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;
    ......
}

熟悉的程式碼出現了!這裡在第7行呼叫了performTraversals()方法,這不就是我們在前面一篇文章中學到的檢視繪製的入口嗎?雖然經過了很多輾轉的呼叫,但是可以確定的是,呼叫檢視的invalidate()方法後確實會走到performTraversals()方法中,然後重新執行繪製流程。之後的流程就不需要再進行描述了吧,可以參考 Android檢視繪製流程完全解析,帶你一步步深入瞭解View(二) 這一篇文章。

瞭解了這些之後,我們再回過頭來看看剛才的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;
}
這裡前面的程式碼我們可以都不管,關鍵是要看到在第54行一定會呼叫invalidateSelf()方法,這個方法中的程式碼如下所示:
public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}
可以看到,這裡會先呼叫getCallback()方法獲取Callback介面的回撥例項,然後再去呼叫回撥例項的invalidateDrawable()方法。那麼這裡的回撥例項又是什麼呢?觀察一下View的類定義其實你就知道了,如下所示:
public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Callback,
AccessibilityEventSource {
    ......
}

View類正是實現了Callback介面,所以剛才其實呼叫的就是View中的invalidateDrawable()方法,之後就會按照我們前面分析的流程執行重繪邏輯,所以檢視的背景圖才能夠得到改變的。

另外需要注意的是,invalidate()方法雖然最終會呼叫到performTraversals()方法中,但這時measure和layout流程是不會重新執行的,因為檢視沒有強制重新測量的標誌位,而且大小也沒有發生過變化,所以這時只有draw流程可以得到執行。而如果你希望檢視的繪製流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而應該呼叫requestLayout()了。這個方法中的流程比invalidate()方法要簡單一些,但中心思想是差不多的,這裡也就不再詳細進行分析了。

這樣的話,我們就將檢視狀態以及重繪的工作原理都搞清楚了,相信大家對View的理解變得更加深刻了。感興趣的朋友可以繼續閱讀 Android自定義View的實現方法,帶你一步步深入瞭解View(四) 。

關注我的技術公眾號,每天都有優質技術文章推送。關注我的娛樂公眾號,工作、學習累了的時候放鬆一下自己。

微信掃一掃下方二維碼即可關注: