Android开发&简单的ViewGroup——FrameLayout

    开门见山的说,一般android开发中,FrameLayout更多的是作为图层功能,或者碎片占位符;如时下的身份证扫描界面,可以利用FrameLayout实现两级图层;再有就是一些自定义的控件,往往是FrameLayout的子类;
    其实对于我来说,这个从出生起一直生存到现在的空间,直接使用的次数并不算多,因为它的功能实在是太过简单,虽然方便定制视图,但造轮子的事情早已有无数的先辈们替我们完成了,因此。。。

一、 简单的FrameLayout

FrameLayout存在的意义仿佛只是提供一个容器,凭借自己的想象,可以放置任何控件,甚至大小什么都无所谓,反正超过屏幕的部分会消失掉,不过也正是如此,才体现出了ViewGroup的含义——可以放View的容器;当然,嵌套ViewGroup也是可以的,毕竟ViewGroup也是View的实现类,虽然两者“长相”相差有点大。

二、 ViewGroup 与View

简单一提,View和ViewGroup之间的关系错综复杂,但他们有一个很直观的相同的特点——都能被看到;
View表示的是视图,能被看到,才能更好的表达信息,才能够与用户交互,否则就又要回归单片机的世界了。
这里就牵扯出重要的部分,view的呈现机制,见得很多地方将“能够理解View机制”作为一个中级菜鸟的合格证,不可否认,纯粹做一个码农,利用现有的控件或者大神写好的类,不理解这些东西也活的下去,确实如此,即便懂了这些,更深层次的底层实现我们还是一无所知,所以,兴趣才是前提。

三、 View的呈现机制

依靠计算机来理解人的思维,并显示到屏幕上,是个有些无从下手的问题,还好,最难的部分已经有人做完了,我们所做的只是拿着一堆的七巧板,来拼凑出让人耳目一新的效果(或许一个良好的UI设计师才适合做移动开发)。好在程序员只需要做一个简单的工作:用丑陋的界面实现交互的功能,剩下的部分就让其他人去完成吧。
提及View,不得不推一下那些总结出结论的前辈,没有他们的努力,在不知道结论的情况下我是不怎么喜欢去瞅源码的(时间好紧张。。。)。可以先看一下一系列的文章:深入了解View(一)
反正就四篇,即便看不懂,先记住结论也是可以的,有一点需要说明,因为上述博客太过经典,所以有些滞后,而sdk版本更迭是很快的,因此有些源码可能会与自己手头的文档有些差异,不过没关系,反正记住了结论,就可以参照源码自己分析了。
言归正传,一个View想要显示在屏幕上,需要的过程是复杂的,不过这已经被人为的抽象成了三个阶段(当前分析参照API23)

  1. measure 测量:确定view的大小
  2. layout 定位:确定view所在的坐标位置
  3. draw 绘制:在指定的坐标位置绘制出view

又是一个老生常谈的部分,没办法,黔驴技穷了,要是不说这个,还真就无从开始了,不过还好,只要看完上述一系列的四篇博客,至少可以去翻源码了,主要总结一下也不复杂:measure、layout和draw都是由一个类调用的,恩,一称为ViewRoot,在该类中由performTraversals()发起这三个过程,先盗用百度图片:

这里写图片描述

若单说view的话,只从DecorView开始就行了;DecorView可看做一个纵向排列的LinearLayout,包含两个子布局TitleView以及ContentView,TitleView我们一般都让它消失,因此只看ContentView就行,该View不仅是一个布局,并且还是正好要讨论的FrameLayout,再好不过。
先简单的叙述一下measure,layout,draw好了,反正看完博客自己也可以理解,就不过多××了。

【measure过程】
1、View系统的绘制流程会从ViewRoot的performTraversals()开始,测量根view(这里从FrameLayout开始考虑比较简单)
2、对于根View,会调用View的measure(),final类型,无法重写,measure()中会调用onMeasure()
3、ViewGroup的子类中一般会重写onMeasure(),完成两个操作:测量自身,测量子布局(调用子View的measure(),返回第2步递归调用)。而对于View的子类(不是ViewGroup的子类)而言,只完成一个操作:测量自身。
4、View的子类(非ViewGroup子类)中onMeasure()会调用setMeasureDimension()为成员变量mMeasuredWidth和mMeasuredHeight赋值,完成测量自身的工作;

