View绘制——画在哪?

这是Android视图绘制系列文章的第二篇,系列文章目录如下:

  1. View绘制——画多大?
  2. View绘制——画在哪?
  3. View绘制——怎么画?

View绘制就好比画画,先抛开Android概念,如果要画一张图,首先会想到哪几个基本问题:

  • 画多大?
  • 画在哪?
  • 怎么画?

Android绘制系统也是按照这个思路对View进行绘制,上面这些问题的答案分别藏在:

  • 测量(measure)
  • 定位(layout)
  • 绘制(draw)

这一篇将从源码的角度分析“定位(layout)”。

如何描述位置

位置都是相对的,比如“我在你的右边”、“你在广场的西边”。为了表明位置,总是需要一个参照物。View的定位也需要一个参照物,这个参照物是View的父控件。可以在View的成员变量中找到如下四个描述位置的参数:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * The distance in pixels from the left edge of this view’s parent
     * to the left edge of this view.
     * view左边相对于父亲左边的距离
     */
    protected int mLeft;
    
    /**
     * The distance in pixels from the left edge of this view‘s parent
     * to the right edge of this view.
     * view右边相对于父亲左边的距离
     */
    protected int mRight;
    
    /**
     * The distance in pixels from the top edge of this view’s parent
     * to the top edge of this view.
     * view上边相对于父亲上边的距离
     */
    protected int mTop;
    
    /**
     * The distance in pixels from the top edge of this view‘s parent
     * to the bottom edge of this view.
     * view底边相对于父亲上边的距离
     */
    protected int mBottom;
    ...
}

View通过上下左右四条线围城的矩形来确定相对于父控件的位置以及自身的大小。 那这里所说的大小和上一篇中测量出的大小有什么关系呢?留个悬念,先看一下上下左右这四个变量在哪里被赋值。

确定相对位置

全局搜索后,找到下面这个函数:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * Assign a size and position to this view.
     * 赋予当前view尺寸和位置
     *
     * This is called from layout.
     * 这个函数在layout中被调用
     *
     * @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
     * @return true if the new size and position are different than the previous ones
     */
    protected boolean setFrame(int left, int top, int right, int bottom) {
        ...
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        ...
    }
}

沿着调用链继续往上查找:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * Assign a size and position to a view and all of its
     * descendants
     * 将尺寸和位置赋予当前view和所有它的孩子
     *
     * <p>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().</p>
     *
     * <p>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.</p>
     * 子类不应该重载这个方法,而应该重载onLayout(),并且在其中局部所有孩子
     *
     * @param l Left position, relative to parent
     * @param t Top position, relative to parent
     * @param r Right position, relative to parent
     * @param b Bottom position, relative to parent
     */
    public void layout(int l, int t, int r, int b) {
        ...
        //为View上下左右四条线赋值
        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);
            ...
        }
    }
    ...
    /**
     * Called from layout when this view should
     * assign a size and position to each of its children.
     * 当需要赋予所有孩子尺寸和位置的时候,这个函数在layout中被调用
     *
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * 带有孩子的子类应该重载这个方法并调用每个孩子的layout()
     * @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) {
    }
}

结合调用链和代码注释,可以得出结论:孩子的定位是由父控件发起的,父控件会在ViewGroup.onLayout()中遍历所有的孩子并调用它们的View.layout()以设置孩子相对于自己的位置。

不同的ViewGroup有不同的方式来布局孩子,以FrameLayout为例:

public class FrameLayout extends ViewGroup {

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }
    
    //布局所有孩子
    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        //遍历所有孩子
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //排除不可见孩子
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                //获得孩子在measure过程中确定的宽高
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
                //确定孩子左边相对于父控件位置
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                //确定孩子上边相对于父控件位置
                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }
                //调用孩子的layout(),确定孩子相对父控件位置
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }
}

FrameLayout所有的孩子都是相对于它的左上角进行定位,并且在定位孩子右边和下边的时候直接加上了在measure过程中得到的宽和高。

测量尺寸和实际尺寸的关系

FrameLayout遍历孩子并触发它们定位的过程中,会用到上一篇测量的结果(通过getMeasuredWidth()getMeasuredHeight()),并最终通过layout()影响mRightmBottom的值。对比一下getWidth()getMeasuredWidth()

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    public final int getWidth() {
        //控件右边和左边差值
        return mRight - mLeft;
    }
    
    /**
     * Like {@link #getMeasuredWidthAndState()}, but only returns the
     * raw width component (that is the result is masked by
     * 获得MeasureSpec的尺寸部分
     * {@link #MEASURED_SIZE_MASK}).
     *
     * @return The raw measured width of this view.
     */
    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
}
  • getMeasuredWidth()是measure过程的产物,它是测量尺寸。getWidth()是layout过程的产物,它是布局尺寸。它们的值可能不相等。
  • 测量尺寸只是layout过程中可能用到的关于控件大小的参考值,不同的ViewGroup会有不同的layout算法,也就有不同的使用参考值的方法,控件最终展示尺寸由layout过程决定(以布局尺寸为准)。

总结

  1. 控件位置和最终展示的尺寸是通过上(mTop)、下(mBottom)、左(mLeft)、右(mRight)四条线围城的矩形来描述的。
  2. 控件定位就是确定自己相对于父控件的位置,子控件总是相对于父控件定位,当根布局的位置确定后,屏幕上所有控件的位置都确定了。
  3. 控件定位是由父控件发起的,父控件完成自己定位之后会调用onLayout(),在其中遍历所有孩子并调用它们的layout()方法以确定子控件相对于自己的位置。
  4. 整个定位过程的终点是View.setFrame()的调用,它表示着视图矩形区域的大小以及相对于父控件的位置已经确定。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353