Android 自定义 View 之 onLayout 源码分析

来源:互联网 发布:知识图谱构建 python 编辑:程序博客网 时间:2024/06/05 00:16

我们都知道,自定义 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() 方法的源码:

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;    }

方法的主要步骤如下:

  1. 确定子 View 在父 View 中的位置

  2. 判断子 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);        }    }}

我们来分析一下这个方法的主要实现步骤:

  1. 计算 child可 使用空间的大小(当前可使用空间的大小)

  2. 获取子 View 的个数

  3. 计算 childTop 从而确定子 View 的开始布局位置

  4. 确定每个子 View 的位置

这一步是最关键的,我们具体分析一下它的实现逻辑吧:

  1. 获取子 View 测量后的宽和高 (这里获取到的 childWidth 和 childHeight 就是在 measure 阶段所确立的宽和高 )

  2. 获取子 View 的 LayoutParams

  3. 根据子 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 这两个过程,一起来回想一下

  1. 获取View的测量大小 measuredWidth 和 measuredHeight 的时机

在某些复杂或者极端的情况下系统会多次执行 measure 过程,所以在 onMeasure() 中去获取 View 的测量大小得到的是一个不准确的值。为了避免该情况,最好在 onMeasure() 的下一阶段即 onLayout() 中去获取。

  1. getMeasuredWidth() 和 getWidth() 的区别

在绝大多数情况下这两者返回的值都是相同的,但是结果相同并不说明它们是同一个东西。

首先,它们的获取时机是不同的

在 measure() 过程结束后就可以调用 getMeasuredWidth() 方法获取到View的测量大小,而 getWidth() 方法要在 layout() 过程结束后才能被调用从而获取 View 的实际大小。

其次,它们返回值的计算方式不同

getMeasuredWidth() 方法中的返回值是通过 setMeasuredDimension() 方法得到的;而 getWidth() 方法中的返回值是通过 View 的右坐标减去其左坐标(right-left)计算出来的。

  1. 刚才说到了关于 View 的坐标,下面是获取 view 的相对位置的四个方法:

view.getLeft(),view.getRight(),view.getBottom(),view.getTop();

这四个方法用于获取子 View 相对于父 View 的位置。

  • getLeft( ) 用于获取子 View 的左边距离父 View 的左边的距离

  • getRight( ) 用于获取子 View 的右边距离父 View 的左边的距离

  • getTop( ) 用于获取子 View 的上边距离父 View 的上边的距离

  • getBottom( ) 用于获取子 View 的下边距离父 View的上边的距离

    1. 直接继承自 ViewGroup 可能带来的复杂处理

刚通过一个例子简单模拟了 ViewGroup 的 onLayout() 过程。而项目开发中实际的情况可能远比这个复杂;比如,在 ViewGroup 中包含了多个 View,每个 View 都设置了 padding 和 margin,除此之外还可能包含各种嵌套。在这种情况下,我们在 onMeasure() 和 onLayout() 中都要花费大量的精力来处理这些问题。所以在一般情况下,我们可以选择继承自 LinearLayout,RelativeLayout 等系统已有的布局从而简化这两部分的处理。


2 0
原创粉丝点击