編輯:關於Android編程
本文通過13問13答學習Android View繪制,供大家參考,具體內容如下
1.View的繪制流程分幾步,從哪開始?哪個過程結束以後能看到view?
答:從ViewRoot的performTraversals開始,經過measure,layout,draw 三個流程。draw流程結束以後就可以在屏幕上看到view了。
2.view的測量寬高和實際寬高有區別嗎?
答:基本上百分之99的情況下都是可以認為沒有區別的。有兩種情況,有區別。第一種 就是有的時候會因為某些原因 view會多次測量,那第一次測量的寬高 肯定和最後實際的寬高 是不一定相等的,但是在這種情況下
最後一次測量的寬高和實際寬高是一致的。此外,實際寬高是在layout流程裡確定的,我們可以在layout流程裡 將實際寬高寫死 寫成硬編碼,這樣測量的寬高和實際寬高就肯定不一樣了,雖然這麼做沒有意義 而且也不好。
3.view的measureSpec 由誰決定?頂級view呢?
答:由view自己的layoutparams和父容器 一起決定自己的measureSpec。一旦確定了spec,onMeasure中就可以確定view的寬高了。
頂級view就稍微特殊一點,對於decorView的測量在ViewRootImpl的源碼裡。
//desire的這2個參數就代表屏幕的寬高, childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); //decorView的measureSpec就是在這裡確定的,其實比普通view的measurespec要簡單的多 //代碼就不分析了 一目了然的東西 private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; }
4.對於普通view來說,他的measure過程中,與父view有關嗎?如果有關,這個父view也就是viewgroup扮演了什麼角色?
答:看源碼:
//對於普通view的measure來說 是由這個view的 父view ,也就是viewgroup來觸發的。 //也就是下面這個measureChildWithMargins方法 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { //第一步 先取得子view的 layoutParams 參數值 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); //然後開始計算子view的spec的值,注意這裡看到 計算的時候除了要用子view的 layoutparams參數以外 //還用到了父view 也就是viewgroup自己的spec的值 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } //這個算view的spec的方法 看上去一大串 但是真的邏輯非常簡單 就是根據父親viewgroup //的meaurespec 同時還有view自己的params來確定 view自己的measureSpec。 //注意這裡的參數是padding,這個值的含義是 父容器已占用的控件的大小 所以view的Specsize //的值 你們可以看到 是要減去這個padding的值的。總大小-已經用的 =可用的。 很好理解。 //然後就是下面的switch邏輯 要自己梳理清楚。其實也不難,主要是下面幾條原則 //如果view采用固定寬高,也就是寫死的數值那種。那就不管父親的spec的值了,view的spec 就肯定是exactly 並且大小遵循layout參數裡設置的大小。 //如果view的寬高是match_parent ,那麼就要看父容器viewgroup的 spec的值了,如果父view的spec是exactly模式, //那view也肯定是exactly,並且大小就是父容器剩下的空間。如果父容器是at_most模式,那view也是at_most 並且不會超過剩余空間大小 //如果view的寬高是wrap_content, 那就不管父容器的spec了,view的spec一定是at_most 並且不會超過父view 剩余空間的大小。 public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
5.view的meaure和onMeasure有什麼關系?
答:看源碼:
//view的measure是final 方法 我們子類無法修改的。 public final void measure(int widthMeasureSpec, int heightMeasureSpec) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int oWidth = insets.left + insets.right; int oHeight = insets.top + insets.bottom; widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); } // Suppress sign extension for the low bytes long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // first clears the measured dimension flag mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } else { long value = mMeasureCache.valueAt(cacheIndex); // Casting a long to int drops the high 32 bits, no mask needed setMeasuredDimensionRaw((int) (value >> 32), (int) value); mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException("View with id " + getId() + ": " + getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension } //不過可以看到的是在measure方法裡調用了onMeasure方法 //所以就能知道 我們在自定義view的時候一定是重寫這個方法! protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
6.簡要分析view的measure流程?
答:先回顧問題4,viewgroup 算出子view的spec以後 會調用子view的measure方法,而子view的measure方法 我們問題5也看過了實際上是調用的onMeasure方法
所以我們只要分析好onMeasure方法即可,注意onMeasure方法的參數 正是他的父view算出來的那2個spec的值(這裡view的measure方法會把這個spec裡的specSize值做略微的修改 這個部分 不做分析 因為measure方法修改specSize的部分很簡單)。
//可以看出來這個就是setMeasuredDimension方法的調用 這個方法看名字就知道就是確定view的測量寬高的 //所以我們分析的重點就是看這個getDefaultSize 方法 是怎麼確定view的測量寬高的 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } //這個方法特別簡單 基本可以認為就是近似的返回spec中的specSize,除非你的specMode是UNSPECIFIED //UNSPECIFIED 這個一般都是系統內部測量才用的到,這種時候返回size 也就是getSuggestedMinimumWidth的返回值 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; } //跟view的背景相關 這裡不多做分析了 protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
7.自定義view中 如果onMeasure方法 沒有對wrap_content 做處理 會發生什麼?為什麼?怎麼解決?
答:如果沒有對wrap_content做處理 ,那即使你在xml裡設置為wrap_content.其效果也和match_parent相同。看問題4的分析。我們可以知道view自己的layout為wrap,那mode就是at_most(不管父親view是什麼specmode).
這種模式下寬高就是等於specSize(getDefaultSize函數分析可知),而這裡的specSize顯然就是parentSize的大小。也就是父容器剩余的大小。那不就和我們直接設置成match_parent是一樣的效果了麼?
解決方式就是在onMeasure裡 針對wrap 來做特殊處理 比如指定一個默認的寬高,當發現是wrap_content 就設置這個默認寬高即可。
8.ViewGroup有onMeasure方法嗎?為什麼?
答:沒有,這個方法是交給子類自己實現的。不同的viewgroup子類 肯定布局都不一樣,那onMeasure索性就全部交給他們自己實現好了。
9.為什麼在activity的生命周期裡無法獲得測量寬高?有什麼方法可以解決這個問題嗎?
答:因為measure的過程和activity的生命周期 沒有任何關系。你無法確定在哪個生命周期執行完畢以後 view的measure過程一定走完。可以嘗試如下幾種方法 獲取view的測量寬高。
//重寫activity的這個方法 public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { int width = tv.getMeasuredWidth(); int height = tv.getMeasuredHeight(); Log.v("burning", "width==" + width); Log.v("burning", "height==" + height); } }
或者重寫這個方法
@Override protected void onStart() { super.onStart(); tv.post(new Runnable() { @Override public void run() { int width = tv.getMeasuredWidth(); int height = tv.getMeasuredHeight(); } }); }
再或者:
@Override protected void onStart() { super.onStart(); ViewTreeObserver observer = tv.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { int width = tv.getMeasuredWidth(); int height = tv.getMeasuredHeight(); tv.getViewTreeObserver().removeOnGlobalLayoutListener(this); } }); }
10.layout和onLayout方法有什麼區別?
答:layout是確定本身view的位置 而onLayout是確定所有子元素的位置。layout裡面 就是通過serFrame方法設設定本身view的 四個頂點的位置。這4個位置以確定 自己view的位置就固定了
然後就調用onLayout來確定子元素的位置。view和viewgroup的onlayout方法都沒有寫。都留給我們自己給子元素布局
11.draw方法 大概有幾個步驟?
答: 一共是4個步驟, 繪制背景---------繪制自己--------繪制chrildren----繪制裝飾。
12.setWillNotDraw方法有什麼用?
答:這個方法在view裡。
/** * If this view doesn't do any drawing on its own, set this flag to * allow further optimizations. By default, this flag is not set on * View, but could be set on some View subclasses such as ViewGroup. * * Typically, if you override {@link #onDraw(android.graphics.Canvas)} * you should clear this flag. * * @param willNotDraw whether or not this View draw on its own */ public void setWillNotDraw(boolean willNotDraw) { setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); }
用於設置標志位的 也就是說 如果你的自定義view 不需要draw的話,就可以設置這個方法為true。這樣系統知道你這個view 不需要draw 可以優化執行速度。viewgroup 一般都默認設置這個為true,因為viewgroup多數都是只負責布局
不負責draw的。而view 這個標志位 默認一般都是關閉的。
13.自定義view 有哪些需要注意的點?
答:主要是要處理wrap_content 和padding。否則xml 那邊設置這2個屬性就根本沒用了。還有不要在view中使用handler 因為人家已經提供了post方法。如果是繼承自viewGroup,那在onMeasure和onLayout裡面 也要考慮
padding和layout的影響。也就是說specSize 要算一下 。最後就是如果view的動畫或者線程需要停止,可以考慮在onDetachedFromWindow裡面來做。
針對上述的幾點,給出幾個簡單的自定義view 供大家理解。
給出一個圓形的view 范例:
package com.example.administrator.motioneventtest; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; /** * Created by Administrator on 2016/2/4. */ public class CircleView extends View { private int mColor = Color.RED; private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private void init() { mPaint.setColor(mColor); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); //處理為wrap_content時的情況 if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(200, 200); } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(200, heightSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, 200); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //處理padding的情況 final int paddingLeft = getPaddingLeft(); final int paddingRight = getPaddingRight(); final int paddingTop = getPaddingTop(); final int paddingBottom = getPaddingBottom(); int width = getWidth() - paddingLeft - paddingRight; int height = getHeight() - paddingTop - paddingBottom; int radius = Math.min(width, height) / 2; canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint); } public CircleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } public CircleView(Context context) { super(context); init(); } public CircleView(Context context, AttributeSet attrs) { super(context, attrs); init(); } }
然後下面再給出一個范例,稍微復雜一點是自定義viewgroup了(主要是加強對onMeasure和onLayout的理解), 需求如下:
一個水平的viewgroup,內部的子元素 為了簡單 我們假定他們的寬高都是一樣的。來寫一個這樣的簡單的viewgroup。
package com.example.administrator.motioneventtest; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; /** * Created by Administrator on 2016/2/4. */ //這裡我們只處理了padding的狀態 沒有處理margin的狀態,子view的margin 對measure和layout的影響 //就留給讀者自己完成了 public class CustomHorizontalLayout extends ViewGroup { //設置默認的控件最小是多少 這裡不提供自定義屬性了 寫死在代碼裡 你們可以自行拓展 final int minHeight = 0; final int minWidth = 0; public CustomHorizontalLayout(Context context) { super(context); } public CustomHorizontalLayout(Context context, AttributeSet attrs) { super(context, attrs); } public CustomHorizontalLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth = 0; int measureHeight = 0; final int childCount = getChildCount(); measureChildren(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); final View childView = getChildAt(0); final int paddingLeft = getPaddingLeft(); final int paddingRight = getPaddingRight(); final int paddingTop = getPaddingTop(); final int paddingBottom = getPaddingBottom(); //沒有子控件 時 我們的寬高要作特殊處理 if (childCount == 0) { //當沒有子控件時,如果長寬有一個為wrap 那麼就讓這個控件以最小的形式展現 //這裡我們最小設置為0 if (widthSpecMode == MeasureSpec.AT_MOST || heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(minWidth, minHeight); } else { //否則根據我們的layout屬性來 setMeasuredDimension(getLayoutParams().width, getLayoutParams().height); } } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { measureWidth = childView.getMeasuredWidth() * childCount; measureHeight = childView.getMeasuredHeight(); setMeasuredDimension(paddingLeft + measureWidth + paddingRight, paddingTop + measureHeight + paddingBottom); } else if (heightSpecMode == MeasureSpec.AT_MOST) { measureHeight = childView.getMeasuredHeight(); setMeasuredDimension(paddingLeft + paddingRight + widthSpecSize, paddingTop + paddingBottom + measureHeight); } else if (widthSpecMode == MeasureSpec.AT_MOST) { measureWidth = childView.getMeasuredWidth() * childCount; setMeasuredDimension(paddingLeft + paddingRight + measureWidth, paddingTop + paddingBottom + heightSpecSize); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int paddingLeft = getPaddingLeft(); final int paddingRight = getPaddingRight(); final int paddingTop = getPaddingTop(); final int paddingBottom = getPaddingBottom(); //左邊初始位置為0 int childLeft = 0 + paddingLeft; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View childView = getChildAt(i); if (childView.getVisibility() != View.GONE) { final int childWidth = childView.getMeasuredWidth(); childView.layout(childLeft, 0 + paddingTop, childLeft + childWidth, paddingTop + childView.getMeasuredHeight()); childLeft += childWidth; } } } }
以上就是本文的全部內容,希望對大家的學習有所幫助。
(1) 首先我們創建一個基於iOS項目,我們就在Storyboard上進行開發。需要選中右側的Use Auto Layout,下面的Use Size Classes可選可
QQ厘米秀是騰訊手機QQ新出的功能,玩家在手機QQ聊天中可以放上自己的厘米秀,也能發很多特定的帶有聲音的表情。QQ厘米秀讓用戶在手機QQ聊天中更有趣,而且目
更多動態視圖MoreNewsView經常看朋友圈的動態,有的動態內容較多就只展示前面一段,如果用戶想看完整的再點擊展開,這樣整個頁面的動態列表比較均衡,不會出現個別動態占
這是一個短信篩選/批量刪除的工具類程序。 首先上圖: 1.展示全部系統短信。 2.新建篩選器(支持按號碼篩選和按內容篩選) 3.篩選,批量刪除。 主要代碼: