Android 自定义View及流程

自定义View绘制流程:


概述


自定义View的基本方法

自定义 View 的最基本的三个方法分别是: onMeasure()、onLayout()、onDraw(); View 在 Activity 中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、layout 和 draw。

  • 测量:onMeasure() 决定 View 的大小;
  • 布局:onLayout() 决定 View 在 ViewGroup 中的位置;
  • 绘制:onDraw() 决定绘制这个 View。

自定义 View 控件分类

  • 自定义 View: 只需要重写 onMeasure() 和 onDraw()
  • 自定义 ViewGroup: 则只需要重写 onMeasure() 和 onLayout()
1. 自定义ViewGroup

自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout,包含有子View。

例如:应用底部导航条中的条目,一般都是上面图标(ImageView),下面文字(TextView),那么这两个就可以用自定义ViewGroup组合成为一个Veiw,提供两个属性分别用来设置文字和图片,使用起来会更加方便。

2. 自定义View

在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View,不包含子View。

例如:制作一个支持自动加载网络图片的ImageView,制作图表等

PS: 自定义View在大多数情况下都有替代方案,利用图片或者组合动画来实现,但是使用后者可能会面临内存耗费过大,制作麻烦等诸多问题。

自定义View基础


View 类简介

  • View 类是Android中各种组件的基类,如View是ViewGroup基类
  • View表现为显示在屏幕上的各种视图

Android中的UI组件都由View、ViewGroup组成。

  • View的构造函数:共有4个
    构造函数是View的入口,可以用于初始化一些内容,和获取自定义属性
    View的构造函数有四种重载分别如下:
  // 如果View是在Java代码里面new的,则调用第一个构造函数
  public void SloopView(Context context) {}
  // 如果View是在.xml里声明的,则调用第二个构造函数 
  // 自定义属性是从AttributeSet参数传进来的 public
  public void SloopView(Context context, AttributeSet attrs) {}
  // 不会自动调用
  // 一般是在第二个构造函数里主动调用 
  // 如 view 有 style 属性时
  public void SloopView(Context context, AttributeSet attrs, int defStyleAttr) {}
  // API 21 之后才使用 
  // 不会自动调用 
  // 一般是在第二个构造函数里主动调用 
  // 如View有style属性时
  public void SloopView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {}

可以看出,关于View构造函数的参数有多有少。有四个参数的构造函数在API21的时候才添加上,暂不考虑

有三个参数的构造函数中第三个参数是默认的Style,这里的默认的Style是指它在当前Application或Activity所用的Theme中的默认Style,且只有在明确调用的时候才会生效,以系统中的ImageButton为例说明:

    public ImageButton(Context context, AttributeSet attrs) {
        //调用了三个参数的构造函数,明确指定第三个参数
        this(context, attrs, com.android.internal.R.attr.imageButtonStyle);
    }

    public ImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
        //此处调了四个参数的构造函数,无视即可
        this(context, attrs, defStyleAttr, 0); 
    }

注意:即使你在View中使用了Style这个属性也不会调用三个参数的构造函数,所调用的依旧是两个参数的构造函数。

由于三个参数的构造函数第三个参数一般不用,暂不考虑,第三个参数的具体用法会在以后用到的时候详细介绍。

排除了两个之后,只剩下一个参数和两个参数的构造函数,他们的详情如下:

 //一般在直接New一个View的时候调用。
  public void SloopView(Context context) {}
  
  //一般在layout文件中使用的时候会调用,关于它的所有属性(包括自定义属性)都会包含在attrs中传递进来。
  public void SloopView(Context context, AttributeSet attrs) {}

以下方法调用的是一个参数的构造函数:

  //在Avtivity中
  SloopView view = new SloopView(this);

以下方法调用的是两个参数的构造函数:

  //在layout文件中 - 格式为: 包名.View名
  <com.sloop.study.SloopView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

AttributeSet 与自定义属性

系统自带的 View 可以在 xml 中配置属性,对于写的好的自定义 View 同样可以在 xml 中配置属性,为了使自定义的 View 的属性可以在 xml 中配置,需要以下4个步骤:

  1. 通过 <declare-styleable>为自定义 View 添加属性
  2. 在 xml 中为相应的属性声明属性值
  3. 在运行时(一般为构造函数)获取属性值
  4. 将获取到的属性值应用到 View

View 视图结构

  1. PhoneWindow 是 Android 系统中最基本的窗口系统,继承自 Windows 类,负责管理界面显示以及事件响应。它是 Activity 与 View 系统交互的接口
  2. DecorView 是 PhoneWindow 中的起始节点 View,继承于 View 类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个 FrameLayout。
  3. ViewRoot 在 Activity 启动时创建,负责管理、布局、渲染窗口 UI 等。

