我的个人网站
Google Blog
Android自定义View学习
Android自定义View之onMeasure()源码分析
Android自定义View之onLayout()的源码分析
如果觉得我的文章还行的话,也可以关注我的公众号,里面也会第一时间更新,并且会有更多的关于技术的最新资讯和一些个人感想。
扫码关注
我们都知道,自定义ViewGroup是所有子View的父控件,而自定义View通常需要重写onMeasure(),onLayout(),onTouchEvent()等方法,当然了,我们都知道自定义最难的地方在于draw(即画)的过程,这难以理解,不过今天这一篇文章要说的不是draw,而是如何了解onLayout()方法。我们都知道,自定义View的第一步是测量当前剩余空间,或者说是界面的大小,也就是measure了,这一点在上一篇通过讲解onMeasure()方法已经向大家解释了,所以今天我们要说的就是onLayout()方法的重写和分析了,也就是确定自定义View显示的位置。
android.view.View.layout()
既然要讲layout,确定子View在父控件的位置,那么我们就从View的layout()方法开始着手吧!
先来看一下Google官方文档对android.view.View.layout()这个方法的描述吧:
Assign a size and position to a view and all of its descendants
也就是说这个方法的作用是为视图及其所有后代分配大小和位置。
This is the second phase of the layout mechanism. (The first is measuring). In this phase, each parent calls layout on all of its children to position them. This is typically done using the child measurements that were stored in the measure pass().
这是布局机制的第二阶段。 (第一个是测量measure)。在这个阶段,每个父级调用所有子级的布局来定位它们。这通常使用存储在度量pass()中的子测量来完成。
Derived classes should not override this method. Derived classes with children should override onLayout. In that method, they should call layout on each of their children.
派生类不应覆盖此方法。具有子代的派生类应该覆盖onLayout()方法。在onLayout()方法方法中,他们应该在每个子View上调用layout()方法。
好了,以上都是通过官方文档而得知的内容,接下来我们直接看layout()方法的源码:
//参数l, t, r, b分别表示子View相对于父View的左、上、右、下的坐标
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);
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;
}
方法的主要步骤如下:
确定子View在父View中的位置
判断子View位置是否发生变化,如果发生变化则调用onLayout()方法
步骤一中调用了setFrame()方法,把l,t, r, b分别与之前的mLeft,mTop,mRight,mBottom逐一比较,假若其中任意一个值发生了变化,那么就判定该View的位置发生了变化。
android.view.View.onLayout()
顺着源码的思路,既然发生变化时调用了onLayout()方法,那么接下来我们就从onLayout()方法的源码中来寻找答案吧!
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
居然是一个空方法,那我们只能看看官方文档对这个方法的解释了:
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.
在布局(layout()方法)中调用时,此视图(onLayout()方法)应为其每个子View分配大小和位置。具有子代的派生类应该覆盖此方法并在其每个子View上调用布局。
那么谁有子View呢?当然是ViewGroup了,ViewGroup是所有子View的管理器嘛!它存在的目的就是为了对其子View进行管理,为其子View添加显示和响应的规则。
简单的,也就是说ViewGroup会调用onLayout()方法来确定View的显示位置。
既然是这样,那么我们就直接看ViewGroup的onLayout()方法是如何实现的吧!
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
天啊,居然是一个抽象方法。那我们只能继续看文档的解释了:
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.
和View.onLayout()方法的解释是一样的:
大概就是说ViewGroup的子类都必须重写这个方法,实现自己的逻辑。比如:FrameLayou,LinearLayout,RelativeLayout等等布局都需要重写这个方法,在该方法内依据各自的布局规则确定子View的位置。
这里我们以LinearLayout为例,来看看它的onLayout()方法是如何实现的
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
可以看到在LinearLayout的onLayout()方法中分别处理了水平线性布局layoutVertical()和垂直线性布局layoutHorizontal()。这里我们选择选择layoutVertical()继续往下看。
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
final int width = right - left;
int childRight = width - mPaddingRight;
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
case Gravity.CENTER_VERTICAL:
childTop =mPaddingTop+(bottom-top-mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child,childLeft,childTop+ getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
我们来分析一下这个方法的主要实现步骤:
计算child可使用空间的大小(当前可使用空间的大小)
获取子View的个数
计算childTop从而确定子View的开始布局位置
确定每个子View的位置
这一步是最关键的,我们具体分析一下它的实现逻辑吧:
获取子View测量后的宽和高 (这里获取到的childWidth和childHeight就是在measure阶段所确立的宽和高 )
获取子View的LayoutParams
根据子View的LayoutParams确定子View的位置
我们可以发现在setChildFrame()中又调用了View的layout()方法来确定子View的位置。
很明显:
ViewGroup首先调用layout()来确定自己本身在其父View中的位置,然后调用onLayout()确定每个子View的位置,每个子View又会调用View的layout()方法来确定自己在ViewGroup的位置。
简单的说:
View的layout()方法用于View确定自己本身在其父View的位置
ViewGroup的onLayout()方法用于确定子View的位置
自定义ViewGroup
为了能够更好的理解,这里通过一个自定义的ViewGroup来模拟onLayout()的过程,重写onMeasure(),onLayout(),onDraw()方法,并实现构造方法:
package com.example.jimcharles.customview;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
if(childCount > 0){
View child = getChildAt(0);
measureChild(child,widthMeasureSpec,heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
if(childCount > 0){
View child = getChildAt(0);
int measuredWidth = child.getMeasuredWidth();
int measuredHeight = child.getMeasuredHeight();
child.layout(0,0,measuredWidth,measuredHeight);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
好了,代码的逻辑很简单
首先在onMeasure()方法中获取当前child的大小,即测量子View的大小;然后在在onLayout()中确定子View的位置。
好了,现在把我们定义的ViewGroup放进布局文件来看一下效果吧!
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/custom"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="#A5FD01"
custom:context="com.example.jimcharles.customview.MainActivity">
<com.example.jimcharles.customview.MyViewGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/myViewGroup"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true">
<ImageView
android:id="@+id/text_girl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/girl"/>
</com.example.jimcharles.customview.MyViewGroup>
</RelativeLayout>
好了,用ImageView通过MyViewGroup在父控件上显示一张图片,很简单的一个布局
然后再MainActivity中通过findViewById()获取ImageView实例
package com.example.jimcharles.customview;
import android.app.Activity;
import android.os.Bundle;
import android.widget.ImageView;
public class MainActivity extends Activity {
private ImageView girlImageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_viewgroup);
girlImageView = (ImageView) findViewById(R.id.text_girl);
}
}
让我们来看一下效果
好了,我们已经分析完了measure和layout这两个过程,一起来回想一下
- 获取View的测量大小measuredWidth和measuredHeight的时机
在某些复杂或者极端的情况下系统会多次执行measure过程,所以在onMeasure()中去获取View的测量大小得到的是一个不准确的值。为了避免该情况,最好在onMeasure()的下一阶段即onLayout()中去获取。
- getMeasuredWidth()和getWidth()的区别
在绝大多数情况下这两者返回的值都是相同的,但是结果相同并不说明它们是同一个东西。
首先,它们的获取时机是不同的
在measure()过程结束后就可以调用getMeasuredWidth()方法获取到View的测量大小,而getWidth()方法要在layout()过程结束后才能被调用从而获取View的实际大小。
其次,它们返回值的计算方式不同
getMeasuredWidth()方法中的返回值是通过setMeasuredDimension()方法得到的;而getWidth()方法中的返回值是通过View的右坐标减去其左坐标(right-left)计算出来的。
- 刚才说到了关于View的坐标,下面是获取view的相对位置的四个方法:
view.getLeft(),view.getRight(),view.getBottom(),view.getTop();
这四个方法用于获取子View相对于父View的位置。
getLeft( )用于获取子View的左边距离父View的左边的距离
getRight( )用于获取子View的右边距离父View的左边的距离
getTop( 用于获取子View的上边距离父View的上边的距离
getBottom( )用于获取子View的下边距离父View的上边的距离
- 直接继承自ViewGroup可能带来的复杂处理
刚通过一个例子简单模拟了ViewGroup的onLayout()过程。而项目开发中实际的情况可能远比这个复杂;比如,在ViewGroup中包含了多个View,每个View都设置了padding和margin,除此之外还可能包含各种嵌套。在这种情况下,我们在onMeasure()和onLayout()中都要花费大量的精力来处理这些问题。所以在一般情况下,我们可以选择继承自LinearLayout,RelativeLayout等系统已有的布局从而简化这两部分的处理。