最基础的自定义ViewGroup

如果本文帮助到你,本人不胜荣幸,如果浪费了你的时间,本人深感抱歉。
希望用最简单的大白话来帮助那些像我一样的人。如果有什么错误,请一定指出,以免误导大家、也误导我。
本文来自:http://www.jianshu.com/users/320f9e8f7fc9/latest_articles
感谢您的关注。

Android中所有界面都是由 View 和 ViewGroup 组成,
View是 所有基本组件的基类,以View结尾的。
ViewGroup是 拼装这些组件的容器,以Layout结尾的。

我们这次用的是 ViewGroup
绘制布局由两个遍历的过程组成:测量过程布局过程
onMeasure() 完成测量过程:测量所有视图的宽度和高度
onLayout() 完成布局过程:利用上步计算出的测量信息,布局所有子视图

我觉得能来搜ViewGroup的应该都基本的知道,这个是用来干嘛的。先来写下个人的整体理解。
先上总结:

  1. 在attr创建属性,在dimens设置默认属性
  1. 在CascadeLayout构造器里面获取设置的默认属性
  2. 在 onMeasure 计算每个子视图的位置和宽度 ————测量过程
    3.1 创建 LayoutParams,来储存每个视图的 尺寸信息
    3.2 添加 LayoutParams 需要的一些方法。
  3. 将算好的子视图的信息 在onLayout中设置给布局。 ————布局过程
  4. 然后在布局中就可以使用了。

然后我们来看看效果图:

最基础的自定义ViewGroup_第1张图片
Paste_Image.png
  

    

    

    

    

只要在代码中使用了我们的父布局,在子布局中不用做任何设置,就会按照指定的规则排列。

具体步骤就是上面总结,看起来挺麻烦的,其实是非常简单。跟着看下去,不想写代码直接复制就可以,都是很简单的代码。走一遍流程之后,就知道自定义 ViewGroup是个怎么会事了,如果有更好的想法,可以再具体实践的。

这里说一下那个app:....... 自定义属性的前缀。
这个app 为什么会是app 呢。
因为我们用到了自定义的属性,需要在xml引入我们需要在最顶部加入这个:

 xmlns:app="http://schemas.android.com/apk/res/com.lyl.viewgroup"

而这个app就是由后面的赋值的,当然也可以改成其他的你想要的。

不想看代码的,可以直接跳到最后看源码,源码也非常简单。注释也比较详细。


然后开始写代码吧。

1. 在attr创建属性,在dimens设置默认属性

首先在res/values的目录下创建一个attrs.xml文件。文件内容如下:

   
   
       
           
           
       
   

attrs.xml文件定义属性的作用是为了在这里使用。看那两个app:的属性,就是为了设置每个子布局距离 左边 和 上面 的距离:

   

然后在res/values的目录下创建一个dimens.xml文件,这里的作用是为了设置每个子布局距离左边和上面的默认距离,也就是上面两个属性没有设置的时候,这个会起作用。

   
   
       20dp
       20dp
   
2. 在CascadeLayout构造器里面获取设置的默认属性

这一步肯定是要先创建一个CascadeLayout类的,继承ViewGroup,实现方法。然后在构造器里面就可以获取设置的属性了,代码如下,有相应注释:

public class CascadeLayout extends ViewGroup {

// 子布局位于 左边 和 上面 的距离
private int mHorizontalSpacing;
private int mVerticalSpacing;

/**
 * 当通过XML文件创建该视图会调用这个方法
 */
public CascadeLayout(Context context, AttributeSet attrs) {
    super(context, attrs);

    // 从自定义属性中获取值,从attrs中
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CascadeLayout);

    try {
        // 获取用户指定的大小,就是我们刚开始指定的那个40dp
        // 如果没有指定,就使用默认值
        mHorizontalSpacing = ta.getDimensionPixelSize(R.styleable.CascadeLayout_horizontal_spacing, getResources()
                .getDimensionPixelSize(R.dimen.cascade_horizontal_spacing));
        mVerticalSpacing = ta.getDimensionPixelSize(R.styleable.CascadeLayout_vertical_spacing, getResources()
                .getDimensionPixelSize(R.dimen.cascade_vertical_spacing));

    } finally {
        ta.recycle();
    }
}
3. 在 onMeasure 计算每个子视图的位置和宽度 ————测量过程

在编写onMeasure()方法之前,我们要先创建LayoutParams类,这个类用来保存每个子视图的x,y轴的位置。一般都是把这个类直接定义为内部类。代码如下:

