android基礎-view的測量,佈局,繪製
知識點
- view的測量
- view的佈局
- view的繪製
android中的view顯示方式主要就是測量出大小→決定在哪個位置→最後進行繪製
一、view的測量
view的測量是通過強大的MeasureSpec類幫助測量的,而關於該類起初我們只要瞭解 它是一個32位的int值,其中高2位是用於標識當前view的測量模式,低30位就是用於記錄view的大小 。更多關於該類的知識可以檢視官方文件 MeasureSpec
view的測量模式有三種:
- EXACTLY : 就是當我們指定view的大小或者使用適配父控制元件的情況 例如:
android:layout_width="100dp" /android:layout_width="match_parent"
- AT_MOST : 該模式一般是控制元件適應自身內容大小,但不能超過父控制元件的大小 例如:
android:layout_width="wrap_content"
- UNSPECIFIED : 該模式標識不限制view的大小,要多大就多大,一般在自定義view的時候使用
在view的測量中,系統預設的大小計算方法如下(API 27):
/** * Utility to return a default size. Uses the supplied size if the * MeasureSpec imposed no constraints. Will get larger if allowed * by the MeasureSpec. * * @param size Default size for this view * @param measureSpec Constraints imposed by the parent * @return The size this view should be. */ 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; }
可以看出,預設的測量方式為,如果測量模式是UNSPECIFIED ,則採用系統預設大小,其餘為measureSpec中所測量的大小,根據這個測量思路我們在自定義view的時候就可以採用自己的測量方式
舉個例子:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int resultWidth; int resultHeight; int specWMode = MeasureSpec.getMode(widthMeasureSpec); int specWSize = MeasureSpec.getSize(widthMeasureSpec); resultWidth=myMeasure(specWMode,specWSize, Dp2PxUtil.dip2px(mContext,200)); int specHMode = MeasureSpec.getMode(heightMeasureSpec); int specHSize = MeasureSpec.getSize(heightMeasureSpec); resultHeight=myMeasure(specHMode,specHSize,Dp2PxUtil.dip2px(mContext,300)); setMeasuredDimension(resultWidth,resultHeight); } /** * * @param specMode 測量模式 * @param specSize 測量大小 * @param result在非精確測量模式中用來約束的大小 * @return */ privateint myMeasure(int specMode,int specSize,int result){ if(specMode==MeasureSpec.EXACTLY){ result=specSize; }else if(specMode==MeasureSpec.AT_MOST){ result=Math.min(specSize,result); }else { } return result; }
測試結果:
結果 | 備註 |
---|---|
![]() 指定大小為100dp*100dp.png |
設定自定義view的寬高為100dp*100dp |
![]() 填充父佈局.png |
設定自定義view的寬高為match_parent |
![]() 自適應.png |
設定自定義view的寬高為wrap_content(實際根據我們的測量方法設定的是寬高為200dp*300dp) |
![]() 不重寫.png |
如果沒有重寫自定義view中的onMeasure方法,並且設定寬高為wrap_content的時候,也是填充父佈局,具體原因可以檢視系統測量預設大小的原始碼 |
二、view的佈局
與view佈局相關主要就有兩個方法:
layout(int l, int t, int r, int b) :
onLayout(boolean changed, int left, int top, int right, int bottom) :
2.1 onLayout
該方法在自定義view中一般不需用重寫,其常用viewgroup通知子view進行佈局Layout的時候使用
2.2 layout
view中原始碼如下(API 27)
public void layout(int l, int t, int r, int b) { if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); if (shouldDrawRoundScrollbar()) { if(mRoundScrollbarRenderer == null) { mRoundScrollbarRenderer = new RoundScrollbarRenderer(this); } } else { mRoundScrollbarRenderer = null; } mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) { mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT; notifyEnterOrExitForAutoFillIfNeeded(true); } }
我們不必要知道該方法的全部流程以及作用,主要看下面這行程式碼:
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
這行程式碼涉及了三個方法isLayoutModeOptical,setOpticalFrame,setFrame。isLayoutModeOptical看註釋說是判斷是否使用光學邊界,這個我們也可以不管, 主要看setOpticalFrame,setFrame這兩個方法
我們先看setOpticalFrame:
private boolean setOpticalFrame(int left, int top, int right, int bottom) { Insets parentInsets = mParent instanceof View ? ((View) mParent).getOpticalInsets() : Insets.NONE; Insets childInsets = getOpticalInsets(); return setFrame( left+ parentInsets.left - childInsets.left, top+ parentInsets.top- childInsets.top, right+ parentInsets.left + childInsets.right, bottom + parentInsets.top+ childInsets.bottom); }
可以看到最終return的方法也是setFrame,那麼我們就往這方法裡面看看
protected boolean setFrame(int left, int top, int right, int bottom) { boolean changed = false; if (DBG) { Log.d("View", this + " View.setFrame(" + left + "," + top + "," + right + "," + bottom + ")"); } if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { changed = true; // Remember our drawn bit int drawn = mPrivateFlags & PFLAG_DRAWN; int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; int newWidth = right - left; int newHeight = bottom - top; boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight); // Invalidate our old position invalidate(sizeChanged); mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); mPrivateFlags |= PFLAG_HAS_BOUNDS; if (sizeChanged) { sizeChange(newWidth, newHeight, oldWidth, oldHeight); } if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) { // If we are visible, force the DRAWN bit to on so that // this invalidate will go through (at least to our parent). // This is because someone may have invalidated this view // before this call to setFrame came in, thereby clearing // the DRAWN bit. mPrivateFlags |= PFLAG_DRAWN; invalidate(sizeChanged); // parent display list may need to be recreated based on a change in the bounds // of any child invalidateParentCaches(); } // Reset drawn bit to original value (invalidate turns it off) mPrivateFlags |= drawn; mBackgroundSizeChanged = true; mDefaultFocusHighlightSizeChanged = true; if (mForegroundInfo != null) { mForegroundInfo.mBoundsChanged = true; } notifySubtreeAccessibilityStateChangedIfNeeded(); } return changed; }
這個方法我們也不用全部理解,主要看到它進行了當前位置引數和傳進來的位置引數是否相等,如果不相等就進行重新繪製,看到這裡, 我們就知道了layout()方法如果傳進去的位置和之前的位置引數不一樣就會重新繪製該view,那麼在viewgroup需要定位子view位置的時候,我們就可以呼叫每個子view的layout方法來重新給子view設定位置
三、view的繪製
當有了view的大小以及view的位置資訊之後,我們就可以在螢幕上繪製該view了, view的繪製比較簡單,我們可以直接重寫onDraw方法
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); }
該方法會傳遞一個Canvas(畫布)過來,我們只需要建立一個Paint(畫筆)之類的在該畫布上進行繪製就可以了
總結
view的測量,佈局,繪製是基礎中比較重要的,因為後續的一些複雜的特效,動畫都可以由自定義view去實現,理解清楚其基本的繪製流程對後續開發會很有幫助
參考文章
《Android群英傳》