写给新人看的自定义 View-onLayout 篇
kkna9885
8年前
<p>先说一下View的layout 和 onLayout。</p> <p>这里为了方便理解,以写出自定义View为目的,不做太深入,其一是因为,我们知道这么多,就已经可以写出自定义ViewGroup了,另一方面,深入了我也不知道。总之,大家在看完文章,如果想知道更多的细节的话,就去研究一下View的layout源码。</p> <p>OK,话不多说,先分析layout主要源码</p> <p>先看一下View layout方法的源码</p> <pre> <code class="language-java">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; }</code></pre> <p>按惯例先说一下每个参数</p> <p><strong>l:</strong> View左边界距离父容器的左边界的距离</p> <p><strong>t:</strong> View上边界距离父容器上边界的距离</p> <p><strong>r:</strong> View右边界距离父容器左边界的距离</p> <p><strong>b:</strong> View下边界距离父容器上边界的距离</p> <p>具体如下图所示如下图所示:(图片是老师[GcsSloop]Github上面的 我拷贝来用一下)</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/85b300e273733a74b4fd68dd6a6447c5.jpg"></p> <p>自定义View.jpeg</p> <p>好的,下面可以直接看几组关键代码</p> <pre> <code class="language-java">boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);</code></pre> <p>可以看到,isLayoutModeOptical这个方法,是判断是否有光学边界的(光学边界这里暂时用不到,请自行谷歌)我们来仔细看setOpticalFrame,setFrame这两个方法</p> <pre> <code class="language-java">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); }</code></pre> <p>可以看到,这个setOpticalFrame方法,最终也是调用了setFrame,那好我们可以直接继续看setFrame方法了。</p> <pre> <code class="language-java">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; if (mForegroundInfo != null) { mForegroundInfo.mBoundsChanged = true; } notifySubtreeAccessibilityStateChangedIfNeeded(); } return changed; }</code></pre> <p>这里面代码就不用一句一句的分析,看大概,我们便可以看出步骤,先是比较了新位置和老位置是否有差异,如果有差异会调用sizechanged来更新我们View的位置。</p> <p>OK 这个方法大概分析完毕了,我们先回到layout方法继续 onLayout(changed, l, t, r, b); OK ,找到今天的主角了。我们点进去这个方法,看里面做了什么。</p> <pre> <code class="language-java">/** * Called from layout when this view should * assign a size and position to each of its children. * * Derived classes with children should override * this method and call layout on each of * their children. * @param changed This is a new size or position for this view * @param left Left position, relative to parent * @param top Top position, relative to parent * @param right Right position, relative to parent * @param bottom Bottom position, relative to parent */ protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }</code></pre> <p>有点奇怪,什么也没做。这个其实是android留给我们自己去实现的一个方法,也就是大家都知道的,去布局子View的位置,只有含有子View的容器,才需要重写这个方法,也就是ViewGroup。</p> <p>OK ,通过上面的分析,可以得到两个结论</p> <p>1、View通过layout方法来确认自己在父容器中的位置</p> <p>2、 ViewGroup通过onLayout 方法来确定View在容器中的位置</p> <p>OK,光有理论没什么卵用,来实现一个简单的流式布局,来验证一下</p> <pre> <code class="language-java">public class MyViewGroup extends ViewGroup { public MyViewGroup(Context context) { super(context); } public MyViewGroup(Context context, AttributeSet attrs) { super(context, attrs); } public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //遍历子View,测量每个View的大小 for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); measureChild(view, widthMeasureSpec, heightMeasureSpec); } } }</code></pre> <p>自定义ViewGroup,通过onLayout()方法给子View布局,前提,我们必须得知道每个子View的宽度和高度,对吧。所以我们先要在onMeasure的时候,测量一下每个子View的具体大小,前面已经把View和ViewGroup的onMeasure都分析过了,这边不在赘述。直接遍历子View,然后measureChild即可得到所有子View的measureSize(注意这里说的是measureSize,为什么是measureSize,之后再谈)。</p> <p>OK 已经测量出子View的具体大小了,那么下面,我们就来安排他们的位置。</p> <pre> <code class="language-java">private int horizontalSpace = 10;//水平间距 private int verticalSpace = 10;//垂直间距 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int hadUsedHorizontal = 0;//水平已经使用的距离 int hadUsedVertical = 0;//垂直已经使用的距离 int width = getMeasuredWidth(); // int height = getMeasuredHeight(); for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); //判断是否已经超出宽度 if (view.getMeasuredWidth() + hadUsedHorizontal > width) { //已经超出了宽度 hadUsedVertical = hadUsedVertical + view.getMeasuredHeight() + verticalSpace; hadUsedHorizontal = 0; } view.layout(hadUsedHorizontal, hadUsedVertical, hadUsedHorizontal + view.getMeasuredWidth(), hadUsedVertical + view.getMeasuredHeight()); hadUsedHorizontal = hadUsedHorizontal + horizontalSpace + view.getMeasuredWidth(); } }</code></pre> <p>我们先是定义了水平已经使用的距离,和垂直已经使用的距离,而且,如果有需要 我们还需要水平和垂直的间距,都定义出来。OK,可以看到,逻辑很简单,每次layout子View的时候,我们都要判断,子View宽度,已经超出了父View的宽度,如果超出了,就换行。最后调用子View的layout来确定子view的位置。</p> <p>OK,记得刚才说我们源码里面获取子View大小的时候,宽度为例子使用getMeasuredWidth,为什么用这个而不用getWidth呢?也就是我前面说的measureSize</p> <p>OK我们看一下。我们在ViewGroup onMeasure的时候,调用了measureChild方法。我们看一下这个源码</p> <pre> <code class="language-java">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); }</code></pre> <p>调用了child.measure,也就是view的measure方法。我们继续看里面做了什么,由于代码很多,我就不粘贴了。view的.measure 方法调用了自己的onMeasure方法,也就像我们在onMeasure说的那样,之后子View会调用setMeasuredDimension来提交自己的宽高。我们看看这个setMeasuredDimension</p> <pre> <code class="language-java">private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }</code></pre> <p>OK ,这就清楚了,最终,我们调用measureChild方法 最终会把子View的大小传给mMeasuredSize。那可能会有朋友问,那getWidth,和getHeight会得到什么呢?在onMeasure 方法的时候,getwidth 和getheight都是0;为什么呢?</p> <p>View的getWidth源码:</p> <pre> <code class="language-java">public final int getWidth() { return mRight - mLeft; }</code></pre> <p>而我们刚才分析layout的源码时候就知道,mRight和mleft是在layout方法之后才赋值的,所以在测量子View的时候,是无法拿到getWidth 和 getHeight的。</p> <p>OK 最后贴出源码和布局文件</p> <pre> <code class="language-java">public class MyViewGroup extends ViewGroup { private int horizontalSpace = 10; private int verticalSpace = 10; public MyViewGroup(Context context) { super(context); } public MyViewGroup(Context context, AttributeSet attrs) { super(context, attrs); } public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int hadUsedHorizontal = 0;//水平已经使用的距离 int hadUsedVertical = 0;//垂直已经使用的距离 int width = getMeasuredWidth(); // int height = getMeasuredHeight(); for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); //判断是否已经超出宽度 if (view.getMeasuredWidth() + hadUsedHorizontal > width) { //已经超出了宽度 hadUsedVertical = hadUsedVertical + view.getMeasuredHeight() + verticalSpace; hadUsedHorizontal = 0; } view.layout(hadUsedHorizontal, hadUsedVertical, hadUsedHorizontal + view.getMeasuredWidth(), hadUsedVertical + view.getMeasuredHeight()); hadUsedHorizontal = hadUsedHorizontal + horizontalSpace + view.getMeasuredWidth(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); measureChild(view, widthMeasureSpec, heightMeasureSpec); } } /** * @param child 子View * @param parentWidthMeasureSpec 宽度测量规格 * @param widthUsed 父view在宽度上已经使用的距离 * @param parentHeightMeasureSpec 高度测量规格 * @param heightUsed 父view在高度上已经使用的距离 */ @Override protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed); } }</code></pre> <p>布局文件</p> <pre> <code class="language-java"><?xml version="1.0" encoding="utf-8"?> <picture.yisi.com.viewconfigrationtest.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="80dp" android:layout_height="40dp" android:background="@color/colorAccent"/> <TextView android:layout_width="80dp" android:layout_height="40dp" android:background="@color/colorAccent"/> <TextView android:layout_width="80dp" android:layout_height="40dp" android:background="@color/colorAccent"/> <TextView android:layout_width="80dp" android:layout_height="40dp" android:background="@color/colorAccent"/> <TextView android:layout_width="40dp" android:layout_height="40dp" android:background="@color/colorAccent"/> <TextView android:layout_width="80dp" android:layout_height="40dp" android:background="@color/colorAccent"/> </picture.yisi.com.viewconfigrationtest.MyViewGroup></code></pre> <p>最终效果</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/66824a1e5723412a7892ad0e95cdd2fd.png"></p> <p style="text-align:center">Paste_Image.png</p> <p>之后我们还会继续来完善这个ViewGroup,让他变成一个强大的ViewGroup。OK,onLayout可能就写到这里了,如果有补充的,之后会在补充。这个分析的比较简单,为了是让新人能快速的学会如何使用onLayout,如果大家想深入了解,建议去谷歌一下onLayout,有很多讲的比较详细的。</p> <p> </p> <p> </p> <p>来自:http://www.jianshu.com/p/a5b1e778744f</p> <p> </p>