本文主要關注View的測量、佈局、繪製三個步驟,討論這三個步驟的執行流程。本文暫不涉及View和Window之間的交互以及Window的管理。再論述完這三個步驟以後,文末以自定義TagGroup爲例,講述如何自定義ViewGroup。java
View樹的繪圖流程是由核心類:ViewRootImpl 來處理的,ViewRootImpl做爲整個控件樹的根部,它是控件樹正常運做的動力所在,控件的測量、佈局、繪製以及輸入事件的派發處理都由ViewRootImpl觸發。android
這裏我主要講幾個Handler:git
這是ViewRootImpl調度的核心,其處理的消息事件主要有: MSG_INVALIDATE、MSG_INVALIDATE_RECT、MSG_RESIZED、MSG_DISPATCH_INPUT_EVENT、MSG_CHECK_FOCUS、MSG_DISPATCH_DRAG_EVENT、MSG_CLOSE_SYSTEM_DIALOGS、MSG_UPDATE_CONFIGURATION等github
主要有如下幾類:View繪製相關、輸入焦點等用戶交互相關、系統通知相關。bash
有經驗的同窗確定遇到過這樣的場景:動態建立一個View以後,想要直接獲取measureWidth 和 measureHeight每每取不到,這個時候咱們會經過view.postDelayed()方法去獲取。那麼,問題來了,爲何這樣就能取到呢?markdown
答案就在ViewRootImpl中的ViewRootHandler,view.post--> attachInfo.mHandler.post --> ViewRootImpl ViewRootHandler. 這個Handler保證了當你post的runable被執行到時,view早就測量好了。app
public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); } // Postpone the runnable until we know on which thread it needs to run. // Assume that the runnable will be successfully placed after attach. getRunQueue().post(action); return true; } 複製代碼
Choreographer這個類來控制同步處理輸入(Input)、動畫(Animation)、繪製(Draw)三個UI操做,這裏不得不提一下Choreographer.FrameHandler目的就在於ViewRootImpl中涉及到到的View繪製流程,是經過Choreographer.FrameHandler來進行調度的。具體的調度過程以下:ide
一、 ViewRootImpl.scheduleTraversals函數
這個方法會往Choreographer註冊類型爲Choreographer.CALLBACK_TRAVERSAL的Callback。oop
// ViewRootImpl.scheduleTraversals 註冊callback
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
複製代碼
二、 Choreographer.FrameHandler
Choreographer.FrameHandler源碼以下,主要處理三個信號: MSG_DO_FRAME:開始渲染下一幀的操做 MSG_DO_SCHEDULE_VSYNC:請求Vsync信號 MSG_DO_SCHEDULE_CALLBACK:請求執行callback
對於這三個信號,Choreographer是有一個調度過程的,最終callback的回調執行都是落實到doFrame()方法上面的。
private final class FrameHandler extends Handler { public FrameHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_DO_FRAME: doFrame(System.nanoTime(), 0); break; case MSG_DO_SCHEDULE_VSYNC: doScheduleVsync(); break; case MSG_DO_SCHEDULE_CALLBACK: doScheduleCallback(msg.arg1); break; } } } 複製代碼
doFrame執行回調有一個順序的,順序依次以下: Choreographer.CALLBACK_INPUT Choreographer.CALLBACK_ANIMATION Choreographer.CALLBACK_TRAVERSAL Choreographer.CALLBACK_COMMIT
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame"); mFrameInfo.markInputHandlingStart(); doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); mFrameInfo.markAnimationsStart(); doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos); mFrameInfo.markPerformTraversalsStart(); doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos); 複製代碼
關於Choreographer,讀者能夠參考下這篇文章,講的很是詳細:Android Choreographer 源碼分析
這裏簡單說一個小竅門,經過Choreographer.getInstance().postFrameCallback() 註冊回調,並計算先後兩幀的時間差,咱們能夠測算出APP的掉幀數,從而動態檢測APP 卡頓。
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { long lastFrameTimeNanos = 0; long currentFrameTimeNanos = 0; @Override public void doFrame(long frameTimeNanos) { if (lastFrameTimeNanos == 0) { lastFrameTimeNanos = frameTimeNanos; } currentFrameTimeNanos = frameTimeNanos; long diffMs = TimeUnit.MILLISECONDS.convert(currentFrameTimeNanos - lastFrameTimeNanos, TimeUnit.NANOSECONDS); long droppedCount = 0; if (diffMs > 100) { droppedCount = (int) (diffMs / 16.6); String anrLog = collectAnrLog(applicationContext); DjLog.e("Block occur, droppedCount: " + droppedCount + ", anrLog: " + anrLog); } lastFrameTimeNanos = frameTimeNanos; Choreographer.getInstance().postFrameCallback(this); } }); 複製代碼
整個 View 樹的繪圖流程在ViewRoot.java類的performTraversals()函數展開,該函數所作 的工做可簡單概況爲是否須要從新計算視圖大小(measure)、是否須要從新安置視圖的位置(layout)、以及是否須要重繪(draw),流程圖以下:
更詳細的圖示以下:
performTraversals 方法很是龐大,整個源碼在800行左右,看起來會讓人吐血。這個方法主要的過程有四個:
預測量階段 這是進入performTraversals()方法後的第一個階段,它會對控件樹進行第一次測量。測量結果能夠經過mView. getMeasuredWidth()/Height()得到。在此階段中將會計算出控件樹爲顯示其內容所需的尺寸,即指望的窗口尺寸。在這個階段中,View及其子類的onMeasure()方法將會沿着控件樹依次獲得回調。
佈局窗口階段 根據預測量的結果,經過IWindowSession.relayout()方法向WMS請求調整窗口的尺寸等屬性,這將引起WMS對窗口進行從新佈局,並將佈局結果返回給ViewRootImpl。
最終測量階段 預測量的結果是控件樹所指望的窗口尺寸。然而因爲在WMS中影響窗口布局的因素不少(參考第4章),WMS不必定會將窗口準確地佈局爲控件樹所要求的尺寸,而迫於WMS做爲系統服務的強勢地位,控件樹不得不接受WMS的佈局結果。所以在這一階段,performTraversals()將以窗口的實際尺寸對控件進行最終測量。在這個階段中,View及其子類的onMeasure()方法將會沿着控件樹依次被回調。
佈局控件樹階段 完成最終測量以後即可以對控件樹進行佈局了。測量肯定的是控件的尺寸,而佈局則是肯定控件的位置。在這個階段中,View及其子類的onLayout()方法將會被回調。
繪製階段 這是performTraversals()的最終階段。肯定了控件的位置與尺寸後,即可以對控件樹進行繪製了。在這個階段中,View及其子類的onDraw()方法將會被回調。
那問題來了,這個方法何時會被觸發,或者說Android系統何時會對整個View樹進行一次全量的操做呢?從源碼中,咱們能夠看到如下幾個核心的方法會觸發:
@Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } } void invalidate() { ... if (!mWillDrawSoon) { scheduleTraversals(); } } 複製代碼
有幾點注意: • invalidate/postInvalidate 只會觸發 draw; • requestLayout,會觸發 measure、layout 和 draw 的過程; • 它們都是走的 scheduleTraversals -> performTraversals,用不一樣的標記位來進行區分; • resume 會觸發 invalidate; • dispatchDraw 是用來繪製 child 的,發生在本身的 onDraw 以後,child 的 draw 以前 Measure 和 Layout 的具體過程
關於Measure過程,不得不詳細提一下MeasureSpec。MeasureSpec是一個複合整型變量(32bit),用於指導控件對自身進行測量,它有兩個份量:前兩位表示SPEC_MODE,後30位表示SPEC_SIZE。SPEC_MODE的取值取決於此控件的LayoutParams.width/height的設置,SPEC_SIZE則是父視圖給定的指導大小。
SPEC_MODE有三種模式,具體的計算以下:
MeasureSpec.UNSPECIFIED: 表示控件在進行測量時,能夠無視SPEC_SIZE的值。控件能夠是它所指望的任意尺寸。
MeasureSpec.EXACTLY: 表示子控件必須爲SPEC_SIZE所制定的尺寸。當控件的LayoutParams.width/height爲一肯定值,或者是MATCH_PARENT時,對應的MeasureSpec參數會使用這個SPEC_MODE。
MeasureSpec.AT_MOST: 表示子控件能夠是它所指望的尺寸,可是不得大於SPEC_SIZE。當控件的LayoutParams.width/height爲WRAP_CONTENT時,對應的MeasureSpec參數會使用這個SPEC_MODE。
講了這麼多,下面咱們來實操一下。
需求:自定義一個TagGroup,用來顯示一系列標籤元素。要求標籤樣式徹底能夠自定義,標籤間距可在xml中指定,要有最多顯示多少行的控制,顯示不全時要展現「更多 ...」
在attrs.xml中協定樣式:
<declare-styleable name="DjTagGroup"> <attr name="tag_horizontalSpacing" format="dimension" /> <attr name="tag_verticalSpacing" format="dimension" /> <attr name="max_row" format="integer"/> </declare-styleable> 複製代碼
協定接口,用來提供具體的標籤元素:
public interface TagViewHolder {
View getView();
}
複製代碼
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); measureChildren(widthMeasureSpec, heightMeasureSpec); int width = 0; int height = 0; int row = 0; // The row counter. int rowWidth = 0; // Calc the current row width. int rowMaxHeight = 0; // Calc the max tag height, in current row. if (moreTagHolder != null) { moreTagMeasureWidth = moreTagHolder.getView().getMeasuredWidth(); moreTagMeasureHeight = moreTagHolder.getView().getMeasuredHeight(); } final int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); if (child.getVisibility() != GONE) { // judge the max_row if (row + 1 >= maxRow && rowWidth + childWidth > widthSize) { break; } rowWidth += childWidth; if (rowWidth > widthSize) { // Next line. rowWidth = childWidth; // The next row width. height += rowMaxHeight + verticalSpacing; rowMaxHeight = childHeight; // The next row max height. row++; } else { // This line. rowMaxHeight = Math.max(rowMaxHeight, childHeight); } rowWidth += horizontalSpacing; } } // Account for the last row height. height += rowMaxHeight; // Account for the padding too. height += getPaddingTop() + getPaddingBottom(); // If the tags grouped in one row, set the width to wrap the tags. if (row == 0) { width = rowWidth; width += getPaddingLeft() + getPaddingRight(); } else {// If the tags grouped exceed one line, set the width to match the parent. width = widthSize; } setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width, heightMode == MeasureSpec.EXACTLY ? heightSize : height); } 複製代碼
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int parentLeft = getPaddingLeft(); final int parentRight = r - l - getPaddingRight(); final int parentTop = getPaddingTop(); final int parentBottom = b - t - getPaddingBottom(); int childLeft = parentLeft; int childTop = parentTop; int row = 0; int rowMaxHeight = 0; boolean showMoreTag = false; final int count = getChildCount(); int unTagCount = count; if (moreTagHolder != null) { unTagCount--; } for (int i = 0; i < unTagCount; i++) { final View child = getChildAt(i); final int width = child.getMeasuredWidth(); final int height = child.getMeasuredHeight(); if (child.getVisibility() != GONE) { if (row + 1 >= maxRow && childLeft + width + (horizontalSpacing + moreTagMeasureWidth) > parentRight) { // 預留一個空位放置moreTag showMoreTag = true; break; } if (childLeft + width > parentRight) { // Next line childLeft = parentLeft; childTop += rowMaxHeight + verticalSpacing; rowMaxHeight = height; row++; } else { rowMaxHeight = Math.max(rowMaxHeight, height); } // this is point child.layout(childLeft, childTop, childLeft + width, childTop + height); childLeft += width + horizontalSpacing; } } if (showMoreTag) { final View child = getChildAt(count - 1); final int width = child.getMeasuredWidth(); final int height = child.getMeasuredHeight(); child.layout(childLeft, childTop, childLeft + width, childTop + height); } } 複製代碼
在xml中直接引用
<com.xud.tag.DjTagGroup android:id="@+id/dj_tag_group" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" app:tag_horizontalSpacing="8dp" app:tag_verticalSpacing="8dp" app:max_row="4"/> 複製代碼
定義本身的TagViewHolder
public class DjTagViewHolder implements DjTagGroup.TagViewHolder { public String content; public View rootView; public TextView tagView; public DjTagViewHolder(View itemView, String content) { this.rootView = itemView; tagView = itemView.findViewById(R.id.tag); tagView.setText(content); tagView.setOnClickListener(v -> Toast.makeText(context, "點擊了:" + content, Toast.LENGTH_SHORT).show()); } @Override public View getView() { return rootView; } } 複製代碼
往DjTagGroup直接設置tags
private void initDjTags() { String[] tags = TagGenarator.generate(10, 6); List<DjTagGroup.TagViewHolder> viewHolders = new ArrayList<>(); for (String tag: tags) { DjTagViewHolder viewHolder = new DjTagViewHolder(LayoutInflater.from(context).inflate(R.layout.view_tag, djTagGroup, false), tag); viewHolders.add(viewHolder); } DjTagViewHolder moreHolder = new DjTagViewHolder(LayoutInflater.from(context).inflate(R.layout.view_tag, djTagGroup, false), "更多 ..."); djTagGroup.setTags(viewHolders, moreHolder); } 複製代碼
實際的效果
源碼地址: Github: 自定義View輯錄DjCustomView
參考文章