編輯:關於Android編程
在前面一篇文章中,我帶著大家一起從源碼的層面上分析了視圖的繪制流程,了解了視圖繪制流程中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文件,在裡面編寫如下代碼:
這段代碼就表示,當視圖處於正常狀態的時候就顯示compose_normal這張背景圖,當視圖獲得到焦點或者被按下的時候就顯示compose_pressed這張背景圖。
創建好了這個selector文件後,我們就可以在布局或代碼中使用它了,比如將它設置為某個按鈕的背景圖,如下所示:
現在運行一下程序,這個按鈕在普通狀態和按下狀態的時候就會顯示不同的背景圖片,如下圖所示:
這樣我們就用一個非常簡單的方法實現了按鈕按下的效果,但是它的背景原理到底是怎樣的呢?這就又要從源碼的層次上進行分析了。
我們都知道,當手指按在視圖上的時候,視圖的狀態就已經發生了變化,此時視圖的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的理解變得更加深刻了。
1. Java知識儲備本知識點不做重點講解:對於有基礎的同學推薦看《Java編程思想》,鞏固基礎,查漏補全,了解並熟悉更多細節知識點。對於沒有基礎的同學推薦看一本Java
C++ LooperLooperLooper類[system/core/libutils/Looper.cpp]提供了pollOnce(),wake()函數來完成睡眠等待
現在很多安全類的軟件,比如360手機助手,百度手機助手等等,都有一個懸浮窗,可以飄浮在桌面上,方便用戶使用一些常用的操作。今天這篇文章,就是介紹如何實現桌面懸浮窗效果的。
紅米note3介紹:外觀設計紅米Note3金屬機身背面是三段式設計,上下兩端為塑料材質。配置方面紅米Note3采用5.5英寸1080P屏幕,1300萬像素後