跟著郭大俠一步步深入了解View第三篇之Android視圖狀態(tài)及重繪流程分析

作者: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)的時候就會顯示不同的背景圖片,如下圖所示:


20190521_091141.gif

這樣我們就用一個非常簡單的方法實現(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)載請附上博文鏈接!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容