对于多 View 的视图,结构是树形结构:最顶层是 ViewGroup,ViewGroup下可能有多个 ViewGroup 或 View,如下图:

无论是 measure 过程、layout 过程还是 draw 过程,永远都是从 View 树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个 View 树中各个 View,最终确定整个 View 树的相关属性。

Android 坐标系及位置获取方式

Android 中的坐标系

Android 中颜色相关内容

Android 支持的颜色模式:

以 ARGB8888 为例介绍颜色定义:

测量View大小(onMeasure)

View 的大小不仅由自身所决定,同时也会受到父控件的影响,为了我们的控件能更好的适应各种情况,一般会自己进行测量。
测量View大小使用的是onMeasure函数,我们可以从onMeasure的两个参数中取出宽高的相关数据:

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //取出宽度的确切数值
        int widthsize = MeasureSpec.getSize(widthMeasureSpec);    
        //取出宽度的测量模式 
        int widthmode = MeasureSpec.getMode(widthMeasureSpec);      
       
        //取出高度的确切数值
        int heightsize = MeasureSpec.getSize(heightMeasureSpec);   
        //取出高度的测量模式
        int heightmode = MeasureSpec.getMode(heightMeasureSpec);    
    }

从上面可以看出 onMeasure 函数中有 widthMeasureSpec 和 heightMeasureSpec 这两个 int 类型的参数, 毫无疑问他们是和宽高相关的, 但它们其实不是宽和高, 而是由宽、高和各自方向上对应的测量模式来合成的一个值:

测量模式一共有三种, 被定义在 Android 中的 View 类的一个内部类View.MeasureSpec中:

模式 二进制数值 描述
UNSPECIFIED 00 默认值,父控件没有给子view任何限制,子View可以设置为任意大小。
EXACTLY 01 表示父控件已经确切的指定了子View的大小。
AT_MOST 10 表示子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小。

在int类型的32位二进制位中,31-30这两位表示测量模式,29~0这三十位表示宽和高的实际值
用 MeasureSpec 的 getSize是获取数值, getMode是获取模式即可。

注意:
如果对View的宽高进行修改了,不要调用super.onMeasure(widthMeasureSpec,heightMeasureSpec);要调用setMeasuredDimension(widthsize,heightsize); 这个函数。

确定View大小(onSizeChanged)

这个函数在视图大小发生改变时调用。
Q: 在测量完View并使用setMeasuredDimension函数之后View的大小基本上已经确定了,那么为什么还要再次确定View的大小呢?

A: 这是因为View的大小不仅由View本身控制,而且受父控件的影响,所以我们在确定View大小的时候最好使用系统提供的onSizeChanged回调函数。

onSizeChanged如下:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }

可以看出,它又四个参数,分别为 宽度,高度,上一次宽度,上一次高度。
我们只需关注 宽度(w), 高度(h) 即可,这两个参数就是View最终的大小。

确定子View布局位置(onLayout)

确定布局的函数是onLayout,它用于确定子View的位置,在自定义ViewGroup中会用到,他调用的是子View的layout函数。

在自定义ViewGroup中,onLayout一般是循环取出子View,然后经过计算得出各个子View位置的坐标值,然后用以下函数设置子View位置。

 child.layout(l, t, r, b);

四个参数分别为:

名称 说明 对应的函数
l View左侧距父View左侧的距离 getLeft();
t View顶部距父View顶部的距离 getTop();
r View右侧距父View左侧的距离 getRight();
b View底部距父View顶部的距离 getBottom();

具体可以参考 坐标系 这篇文章。

5.绘制内容(onDraw)

onDraw是实际绘制的部分,也就是我们真正关心的部分,使用的是Canvas绘图。

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

getMeasureWidth 与 getWidth 的区别

  • getWidth 在layout()过程结束后才能获取到;通过视图右边的坐标减去左边的坐标计算出来的.
  • getMeasuredWidth 在measure()过程结束后就可以获取到对应的值;通过setMeasuredDimension()方法来进行设置的.

LayoutParams

LayoutParams 翻译过来就是布局参数,子 View 通过 LayoutParams 告诉父容器(ViewGroup)应该如何放置自己。从这个定义中也可以看出来 LayoutParams 与 ViewGroup 是息息相关的,因此脱离 ViewGroup 谈 LayoutParams 是没有意义的。

事实上,每个 ViewGroup 的子类都有自己对应的 LayoutParams 类,典型的如 LinearLayout.LayoutParams 和 FrameLayout.LayoutParams 等,可以看出来 LayoutParams 都是对应 ViewGroup 子类的内部类

