转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持!
简单介绍:
在自己定义view的时候。事实上非常easy。仅仅须要知道3步骤:
1.測量——onMeasure():决定View的大小
2.布局——onLayout():决定View在ViewGroup中的位置
3.绘制——onDraw():怎样绘制这个View。
而第3步的onDraw系统已经封装的非常好了,基本不用我们来担心,仅仅须要专注到1,2两个步骤就中好了。
第一步的測量,能够參考我之前的文章:(ANDROID自己定义视图——onMeasure流程。MeasureSpec具体解释)
而这篇文章就来谈谈第二步:“布局(Layout)”
知识点回想:
在谈怎样使用onLayout方法前,先简单回顾一下知识点:
View视图结构:
View视图能够是单一的一个如TextView,也能够是一个视图组(ViewGroup)如LinearLayout。
如图:对于多View的视图他的结构是树形结构,最顶层是ViewGroup。ViewGroup下可能有多个ViewGroup或View。
这个树的概念非常重要,由于不管我们是在測量大小或是调整布局的时候都是从树的顶端開始一层一层。一个分支一个分支的进行(树形递归)。
Measure简单回想:
measure的作用就是为整个View树计算实际的大小。而通过刚才对View树的介绍知道,想计算整个View树的大小,就须要递归的去计算每个子视图的大小(Layout同理)。
对每个视图通过onMeasure方法的一系列測量流程后计算出实际的高(mMeasuredHeight)和宽(mMeasureWidth)传入setMeasuredDimension()方法完毕单个View的測量,如果所測的视图是ViewGroup则能够通过measureChild方法递归的计算当中的每个子view。对于每个View的实际宽高都是由父视图和本身视图决定的。
Layout(源代码分析):
Layout的作用就是为整个View树计算实际的位置。而通过刚才对View树的介绍知道,想计算整个View树的位置,就须要递归的去计算每个子视图的位置(Measure同理)。
而确定这个位置非常easy,仅仅须要mLeft,mTop,mRight,mBottom四个值(注意:这4个值是子View相对于父View的值。以下会具体介绍)。
在代码中怎样设置这4个值呢?
首先,不管是系统提供的LinearLayout还是我们自己定义的View视图,他都需要继承自ViewGroup类。之后必需要做的就是重写onLayout方法(由于在onLayout在ViewGroup中被定义为抽象方法)。
ViewGroup-onlayout:
@Override protected abstract void onLayout(boolean changed, int l, int t, int r, int b);onLayout被定义为抽象方法,所以在继承ViewGroup时必需要重写该方法(onMeasure不需要)。另外这种方法也被override标注,所以也是重写的方法。他重写的是其父类view中的onLayout方法。
View-onlayout:
/** * 当这个view和其子view被分配一个大小和位置时,被layout调用。 * @param changed 当前View的大小和位置改变了 * @param left 左部位置(相对于父视图) * @param top 顶部位置(相对于父视图) * @param right 右部位置(相对于父视图) * @param bottom 底部位置(相对于父视图) */ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}注讲解:当这个view和其子view被分配一个大小和位置时,被layout调用。所以我们去看看layout中做了什么。(注解没有全然依照英文翻译。而且有省略)
View-layout:
/** * 给View和其全部子View分配大小和位置 * * 这是布局的第二个阶段(第一个阶段是測量)。在这个阶段中。每个父视图须要去调用layout去为他全部的子视图确定位置 * 派生的子类不应该重写layout方法,应该重写onLayout方法,在onlayout方法中应该去调用每个view的layout */ public void layout(int l, int t, int r, int b) { // 将当前视图的左上右下记录为old值(參数中传入的为新的l,t,r,b值) int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; // setFrame方法的作用就是将新传入的ltrb属性赋值给View。然后推断当前View大小和位置是否发生了变化并返回 boolean changed = setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { // 调用onLayout回调方法。具体实现由重写了onLayout方法的ViewGroup的子类去实现(后面具体说明) onLayout(changed, l, t, r, b); mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; // 调用全部重写了onLayoutChange监听的方法。通知View大小和位置发生了改变 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; }
在这段代码中我们仅仅要知道:假设视图的大小和位置发生变化后。会调用我们前面分析过的onLayout方法。
对于onLayout方法的终于实现所有依靠我们在自己定义ViewGroup类中重写的onLayout去实现。
计算View位置:
在重写的onLayout方法中,唯一的目的就是:
对当前视图和其全部子View设置它们在父视图中详细位置(确定这个位置就依靠mLeft,mTop。mRight。mBottom这四个值)
之前介绍过,mLeft,mTop。mRight,mBottom这四个值表示的是子view相对于父view的位置。
以下我贴出我画的图看一下就明确了。
如图,黄色区域是我们的父view,而中间的深色的区域就是我们的子view。
所以对于这个View来说,我列出它相对于父view的各个值是怎样计算和相关函数:
mLeft,mTop,mRight,mBottom:
view.getLeft()——mLeft:子View左边界到父view左边界的距离
public final int getLeft() { return mLeft; }
view.getTop()——mTop:子View上边界到父view上边界的距离
view.getRight()——mRight:子View右边界到父view左边界的距离
view.getBottom()——mBottom:子View下边距到父View上边界的距离
视图宽高:
视图宽度 view.getWidth();子View的右边界 - 子view的左边界。
public final int getWidth() { return mRight - mLeft; }视图高度 view.getHeight() ;子View的下边界 - 子view的上边界。
public final int getHeight() { return mBottom - mTop; }
測量宽高:
view.getMeasuredWidth();measure过程中返回的mMeasuredWidth
public final int getMeasuredWidth() { return mMeasuredWidth & MEASURED_SIZE_MASK; }view.getMeasuredHeight();measure过程中返回的mMeasuredHeight
public final int getMeasuredHeight() { return mMeasuredHeight & MEASURED_SIZE_MASK; }
最后介绍一下getWidth/Height和getMeasuredWidth/Height的差别:
getWidth,和getLeft等这些函数都是View相对于其父View的位置。而getMeasuredWidth,getMeasuredHeight是測量后该View的实际值(有点绕,以下摘录一段jafsldkfj所写的Blog中的解释).
实际上在当屏幕能够包裹内容的时候,他们的值是相等的。仅仅有当view超出屏幕后,才干看出他们的差别:
当超出屏幕后,getMeasuredHeight()等于getHeight()加上屏幕之外没有显示的大小
在计算子View在父View中的位置时,主要就是应用上面这几个函数。以下就来看看怎样去重写onLayout。
onLayout:
对于重写onLayout的思路和重写onMeasure同样:
假设仅仅须要測量单个View。则单独測量它自己即可。假设须要測量的View其下还有子View,则须要測量其全部的子View。
并传递到onlayout( l, t, r, b )中;
剩下的任务就仅仅须要知道它的mLeft值。mTop值。加上长、宽值即可了。
长宽值非常easy,使用getWidth/Height和getMeasuredWidth/Height都能够。
因为这个View须要居中显示。剩下的问题就是怎样计算该View的mLeft值和mTop值。我的思路例如以下:
r(父View的mRight) = mLeft + width + mLeft(由于左右间距一样)
b(父View的mBottom) = mTop + height + mTop(由于上下间距一样)
我的代码例如以下:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // 循环全部子View for (int i=0; i<getChildCount(); i++) { View child = getChildAt(i); // 取出当前子View长宽 int width = child.getMeasuredWidth(); int height = child.getMeasuredHeight(); // 计算当前的mLeft和mTop值(r,b为传递进来的父View的mRight和mBottom值) int mLeft = (r - width) / 2; int mTop = (b - height) / 2; // 调用layout并传递计算过的參数为子view布局 child.layout(mLeft, mTop, mLeft + width, mTop + height); } }
布局文件例如以下:
<com.gxy.text.CostomViewGroup xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#eee999" > <Button android:text="ChildView" android:layout_width="200dip" android:layout_height="200dip" android:background="#333444" android:id="@+id/textView2" /> </com.gxy.text.CostomViewGroup>
效果图:
总结:
onMeasure和onLayout的大致总结完了,在自己定义View的时候最关键的是onLayout,由于不管你怎样Measure这个View的大小,最后的决定权永远在onLayout手中,onLayout会决定详细View的大小和位置。当然onMeasure也非常重要,有的情况控件的宽高不确定或者须要自己定义,这时候须要我们人工Measure它。
而在复杂的自己定义View时。非常多计算也须要在onMeasure中完毕。而且些值会记录下来在onLayout中又一次使用(个人理解,欢迎指正)。
写onMeasure和onLayout的时候仅仅是想自己总结一下。整理一下思路。由于网上有太多写的好了。这里推荐一下qinjuning这位大神的blog。关于View的内容他总结的相当全面和深入。
尽管有非常好的了。但我还会坚持自己总结一遍,接下来的计划是写一个略微复杂点的小样例。将onMeasure和onLayout结合起来(已经写完了,链接例如以下:ANDROID自己定义视图——仿瀑布布局)。
之后深入的研究一下View和ViewGroup的源代码,总结一下LayoutParams,LayoutInflater等简单经常使用的知识点。了解一下View的绘制刷新流程等等等。
。。
太多不会的了,慢慢来吧。