【layout过程】
1、ViewRoot的performTraversals()会在measure结束后继续执行,并调用View的layout()来执行定位过程。
2、View类的layout()会调用onLayout(),View类的onLayout()为空,不进行任何操作;onLayout()主要确定视图在布局中的位置,因此一般由具体的ViewGroup的子类来重写(且在ViewGroup类中,onLayout为抽象方法)。(View类的layout()只会完成定位自身的工作,ViewGroup的onLayout()来让子View调用layout())
3、也就是说:若调用layout方法的为View的子类,那整个layout过程就已经结束,如果调用layout方法的为ViewGroup子类,则会在ViewGroup子类中调用onLayout方法,该方法会调用所有子view的layout方法,即返回第2步,递归调用

【draw过程】
ViewRoot的performTraversals()会在layout结束后继续执行;会先调用最上层视图的draw方法(View类中重载了两个draw方法,一个为public权限,一个为默认权限,首先系统会调用default的draw(),在其中会调用computeScroll()。
接下来,一般情况来看(忽略滚动条的绘制或保存canvas等比较麻烦的部分):
若对象为View的子类,不是ViewGroup的子类:default的draw()会调用public的draw()。
若对象为ViewGroup的子类,且ViewGroup自身没有可视部分,则default的draw()会直接调用dispatchDraw(),即一些ViewGroup子类中并不会调用public的draw()和onDraw()。
一般来说,public的draw()不会被重写,该方法会依次进行如下操作:
* 1、绘制background
* 2、If necessary, save the canvas’ layers to prepare for fading(暂不考虑)
* 3、Draw view’s content,此时即调用onDraw(),View类的该方法为空,一般View的子类需要重写该方法,进行内容的绘制。
* 4、draw the children,调用dispatchDraw(),该方法在View类中为空方法,在ViewGroup子类中则有具体的实现。dispatchDraw()中会调用ViewGroup子类中的drawChild(),drawChild()中会让子View调用View类非public的draw(),即返回draw流程开始部分。
* 5、If necessary, draw the fading edges and restore layers
* 6、Draw decorations (scrollbars for instance),即前景onDrawForeground 等(忽略此步骤即可)

当然肯定还有一些东西没有考虑到,比如padding,margin,滚动等等,不过熟悉基本调用过程就可以自定义view了。然后参照一下FrameLayout的源码,看一下一个ViewGroup如何对自身和子view完成这三个流程。

四、FrameLayout代码解析

再次说明,这里参照的SDK版本为23,每一个版本代码都会有变化,因此最好找个尽可能“古老”的版本,至少逻辑会清楚一些。接下来代码会将FrameLayout中重要部分全部注释出来,有很多变量源码中也找不到引用,因此这里只是将一些有View呈现有关的方法列出,一些成员变量即便没有引用的出处,也给出了大概表示的含义。

package com.android.senior.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by Administrator on 17-1-7.
 * <p/>
 * 注释FrameLayout的代码
 */
public class FrameLayout extends android.widget.FrameLayout {
    //默认的gravity
    private static final int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START;

    /**
     * 构造函数
     */
    public FrameLayout(
            Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        //获取属性集合
        final TypedArray a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.FrameLayout, defStyleAttr, defStyleRes);

        //获取xml中FrameLayout的measureAllChildren属性,属性为true则会在测量等过程考虑所有的即使是View.Gone的子View,默认为false
        if (a.getBoolean(com.android.internal.R.styleable.FrameLayout_measureAllChildren, false)) {
            setMeasureAllChildren(true);
        }

        a.recycle();
    }

    /**
     * 设置是否考虑所有的子view,即连同View.GONE的子view都进行测量,这里只是修改了一个标志位
     */
    @android.view.RemotableViewMethod
    public void setMeasureAllChildren(boolean measureAll) {
        mMeasureAllChildren = measureAll;
    }

    /**
     * 这里及接下来的三个方法可以先简单的看做时获取FrameLayout的padding值的(其实该值需要参照xml中FrameLayout的padding属性值和background(背景图片或背景资源xml文件——R.drawable.*)中的padding值)
     */
    int getPaddingLeftWithForeground() {
        return isForegroundInsidePadding() ? Math.max(mPaddingLeft, mForegroundPaddingLeft) :
                mPaddingLeft + mForegroundPaddingLeft;
    }

    int getPaddingRightWithForeground() {
        return isForegroundInsidePadding() ? Math.max(mPaddingRight, mForegroundPaddingRight) :
                mPaddingRight + mForegroundPaddingRight;
    }

    private int getPaddingTopWithForeground() {
        return isForegroundInsidePadding() ? Math.max(mPaddingTop, mForegroundPaddingTop) :
                mPaddingTop + mForegroundPaddingTop;
    }

    private int getPaddingBottomWithForeground() {
        return isForegroundInsidePadding() ? Math.max(mPaddingBottom, mForegroundPaddingBottom) :
                mPaddingBottom + mForegroundPaddingBottom;
    }

    /**
     * 系统会在View的measure方法中调用onMeasure方法,该方法完成两个操作:
     * 测量FrameLayout自身的大小
     * 测量每一个子View的大小
     * <p/>
     * 在该方法调用之后,才可以通过getMeasuredWidth()和getMeasuredHeight()方法获取FrameLayout真实的大小
     * {@inheritDoc}
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取所有子View的个数,该方法返回ViewGroup的成员变量mChildrenCount,该成员变量在ViewGroup的addInArray方法中更改值的大小。
        int count = getChildCount();

        //当传入的宽高对象中mode不都是EXACTLY时,该变量measureMatchParentChildren为true;即表示该FrameLayout的宽高还是有待测量的,不等于父布局的宽高(先不考虑margin和padding)
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                        MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;

        //mMatchParentChildren表示View的集合,该集合中每个宽高属性中至少有一个为MATCH_PARENT
        mMatchParentChildren.clear();

        //maxHeight表示该FrameLayout最大的高度,maxWidth同理;childState属性暂时不考虑
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        //对所有的子View进行遍历
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //mMeasureAllChildren默认为false,在构造函数中进行了赋值操作,因此不为Gone则进入if中
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                //先对子View进行一次测量,此次测量出的子View宽高是没有除去子view的margin以及FrameLayout的padding的,因此肯定不准确;在该measureChildWithMargins方法中会让子View去调用自身的measure方法;这里可以成为估测
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                //获取子View的LayoutParams对象,该对象中包括了layout_width等属性的值
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //取(maxWidth,子View的width加上子view的左右margin值)中较大的值,maxHeight同理
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        //如果此FrameLayout的宽高并不能直接确定,且子View的宽高有一个是MATCH_PARENT,则将该子View放到mMatchParentChildren集合中,该集合表示match自身布局的子View集合
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        // Account for padding too;负责padding的处理,将上面计算出的maxWidth和maxHeight再加上自身定义的padding值(paddingBottom+paddingTop+maxHeight,paddingLeft+paddingRight+maxWidth)
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width;结合minHeight等属性来选择出最大的一个maxHeight和maxWidth值
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width;结合background等属性来选择出最大的一个maxHeight和maxWidth值
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

        //提供一个maxWidth值和widthMeasureSpec对象,Spec对象中可以提取Mode属性,根据该属性来决定FrameLayout自身应当取的width值;测量height同理;然后调用setMeasuredDimension方法,该方法调用之后,通过getMeasuredWidth()和getMeasuredHeight()方法才能获取到FrameLayout正确的宽高值。
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        //如果FrameLayout的宽高并不能直接确定(如果可以确定的话mMatchParentChildren的size肯定为0),且有超过一个子View在集合中
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);

                //获取MarginLayoutParams对象,该对象在ViewGroup中定义,所有自定义的ViewGroup对象中的LayoutParams对象基本都是该对象的子类
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                final int childWidthMeasureSpec;
                //如果子View宽为MATCH_PARENT,则进行如下操作(注:mMatchParentChildren集合中所有的子View宽高至少有一个属性为MATCH_PARENT)
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    //这里getMeasuredWidth()就 可以获取到该FrameLayout正确的宽度;然后减去FrameLayout可能有的paddingLeft和paddingRight值,在减去子view中可能定义的layout_marginLeft,layout_marginRight值,剩下的部分就是子View的宽度;如果FrameLayout测量正确且宽度为MATCH_PARENT,则此处就完成了子view宽度的测量。
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    //宽度值MATCH_PARENT属于EXACTLY模式,并且求出了子view的width,因此可以生成宽度的Spec对象。
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    //如果宽度不是MATCH_PARENT,那么高度肯定是 MATCH_PARENT;此时要计算子View的宽度,就只能依赖view的measure过程中最最核心的方法——getChildMeasureSpec();该方法根据父布局的widthMeasureSpec,多余的FrameLayout的padding、子View的margin,以及子布局估测的宽度来综合得出一个合理的子view的width值
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                                    lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                //childHeightMeasureSpec计算方法与子view的width的计算方法类似
                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                                    lp.topMargin + lp.bottomMargin,
                            lp.height);
                }

                //然后让子view去调用一次measure方法
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

    /**
     * ViewGroup类中onLayout为抽象方法,因此所有的ViewGroup的实现类必须重写该方法
     * <p/>
     * View类中onLayout的方法说明如下:
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * 即:所有的ViewGroup实现类,都必须重写onLayout方法,并在该方法中调用所有子View的layout方法;
     * 更多的是,在View的layout方法中,已经通过setFrame方法确定了自身的位置
     * <p/>
     * 注:layout过程是一个确定位置的过程,即父布局有所有权来决定子布局应当所处的位置,这与measure过程是不同的
     * measure过程需要父布局给的限定大小,以及Spec的mode,再加上子view的mode来综合确定。
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //根据该方法的说明,在调用onLayout方法前,就已经在layout方法中调用了setFrame为ViewGroup(这里是FrameLayout)自身进行了layout过程,即left,top,right,bottom已经被赋值了。
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    /**
     * 尤其重要的是,FrameLayout的left,top,right,bottom值是相对于FrameLayout的父布局而言的;对子view调用layout方法时,传入的坐标系则是以FrameLayout而言的,这点需要进行转换
     *
     * @param left             FrameLayout的左侧位置
     * @param top              FrameLayout的顶部位置
     * @param right            FrameLayout的右侧位置
     * @param bottom           FrameLayout的底部位置
     * @param forceLeftGravity 是否强行设置子view的排列方式Gravity为left
     */
    void layoutChildren(int left, int top, int right, int bottom,
                        boolean forceLeftGravity) {
        //子view的个数
        final int count = getChildCount();

        //这里进行坐标系的变换:FrameLayout的左上角现在变为了(0,0),因此FrameLayout的paddingLeft值会限定子View所处的位置,即子view的left值一般情况下是小于FrameLayout的paddingLeft值的(有一种情况除外,即子view的marginLeft属性可能设置为负数),因此这里parentLeft表示:相对于子view来说,最左侧的坐标值。 paddingRight属性值
        final int parentLeft = getPaddingLeftWithForeground();

        //同理,进行坐标系转换,parentRight表示子View右侧坐标最大的值,计算方法是:FrameLayout的右侧坐标减去左侧坐标减去paddingRight值,即width减去paddingRight(正常情况下)
        final int parentRight = right - left - getPaddingRightWithForeground();

        //这里parentTop和parentBottom同上
        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        //开始layout子view的过程
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //如果子view的visibility为View.GONE,则不用考虑,直接当做没有这个子View
            if (child.getVisibility() != GONE) {
                //子view获取LayoutParams,这个对象可以当做在xml文件中添加元素时设置的,padding,margin,layout_width等值的集合。
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                //因为这个方法属于layout过程,measure过程会先执行,因此这里可以直接获取子view的宽高
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                //子view的左侧,上侧位置;这里只需要这两个属性就行,右下坐标可以结合width,height属性得到
                int childLeft;
                int childTop;

                //gravity:子view的放置方式,这里与layout_gravity要区分开;layout_gravity表示FrameLayout在父布局中如何放置;gravity表示子view在FrameLayout中如何放置;
                int gravity = lp.gravity;
                if (gravity == -1) {
                    //这里很明显,如果gravity为-1,则设置gravity为默认值;这里的-1从何而来暂不考虑,姑且当做如果FrameLayout没有设置gravity属性,则lp.gravity默认为-1
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                //layoutDirection表示习惯的阅读方式,一般习惯时从左向右读,但有些情况下(如古代诗文的阅读方式,或台湾文字阅读方式),阅读时从右向左的;该属性值可以通过在manifest文件中application节点下设置android:supportsRtl 来改变
                final int layoutDirection = getLayoutDirection();

                //结合gravity和layoutDirection得出真实的布局方式absoluteGravity(水平方向)
                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:
                        //如果是水平方向上时Gravity.CENTER_HORIZONTAL布局方式,则子view左侧坐标就是——parentLeft(子view初始处于的最左侧的坐标)【加上】(parentRight - parentLeft - width)/2(这个表示ziview可以处于的最右侧坐标减去最左侧坐标再减去子view的宽度,得到横向上除内容部分空白区域的宽度,然后除以2得到左侧空白的宽度)【加上】lp.leftMargin(子view的marginLeft的值,该值可能为负数,为负数则表示向左偏移的坐标数)【减去】lp.rightMargin(子view的marginRight的值);其实从这里可以看到,leftMargin和rightMargin在这种情况下不是相对左右侧的距离,而是偏移量
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                                lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            //如果是Gravity.RIGHT布局方式,并且没有强行限制forceLeftGravity,则子view左侧坐标为parentRight减去子view的宽度,再留出marginLeft的空白
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        //其他情况就相对简单,parentLeft加上子view的marginLeft的值;这里需要说明:安卓中子view的宽度和前端中div宽度是不同的,这里border只是背景的一种效果,padding只是限制内容区域或者子view应当偏离的值,这两者不会影响自身的width(会影响子view的大小),而margin只是设置自身在父布局中偏离的值,同样不会修改自身width,事实上在measure过程中,一个view的大小就已经确定了
                        childLeft = parentLeft + lp.leftMargin;
                }

                //计算childTop和计算childLeft类似;
                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;
                }

                //然后通过childLeft和childTop就可以确定子view所处的坐标了
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

    /**
     * 在FrameLayout中没有draw过程,因此draw过程会调用父类ViewGroup中默认的dispatchDraw方法
     *
     * 一般而言,带有进度条ScrollBar的view绘制流程会很复杂,view的绘制流程参照View类的public的draw方法可以看到很清楚。
     *
     * 到此为止,FrameLayout中measure,layout,draw三个view流程全部完成。
     */

    /**
     * 还有一个重要的部分,也是每个ViewGroup子类都会有的部分——定义自己的LayoutParams
     * <p/>
     * 所有ViewGroup子类中的LayoutParams都继承自ViewGroup类的MarginLayoutParams,因此动态添加布局时,可以使用MarginLayoutParams对象,这样就不用在不同的ViewGroup中设置不同的LayoutParams对象了。
     * <p/>
     * Per-child layout information for layouts that support margins.
     * See {@link android.R.styleable#FrameLayout_Layout FrameLayout Layout Attributes}
     * for a list of all child view attributes that this class supports.
     *
     * @attr ref android.R.styleable#FrameLayout_Layout_layout_gravity
     */
    public static class LayoutParams extends MarginLayoutParams {
        /**
         * 在这里可以看到,默认的gravity属性值确实是-1
         * <p/>
         * The gravity to apply with the View to which these layout parameters
         * are associated.
         *
         * @attr ref android.R.styleable#FrameLayout_Layout_layout_gravity
         * @see android.view.Gravity
         */
        public int gravity = -1;

        /**
         * {@inheritDoc}
         */
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            TypedArray a = c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.FrameLayout_Layout);
            //在未设置gravity属性值时,该方法返回值也是-1
            gravity = a.getInt(com.android.internal.R.styleable.FrameLayout_Layout_layout_gravity, -1);
            a.recycle();
        }

        /**
         * {@inheritDoc}
         */
        public LayoutParams(int width, int height) {
            super(width, height);
        }

        /**
         * Creates a new set of layout parameters with the specified width, height
         * and weight.
         *
         * @param width   the width, either {@link #MATCH_PARENT},
         *                {@link #WRAP_CONTENT} or a fixed size in pixels
         * @param height  the height, either {@link #MATCH_PARENT},
         *                {@link #WRAP_CONTENT} or a fixed size in pixels
         * @param gravity the gravity
         * @see android.view.Gravity
         */
        public LayoutParams(int width, int height, int gravity) {
            super(width, height);
            this.gravity = gravity;
        }

        /**
         * {@inheritDoc}
         */
        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }

        /**
         * {@inheritDoc}
         */
        public LayoutParams(ViewGroup.MarginLayoutParams source) {
            super(source);
        }

        /**
         * Copy constructor. Clones the width, height, margin values, and
         * gravity of the source.
         *
         * @param source The layout params to copy from.
         */
        public LayoutParams(LayoutParams source) {
            super(source);

            this.gravity = source.gravity;
        }
    }
}

由于代码过多,因此可能有些东西会出错,不过对于一个真实的ViewGroup的实现类而言,除了draw过程有些“水”,measure和layout过程都考虑到了很多方面,包括margin,padding,gravity,rtl等等;其实参照源码可以更好的理解,一些关键的英文注释会指明方法的功能等。

五、Broad vision

在大多时候,需要查看多个文档才能多篇博客,才能真正理解,即便是百年难得一用的FrameLayout,在配合上自定义的事件动画时,也可以有好的效果。类似这种Android View框架的measure机制以前感觉很神奇的问题,总有人知道为什么。总要多看看,当然,前提是喜欢 Android View刷新机制 。还有,发现不管学多少,都只是起点ViewGroup详解

猜你喜欢

转载自blog.csdn.net/lovingning/article/details/54292337