MarginLayoutParams

MarginLayoutParams 是和外间距有关的。事实也确实如此,和 LayoutParams 相比,MarginLayoutParams 只是增加了对上下左右外间距的支持。实际上大部分 LayoutParams 的实现类都是继承自 MarginLayoutParams,因为基本所有的父容器都是支持子 View 设置外间距的。

  • 属性优先级问题
    MarginLayoutParams 主要就是增加了上下左右4种外间距。在构造方法中,先是获取了 margin 属性;如果该值不合法,就获取 horizontalMargin;如果该值不合法,再去获取 leftMargin 和 rightMargin 属性(verticalMargin、topMargin和bottomMargin同理)。我们可以据此总结出这几种属性的优先级

margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin

  • 属性覆盖问题
    优先级更高的属性会覆盖掉优先级较低的属性。此外,还要注意一下这几种属性上的注释

Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value

LayoutParams 与 View 如何建立联系

  • 在XML中定义 View
  • 在 Java 代码中直接生成 View 对应的实例对象

addView

/**
 * 重载方法1:添加一个子View
 * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
 */
public void addView(View child) {
    addView(child, -1);
}

/**
 * 重载方法2:在指定位置添加一个子View
 * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
 * @param index View将在ViewGroup中被添加的位置(-1代表添加到末尾)
 */
public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        params = generateDefaultLayoutParams();// 生成当前ViewGroup默认的LayoutParams
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

/**
 * 重载方法3:添加一个子View
 * 使用当前ViewGroup默认的LayoutParams,并以传入参数作为LayoutParams的width和height
 */
public void addView(View child, int width, int height) {
    final LayoutParams params = generateDefaultLayoutParams();  // 生成当前ViewGroup默认的LayoutParams
    params.width = width;
    params.height = height;
    addView(child, -1, params);
}

/**
 * 重载方法4:添加一个子View,并使用传入的LayoutParams
 */
@Override
public void addView(View child, LayoutParams params) {
    addView(child, -1, params);
}

/**
 * 重载方法4:在指定位置添加一个子View,并使用传入的LayoutParams
 */
public void addView(View child, int index, LayoutParams params) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }

    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child's request
    // will be blocked at our level
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {
    .....
    if (mTransition != null) {
        mTransition.addChild(this, child);
    }

    if (!checkLayoutParams(params)) { // ① 检查传入的LayoutParams是否合法
        params = generateLayoutParams(params); // 如果传入的LayoutParams不合法,将进行转化操作
    }

    if (preventRequestLayout) { // ② 是否需要阻止重新执行布局流程
        child.mLayoutParams = params; // 这不会引起子View重新布局(onMeasure->onLayout->onDraw)
    } else {
        child.setLayoutParams(params); // 这会引起子View重新布局(onMeasure->onLayout->onDraw)
    }

    if (index < 0) {
        index = mChildrenCount;
    }

    addInArray(child, index);

    // tell our children
    if (preventRequestLayout) {
        child.assignParent(this);
    } else {
        child.mParent = this;
    }
    .....
}

自定义LayoutParams

  1. 创建自定义属性
<resources>
    <declare-styleable name="xxxViewGroup_Layout">
        <!-- 自定义的属性 -->
        <attr name="layout_simple_attr" format="integer"/>
        <!-- 使用系统预置的属性 -->
        <attr name="android:layout_gravity"/>
    </declare-styleable>
</resources>
  1. 继承MarginLayout
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    public int simpleAttr;
    public int gravity;

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        // 解析布局属性
        TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.SimpleViewGroup_Layout);
        simpleAttr = typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0);
        gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity, -1);

        typedArray.recycle();//释放资源
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }

    public LayoutParams(MarginLayoutParams source) {
        super(source);
    }

    public LayoutParams(ViewGroup.LayoutParams source) {
        super(source);
    }
}
  1. 重写ViewGroup中几个与LayoutParams相关的方法
// 检查LayoutParams是否合法
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 
    return p instanceof SimpleViewGroup.LayoutParams;
}

// 生成默认的LayoutParams
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 
    return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}

// 对传入的LayoutParams进行转化
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 
    return new SimpleViewGroup.LayoutParams(p);
}

// 对传入的LayoutParams进行转化
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 
    return new SimpleViewGroup.LayoutParams(getContext(), attrs);
}

LayoutParams常见的子类