3.1 创建 LayoutParams,来储存每个视图的 尺寸信息
/** 保存每个子视图的x、y轴的位置 **/
public static class LayoutParams extends ViewGroup.LayoutParams {
    int x;
    int y;

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }
}
3.2 添加 LayoutParams 需要的一些方法。

要使用LayoutParams,还需要在我们的CascadeLayout类中重写以下方法。这些方法在不同的Viewgroup通常都是相同的,可参考 LinearLayout 源码。

@Override
protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {
    return p instanceof LayoutParams;
}

@Override
protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return new LayoutParams(p.width, p.height);
}
3.3 编写onMeasure()方法。这是这个类的核心代码

注释的很清楚。

/**
 * 测量所有视图的宽度和高度
 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // !!!在编写onMeasure方法之前,先创建自定义LayoutParams类,保存每个子视图的x、y轴的位置。

    // 使用宽和高 计算布局的最终大小,以及子视图的x、y轴的位置
    int width = getPaddingLeft();
    int height = getPaddingTop();

    // 1. 获取子视图的数量
    final int count = getChildCount();
    for (int i = 0; i < count; i++) {

        View child = getChildAt(i);

        // 2. 让 每个子视图 测量自己
        measureChild(child, widthMeasureSpec, heightMeasureSpec);

        LayoutParams lp = (LayoutParams) child.getLayoutParams();

        // 3. 在LayoutParams中保存每个子视图的x、y坐标,以便在onLayout()中布局
        lp.x = width;
        width += mHorizontalSpacing;

        lp.y = height;
        height += mVerticalSpacing;
    }

    // 4. 计算整体的宽、高
    width += getChildAt(getChildCount() - 1).getMeasuredWidth() + getPaddingRight();
    height += getChildAt(getChildCount() - 1).getMeasuredHeight() + getPaddingBottom();

    // 5. 使用计算的 宽、高 设置整个布局的测量尺寸
    setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
}
4. 将算好的子视图的信息 在onLayout中设置给布局。 ————布局过程
/**
 * 利用上步计算出的测量信息,布局所有子视图
 */
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int count = getChildCount();
    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        LayoutParams lp = (LayoutParams) child.getLayoutParams();
        child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y + child.getMeasuredHeight());
    }
}

到这里就可以结束了。

5. 然后在布局中就可以使用了。

现在就可以使用了,跟我们刚开始的时候那个布局一样。



为子布局添加属性。

因为上面的大体逻辑已经写完,添加子布局的属性也比较简单。
还是先上总结:

  1. 在attrs.xml 文件,添加子布局的新属性
  1. 在CascadeLayout的内部类LayoutParams 中接受这个属性
  2. 在onMeasure()使用这个属性

来写代码

1. 在attrs.xml 文件,添加子布局的新属性

    
    

2. 在CascadeLayout的内部类LayoutParams 中接受这个属性

修改之后LayoutParams 类的代码如下,同样注释写的很清楚:

/** 保存每个子视图的x、y轴的位置 **/
public static class LayoutParams extends ViewGroup.LayoutParams {
    int x;
    int y;
    // 子布局距离 左边 和 上面的距离
    public int verticalSpacing;
    public int horizontalSpacing;

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);

        // 1. 从自定义属性中获取值
        TypedArray ta = c.obtainStyledAttributes(attrs, R.styleable.CascadeLayout_LayoutParams);

        // 2.将获取的值拿到,以便onMeasure()使用
        try {
            horizontalSpacing = ta.getDimensionPixelSize(
                    R.styleable.CascadeLayout_LayoutParams_layout_horizontal_spacing, -1);
            verticalSpacing = ta.getDimensionPixelSize(
                    R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing, -1);
        } finally {
            ta.recycle();
        }
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }
}
3. 在onMeasure()使用这个属性

在onMeasure()中修改了 子布局 x、y的位置代码。其余的没有变

    if (lp.horizontalSpacing >= 0) {
        lp.x = width + lp.horizontalSpacing;
        width += mHorizontalSpacing + lp.horizontalSpacing;
    } else {
        lp.x = width;
        width += mHorizontalSpacing;
    }

    if (lp.verticalSpacing >= 0) {
        lp.y = height + lp.verticalSpacing;
        height += mVerticalSpacing + lp.verticalSpacing;
    } else {
        lp.y = height;
        height += mVerticalSpacing;
    }

然后就可以在子视图中添加属性了:
app:layout_horizontal_spacing="40dp"
app:layout_vertical_spacing="40dp"





最后项目源码:
https://github.com/Wing-Li/PracticeDemos
欢迎Star,欢迎批评指正。

你可能感兴趣的:(最基础的自定义ViewGroup)