概述
原文:http://blog.csdn.net/a396901990/article/details/38129669
一、简介
在自定义view的时候,其实很简单,只需要知道3步骤:
1.测量——onMeasure():决定View的大小,关于此请阅读《Android自定义控件之onMeasure》
2.布局——onLayout():决定View在ViewGroup中的位置
3.绘制——onDraw():如何绘制这个View。
这篇文章主要来谈谈第二步布局(Layout)
二、View视图结构
View视图可以是单一的一个如TextView,也可以是一个视图组(ViewGroup)如LinearLayout。
如图:对于多View的视图他的结构是树形结构,最顶层是ViewGroup,ViewGroup下可能有多个ViewGroup或View。
这个树的概念很重要,因为无论我们是在测量大小或是调整布局的时候都是从树的顶端开始一层一层,一个分支一个分支的进行(树形递归)。
三、onLayout函数
measure的作用就是为整个View树计算实际的大小,而通过刚才对View树的介绍知道,想计算整个View树的大小,就需要递归的去计算每一个子视图的大小(Layout同理)。
对每一个视图通过onMeasure方法的一系列测量流程后计算出实际的高(mMeasuredHeight)和宽(mMeasureWidth)传入setMeasuredDimension()方法完成单个View的测量,如果所测的视图是ViewGroup则可以通过measureChild方法递归的计算其中的每一个子view。对于每个View的实际宽高都是由父视图和本身视图决定的。
Layout的作用就是为整个View树计算实际的位置,而通过刚才对View树的介绍知道,想计算整个View树的位置,就需要递归的去计算每一个子视图的位置(Measure同理)。
而确定这个位置很简单,只需要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);
View.java的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的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去实现。
ViewGroup.java的layout函数
/**
* {@inheritDoc}
*/
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
四、计算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;
- }
- public final int getHeight() {
- return mBottom - mTop;
- }
测量宽高:
view.getMeasuredWidth();measure过程中返回的mMeasuredWidth
- public final int getMeasuredWidth() {
- return mMeasuredWidth & MEASURED_SIZE_MASK;
- }
- 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。
剩下的任务就只需要知道它的mLeft值,mTop值,加上长、宽值就行了。
长宽值很简单,使用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中使用(个人理解,欢迎指正)。
最后
以上就是高大大神为你收集整理的Android自定义控件之onLayout的全部内容,希望文章能够帮你解决Android自定义控件之onLayout所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复