在为View设置LayoutParams的时候需要根据它的父容器选择对应的LayoutParams,否则结果可能与预期不一致,这里简单罗列一些常见的LayoutParams子类:

  • ViewGroup.MarginLayoutParams
  • FrameLayout.LayoutParams
  • LinearLayout.LayoutParams
  • RelativeLayout.LayoutParams
  • RecyclerView.LayoutParams
  • GridLayoutManager.LayoutParams
  • StaggeredGridLayoutManager.LayoutParams
  • ViewPager.LayoutParams
  • WindowManager.LayoutParams

MeasureSpec

定义

测量规格,封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize 是指在某种 SpecMode 下的参考尺寸,其中 SpecMode 有如下三种:

  • UNSPECIFIED
    父控件不对你有任何限制,你想要多大给你多大,想上天就上天。这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大)
  • EXACTLY
    父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。
  • AT_MOST
    你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。

MeasureSpecs 的意义

通过将 SpecMode 和 SpecSize 打包成一个 int 值可以避免过多的对象内存分配,为了方便操作,其提供了打包 / 解包方法

MeasureSpec值的确定

MeasureSpec值到底是如何计算得来的呢?

子 View 的 MeasureSpec 值是根据子 View 的布局参数(LayoutParams)和父容器的 MeasureSpec 值计算得来的,具体计算逻辑封装在 getChildMeasureSpec()

  /**
     *
     * 目标是将父控件的测量规格和child view的布局参数LayoutParams相结合,得到一个
     * 最可能符合条件的child view的测量规格。  

     * @param spec 父控件的测量规格
     * @param padding 父控件里已经占用的大小
     * @param childDimension child view布局LayoutParams里的尺寸
     * @return child view 的测量规格
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec); //父控件的测量模式
        int specSize = MeasureSpec.getSize(spec); //父控件的测量大小

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // 当父控件的测量模式 是 精确模式,也就是有精确的尺寸了
        case MeasureSpec.EXACTLY:
            //如果child的布局参数有固定值,比如"layout_width" = "100dp"
            //那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是EXACTLY
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } 

            //如果child的布局参数是"match_parent",也就是想要占满父控件
            //而此时父控件是精确模式,也就是能确定自己的尺寸了,那child也能确定自己大小了
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            }
            //如果child的布局参数是"wrap_content",也就是想要根据自己的逻辑决定自己大小,
            //比如TextView根据设置的字符串大小来决定自己的大小
            //那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛
            //所以测量模式就是AT_MOST,测量大小就是父控件的size
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 当父控件的测量模式 是 最大模式,也就是说父控件自己还不知道自己的尺寸,但是大小不能超过size
        case MeasureSpec.AT_MOST:
            //同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也优先满足孩子的需求
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } 
            //child想要和父控件一样大,但父控件自己也不确定自己大小,所以child也无法确定自己大小
            //但同样的,child的尺寸上限也是父控件的尺寸上限size
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            //child想要根据自己逻辑决定大小,那就自己决定呗
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

针对上表,这里再做一下具体的说明

  • 对于应用层 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定
  • 对于不同的父容器和view本身不同的LayoutParams,view就可以有多种MeasureSpec。
    1. 当view采用固定宽高的时候,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小;
    2. 当view的宽高是match_parent时,这个时候如果父容器的模式是精准模式,那么view也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那么view也是最大模式并且其大小不会超过父容器的剩余空间;
    3. 当view的宽高是wrap_content时,不管父容器的模式是精准还是最大化,view的模式总是最大化并且大小不能超过父容器的剩余空间。
    4. Unspecified模式,这个模式主要用于系统内部多次measure的情况下,一般来说,我们不需要关注此模式(这里注意自定义View放到ScrollView的情况 需要处理)。

实例 流式布局

public class FlowLayout extends ViewGroup {

  /**
   * 每个item 横向间距
   */
  private final int mHorizontalSpacing = dp2px(16);
  /**
   * 每个item 竖向间距
   */
  private final int mVerticallSpacing = dp2px(16);

  /**
   * 记录所有的行,一行一行的存储,用于layout
   */
  private List<List<View>> allLines = new ArrayList<>();

  /**
   * 记录每一行的行高,用于layout
   */
  private List<Integer> lineHeights = new ArrayList<>();

  public FlowLayout(Context context) {
    super(context);
  }

  public FlowLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
  }

  public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
  }

  private void clearMeasureParams() {
    allLines.clear();
    lineHeights.clear();
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 内存 抖动
    clearMeasureParams();
    // 先测量孩子
    int childCount = getChildCount();
    int paddingLeft = getPaddingLeft();
    int paddingRight = getPaddingRight();
    int paddingBottom = getPaddingBottom();
    int paddingTop = getPaddingTop();
    //记录这行已经使用了多宽的size
    int lineWidthUsed = 0;
    // ViewGroup解析的父亲给我的宽度
    int selfWidth = MeasureSpec.getSize(widthMeasureSpec);
    // ViewGroup解析的父亲给我的高度
    int selfHeight = MeasureSpec.getSize(heightMeasureSpec);
    // 保存一行中的所有的view
    List<View> lineView = new ArrayList<>();
    // 一行的行高
    int lineHeight = 0;
    // measure过程中,子View要求的父ViewGroup的宽
    int parentNeededWidth = 0;
    // measure过程中,子View要求的父ViewGroup的高
    int parentNeededHeight = 0;
    for (int i = 0; i < childCount; i++) {
      View childView = getChildAt(i);
      LayoutParams childLP = childView.getLayoutParams();
      if (childView.getVisibility() != View.GONE) {
        // 将layoutParams转变成为 measureSpec
        // 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
        // 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
        // 参数说明
        //  * @param spec 父view的详细测量值(MeasureSpec)
        //  * @param padding view当前尺寸的的内边距和外边距(padding,margin)
        //  * @param childDimension 子视图的布局参数(宽/高)
        int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width);
        int childHeigthMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height);
        // 测量子view的方法,就把孩子测量完了
        childView.measure(childWidthMeasureSpec, childHeigthMeasureSpec);
        // 获取子view的测量宽高
        int childMesauredWidth = childView.getMeasuredWidth();
        int childMeasuredHeight = childView.getMeasuredHeight();
        // 这里需要换行,等于说 接下来要放置的控件放不下了,需要换行
        if (childMesauredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {
          // 一旦换行,我们就可以判断当前行需要的宽和高了,所以此时要记录下来
          allLines.add(lineView);
          lineHeights.add(lineHeight);
          // 一旦换行,我们就可以判断当前需要的宽和高了,所以要记录起来
          parentNeededHeight = parentNeededHeight + lineHeight + mVerticallSpacing;
          parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);

          lineView = new ArrayList<>();
          lineHeight = 0;
          lineWidthUsed = 0;
        }
        // view 是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局
        lineView.add(childView);
        // 每行也需要加上空格
        lineWidthUsed = lineWidthUsed + childMesauredWidth + mHorizontalSpacing;
        // 获取每行最高的高度
        lineHeight = Math.max(lineHeight, childMeasuredHeight);
        //处理最后一行数据
        if (i == childCount - 1) {
          allLines.add(lineView);
          lineHeights.add(lineHeight);
          // 一旦换行,我们就可以判断当前需要的宽和高了,所以要记录起来
          parentNeededHeight = parentNeededHeight + lineHeight + mVerticallSpacing;
          parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
        }
      }
    }

    // setMeasuredDimension 此接口是设置自己的大小,并且保存起来
    // 在度量自己,并保存,父亲想要获取的时候,直接调用孩子的 child.getMeasureWidth就行
    // setMeasuredDimension(width,height);

    // 再测量自己,保存
    // 根据子View的度量结果,来重新度量自己ViewGroup
    // 作为一个ViewGroup,它自己也是一个View,它的大小也需要根据它的父亲给它提供的宽高来度量
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int realWidth = widthMode == MeasureSpec.EXACTLY ? selfWidth : parentNeededWidth;
    int realHeight = heightMode == MeasureSpec.EXACTLY ? selfHeight : parentNeededHeight;
    // 这个传递的是具体的size,不是MeasureSpec
    setMeasuredDimension(realWidth, realHeight);
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    // 总共的行数
    int lineCount = allLines.size();
    int curT = getPaddingTop();
    int curL = getPaddingLeft();
    for (int i = 0; i < lineCount; i++) {
      List<View> lineViews = allLines.get(i);
      int lineHeight = lineHeights.get(i);
      // 每行的view进行布局
      for (int j = 0; j < lineViews.size(); j++) {
        View view = lineViews.get(j);
        int left = curL;
        int top = curT;
        // getWidth 在layout()过程结束后才能获取到;通过视图右边的坐标减去左边的坐标计算出来的.
        // int right = left + view.getWidth();
        // int bottom = top + view.getHeight();
        // getMeasuredWidth 在measure()过程结束后就可以获取到对应的值;通过setMeasuredDimension()方法来进行设置的.
        int right = left + view.getMeasuredWidth();
        int bottom = top + view.getMeasuredHeight();
        view.layout(left, top, right, bottom);
        curL = right + mHorizontalSpacing;
      }
      curL = getPaddingLeft();
      curT = curT + lineHeight + mVerticallSpacing;
    }
  }

  public static int dp2px(int dp) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
  }
}

效果图:


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容