layout优化之view绘制流程
在加载了layoutxml文件创建view后,本文主要讲解view的显示过程,因为这个一般是不做修改的,我们这里只是大致的描述一下,不做详细的分析。
view从加载到绘制一般要经过三个阶段:measure,layout,draw。我们在重写view的时候一般情况下重写的是draw方法,这个负责具体的显示界面,前两个流程主要是测量定位view的位置,以及相关的属性。
首先我们看下measure方法,这个是在整个过程中是相对比较难理解的,其中又根据父控件的属性不同划分为三种模式,一种是upspecified,exactly,at-most,其中upspecified是无限制的,想多大就多大,一般来说针对的属性是fill_parent,match_patent等父控件没有规定大小的时候,子空间想多大就多大。
exactly模式应该说是我们写作的时候都固定了大小直接把view的height和width的大小固定,如:android:layout_height=”100dp”android:layout_width=”200dp”
at-most模式应该是规定父控件的大小,view只能在一定的范围内进行变化,但是并不能超过parent的大小。
Viewmeasure主要是根据parentview的参数和自身的参数进行测量,在onmeasure方法中进行具体的操作。V
ViewGroup类没有具体的是现实onmeasure方法,其实我们知道具体的测量都是在onMeasure方法中进行实现的,我们去查找子类发现framelayout,linearlayout等有具体实现onMeasure方法,现在我们看下framelayout中onmeasure方法的实现。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); final boolean measureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; mMatchParentChildren.clear(); int maxHeight = 0; int maxWidth = 0; int childState = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (mMeasureAllChildren || child.getVisibility() != GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); childState = combineMeasuredStates(childState, child.getMeasuredState()); if (measureMatchParentChildren) { if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { mMatchParentChildren.add(child); } } } } // Account for padding too maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground(); maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground(); // Check against our minimum height and width maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); // Check against our foreground's minimum height and width final Drawable drawable = getForeground(); if (drawable != null) { maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); } setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); count = mMatchParentChildren.size(); if (count > 1) { for (int i = 0; i < count; i++) { final View child = mMatchParentChildren.get(i); final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec; if (lp.width == LayoutParams.MATCH_PARENT) { final int width = Math.max(0, getMeasuredWidth() - getPaddingLeftWithForeground() - getPaddingRightWithForeground() - lp.leftMargin - lp.rightMargin); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( width, MeasureSpec.EXACTLY); } else { childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeftWithForeground() + getPaddingRightWithForeground() + lp.leftMargin + lp.rightMargin, lp.width); } final int childHeightMeasureSpec; if (lp.height == LayoutParams.MATCH_PARENT) { final int height = Math.max(0, getMeasuredHeight() - getPaddingTopWithForeground() - getPaddingBottomWithForeground() - lp.topMargin - lp.bottomMargin); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( height, MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTopWithForeground() + getPaddingBottomWithForeground() + lp.topMargin + lp.bottomMargin, lp.height); } child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } }
源码好长,从源码中我们能看到framelayout的大小主要是根据subview的大小和其他的一些因素共同决定的。从代码来看所有的孩子测量之后,经过一系类的计算之后得到寬高的最大值,这个值就是Framelayout的寬高,然后在通过setMeasuredDimension设置自己的宽高,FrameLayout可能用最大的子View的大小。对于
LinearLayout
,可能是高度的累加
,也有可能是宽度的叠加。
总的来说,父
View
是等所有的子
View
测量结束之后,再来测量自己
,根据子
view
值进行一定的计算,然后得到自己的寬高。
measure过程主要就是从顶层父View向子View递归调用view.measure方法(measure中又回调onMeasure方法)的过程
layout的过程
在完成了measure后就会开始layout过程,整个layout过程比较容易理解,layout也是从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。整个layout的核心主要是一下几点
1,View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑
2,measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的
3,View的getWidth()和getHeight()方法来获取View测量的宽高,必须在onlayout结束后才有实际的值,意思就是在我们加载xml前取view的高和寬都是错误的。
4,layout必须是有parent的,没有parent的layout是没有意义的
Draw的过程
Draw发生在measure和layout之后进行,此时view的大小和位置都已经固定,显示的位置也一定固定,只是还么有显示出来,draw就是把具体的需要展现给用户的效果给显示出来,其实整个draw的过程就是在一个Canvas上做画的过程。具体是在PhoneWindow.DecorView,ViewRootImpl中的代码会创建一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工,若是viewgroup的话会执行递归subviewdraw方法进行subview的绘制。
现在我们查看一下viewdraw方法中具体的实现,这个是view展现给我们的效果,所以我们需要详细的分析下
public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; /* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */ // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) { drawBackground(canvas); } // skip step 2 & 5 if possible (common case) final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); drawAutofilledHighlight(canvas); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // Step 7, draw the default focus highlight drawDefaultFocusHighlight(canvas); if (debugDraw()) { debugDrawFocus(canvas); } // we're done... return; }
第一步:背景绘制,这个基本上每个view都会绘制
从代码来看在画背景的时候,当背景为null的时候是不进行绘制的,直接返回。这个主要看注释就可以了,不做过多解释。
第二步:view内容的绘制
onDraw(canvas)方法是view用来draw自己的,在这里是一个null的方法,因为具体如何绘制,颜色线条什么样式就需要子View自己去实现,ViewGroup也没有实现。因为每个View的内容是各不相同的,所以需要由子类去实现具体逻辑。不同的view画出的效果不同。
第三步:对子view的绘制
dispatchDraw(canvas)方法是用来绘制子View的,但是在view方法中没有实现这个方法,只有在viewGroup中才会实现此问题。
剩下的几步主要也是对各种条件的绘制,焦点高亮滑动条等,基本上和绘制自己一致,这里不再做详细的分析
drawAutofilledHighlight(canvas); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // Step 7, draw the default focus highlight drawDefaultFocusHighlight(canvas); if (debugDraw()) { debugDrawFocus(canvas); }