編輯:關於Android編程
經過前面2篇的鋪墊,終於到正式學習 View 的三大流程:測量、布局、繪制流程了,這一篇就先從學習 measure 過程開始吧。
measure 過程要分兩種情況,第一種是 View,第二種是 ViewGroup。如果是 View 的話,那麼只通過 measure 方法就完成其測量過程,但是如果是 ViewGroup 的話,不僅需要完成自己的測量過程,還需要完成它所有子 View 的測量過程。如果子 View 又是一個 ViewGroup,那麼繼續遞歸這個流程。下面先從 View 開始,詳細了解下 View 的 measure 過程。
View 的測量過程是由 View 的 measure 方法來完成的,但是該方法是一個 finall 方法,所以不能被重寫。在 measure 方法中會去調用 onMeasure() 方法,因此我們只需在 View 中重寫 onMeasure() 方法來完成 View 的測量即可。那麼 View 默認的 measure 實現是怎樣的呢? 來看下 View 的 onMeasure() 方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
可以看到,該方法的實現很簡單,直接調用了 setMeasuredDimension() 方法來設置測量的尺寸。關鍵就在於 getDefaultSize() 方法上, 繼續跟進,看看 getDefaultSize() 方法的實現::
public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }
從上述代碼上可以看到,關於我們關心的 AT_MOST 和 EXACTLY 測量模式,其實 getDefaultSize() 方法返回的就是 MeasureSpec 的 specSize。
而這個 MeasureSpec 如果閱讀過上篇文章後,就應該知道是 ViewGroup 傳遞而來的。如果不太了解,建議返回去看下上篇文章,這裡就不重復介紹了。
到這裡也就理解了,為什麼當我們在布局中寫 wrap_content,如果不重寫 onMeasure() 方法,則默認大小是父控件的可用大小了。
當我們在布局中寫 wrap_content 時,那麼測量模式就是: AT_MOST,在該模式下,它的寬高等於 specSize。而 specSize 由 ViewGroup 傳遞過來時就是 parentSize,也就是父控件的可用大小。
當我們在布局中寫 match_parent 時,那麼不用多說,寬高當然也是 parentSize。這時候,我們只需對 AT_MOST 測量模式進行處理:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width = 0; int height = 0; if(widthMode == MeasureSpec.AT_MOST){ width = ... } if(heightMode == MeasureSpec.AT_MOST){ height = ... } setMeasuredDimension(widthMode != MeasureSpec.AT_MOST ? widthSize : width, heightMode != MeasureSpec.AT_MOST? heightSize : height); }
上述代碼,判斷當測量模式是最大模式時,自己計算 View 的寬高。其他情況,直接使用 specSize。
至於 UNSPECIFIED 這種情況,則是使用的第一個參數的值,也就是:getSuggestedMinimumWidth()和getSuggestedMinimumHeight()方法,一般用於系統內部的測量過程。
這兩個方法的源碼如下:
protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); } protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
大概意思就是,判斷 View 有沒有背景,沒有背景的話,那麼值就是 View 最小的寬度或高度,也就是對應 xml 中的:android:minWidth、android:minHeight 屬性,如果屬性沒有指定的話,默認為0。
有背景的話,那麼值就是 View 最小的寬度或高度 和 背景的最小寬度或高度,取兩者中最大的一個值。這個值就是當測量模式是 UNSPECIFIED 時 View 的測量寬/高。
到這裡就完成了整個 View 的 measure 過程,完成之後我們就可以通過 getMeasureWidth() 和 getMeasureHeight() 方法獲取 View 正確的測量寬/高了。但是需要注意的時,在某些極端情況下,系統可能需要再多次 measure 過程後才能確定最終的測量寬/高,在這種情況下,直接在 onMeasure() 方法中獲取的測量寬/高可能是不准確的,保險的做法是在 onLayout() 方法中去獲取。
ViewGroup 的 measure 過程 和 View 不同,不僅需要完成自身的 measure 過程,還需要去遍歷所有子 View 的 measure 方法,各個子元素之間再遞歸這個流程。
ViewGroup 提供了一個叫 measureChildren() 的方法:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < size; ++i) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }
該方法遍歷了所有的子 View,判斷如果子 View 沒有 GONE 掉的時候,就繼續執行 measureChild() 方法:
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
該方法獲取了子 View 的 LayoutParams,然後通過 getChildMeasureSpec() 方法創建了子 View 的 MeasureSpec,至於是怎麼生成的,上一篇關於 MeasureSpec 的文章有寫。
創建好子 View 的 MeasureSpec 後,然後將 MeasureSpec 傳給了子 VIew 進行 View 的 measure 過程。
通過上面的代碼我們可以發現,ViewGroup 並沒有定義其具體的測量過程,這是因為 ViewGroup 是一個抽象類,它測量過程的 onMeasure 方法需要它的子類去實現,比如說像 LinearLayout、RelativeLayout等。
它並不像 View 一樣,對 onMeasure 方法做了統一實現,這是因為它的子類都有不同的布局特性,就像 LinearLayout 和 RelativeLayout 一樣,兩者的布局特性截然不同,沒有辦法做統一實現。
由於 View 的 measure 過程和 Activity 的生命周期不是同步的,那麼如果直接在 Activity 的生命周期方法,如:onCreate() 、onStart()、onResumt() 中直接獲取 View 的寬/高是無法正確獲取到的。
因為沒辦法保證當走這些生命周期回調方法前,View 的 measure 過程已經走完。如果沒有走完就直接獲取的話,那麼得到的只會是 0。下面給出幾種解決方法:
@Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if(hasFocus){ int measuredWidth = view.getMeasuredWidth(); int measuredHeight = view.getMeasuredHeight(); } }
該方法會在當前 Activity 的 Window 獲得或失去焦點的時候回調,當回調該方法時,表示 Activtiy 是完全對用戶可見的,這時候 View 已經初始化完畢、寬/高都已經測量好了,這時就能獲取到寬/高了。
方案2:view.post(new Runnable() { @Override public void run() { int measuredWidth = view.getMeasuredWidth(); int measuredHeight = view.getMeasuredHeight(); } });
該方案,通過 post 方法將一個 runnable 投遞到消息隊列的底部,然後等待 Looper 調用該 runnable 時,View 也已經初始化好了,這時就能獲取到寬/高了。
方案3:ViewTreeObserver treeObserver = view.getViewTreeObserver(); treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { view.getViewTreeObserver().removeGlobalOnLayoutListener(this); int measuredWidth = view.getMeasuredWidth(); int measuredHeight = view.getMeasuredHeight(); } });
該方案,通過監聽 View 樹的狀態發生改變或者 View 樹內部的 View 可見性發生改變時,在 onGlobalLayout 回調中獲取 View 的寬/高。需要注意的時,該回調會被調用多次,所以這裡在第一次回調中,就移除了監聽,避免多次獲取。
查看Android應用(apk)簽名 在微博、微信開放平台注冊應用時,需要填寫應用(apk)的簽名,可以使用keytool工具找
一.包引入dependencies { compile fileTree(dir: 'libs', include: ['*.jar'
自Android 5.0之後,谷歌公司推出了RecylerView控件,RecylerView,我想看到一個新名詞後大部分人會首先發出一個疑問,recylerview是什
因項目需求,做一個有關wifi的Demo,現已經上傳到GitHub 上面地址:https://github.com/git-xuhao/WifiDemo源碼片段packa