自定义View之LayoutParams

前言

顾名思义,LayoutParams是一个用于保存和布局有关的属性的类,layout_width和layout_height两个必不可少的属性就是由它管理的,可见它的重要性。本文将详细介绍LayoutParams类,以及如何为自己的ViewGroup实现LayoutParams。

ViewGroup.LayoutParams

LayoutParams是ViewGroup的一个静态内部类,所有其他ViewGroup自定义的LayoutParams都是直接或间接地从它继承而来。首先看看它的几个域:

public static class LayoutParams {
public static final int MATCH_PARENT = -1;
public static final int WRAP_CONTENT = -2;

public int width;
public int height;
}

想必不需要多解释。特别注意一下,MATCH_PARENT和WRAP_CONTENT两个标志位的值都是负的,这个特点下面会用到。
接下来看看它的几个构造器:

public LayoutParams(Context c, AttributeSet attrs) {
    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
    setBaseAttributes(a,
            R.styleable.ViewGroup_Layout_layout_width,
            R.styleable.ViewGroup_Layout_layout_height);
    a.recycle();
}

protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
    width = a.getLayoutDimension(widthAttr, "layout_width");
    height = a.getLayoutDimension(heightAttr, "layout_height");
}

利用TypedValue类在布局文件中提取出layout_width和layout_height两个属性,并保存在了width和height中。不了解TypedValue的话可以看自定义View之添加自定义属性。这里很重要的一点是,在保存时不会区分获得的是固定值(如100dp)还是标志位(如MATCH_PARENT)。为了在使用时能够区分,MATCH_PARENT和WRAP_CONTENT两个标志位的值都是负的。也就是说,如果width或者height的值≥0,那么就代表固定值,否则就代表一个标志位。这个特性在构建MeasureSpec时常常用到。

public LayoutParams(LayoutParams source) {
    this.width = source.width;
    this.height = source.height;
}

根据一个现有的LayoutParams来设置自己的width与height域。

public LayoutParams(int width, int height) {
    this.width = width;
    this.height = height;
}

根据给定的width与height设置相应的域,在动态创建view时常常会用到。

ViewGroup.MarginLayoutParams

相比于原生的ViewGroup.LayoutParams,自定义的LayoutParams往往会选择继承自ViewGroup.MarginLayoutParams,这个类在父类的基础之上提供了对margin属性的支持。margin属性是子view的位置相对于父view边界或是其他子view的偏移量,有leftMargin、topMargin、rightMargin、bottomMargin四种。另外,还有一个总的margin属性。在布局文件中,如果设置了margin属性,另外四个属性就会失效,实际的值会全部取和margin相等。
MarginLayoutParams的构造器无非是在LayoutParams的基础上增加了关于提取与设置各个margin属性的代码,这里就不重复贴出来了。

LayoutParams是如何发挥作用的

在向一个父view中添加新的子view时,偷懒的人往往会直接这么写:

addView(myView);

单从字面意思上看,这句代码仅仅提供了要添加的view的引用,完全没有任何关于这个view应当有多大,放在什么位置的信息。实际上,这些信息父view在背后帮你补上了。下面通过addView()方法的实现,来看看LayoutParams是如何发挥作用的。
首先是最简单的版本:

public void addView(View child) {
    addView(child, -1);
}

没什么内容。继续看下去:

public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    //获取子view的LayoutParams
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        //如果子view没有设置LayoutParams,就为它生成一个默认的
        params = generateDefaultLayoutParams();
        //默认生成的LayoutParams不能为null
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

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

这里可以清楚地看到,父view会判断将要添加的子view有没有自带一个LayoutParams。如果没有的话,会调用generateDefaultLayoutParams()为它提供一个默认的。ViewGroup默认会提供一个width与height均为WRAP_CONTENT的LayoutParams。如果ViewGroup的子类实现了自己的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");
    }

    //重新进行measure与layout
    requestLayout();
    //重绘界面
    invalidate(true);
    addViewInner(child, index, params, false);
}

添加新的子view之后自然需要重新执行measure、layout与draw。重点在addViewInner()方法:

private void addViewInner(View child, int index, LayoutParams params,boolean preventRequestLayout) {
    //省略大量代码...

    //确认子View的LayoutParams是否合法
    if (!checkLayoutParams(params)) {
        //如果不合法,则提取这个params中有用的信息,并生成一个合法的LayoutParams
        params = generateLayoutParams(params);
    }

    //省略大量代码...
 }

protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return  p != null;
}

protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return p;
}

checkLayoutParams()与generateLayoutParams()方法一般也是需要重写的。可以看出,ViewGroup在确保子view的LayoutParams合法这一方面下了很大功夫。这也充分体现了LayoutParams的重要性。
最后补充一个方法:

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

这个方法在ViewGroup中没有直接调用到,它的作用是用xml中加载的属性生成一个LayoutParams对象。如果你想要给自己的LayoutParams添加自定义属性,就必须同时覆盖这个方法。例:

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

总结:如何为自己的ViewGroup实现LayoutParams

关于如何为自己的ViewGroup实现LayoutParams,前面部分已经讲的差不多了,只差一步——为LayoutParams提供自定义属性。这部分其实和为view提供自定义属性的方法是一样的:首先在attrs.xml中声明declare-styleable,然后在里面设置属性的名称与数据类型,最后在LayoutParams的构造器中提取。可以参考自定义View之添加自定义属性
最后将步骤重新整理一遍:
(1)attrs.xml中声明declare-styleable,并设置属性的名称与数据类型;
(2)创建LayoutParams的子类,并在其构造器中提取自定义属性;
(3)覆盖ViewGroup的generateDefaultLayoutParams()方法为子view提供默认LayoutParams实现
(4)覆盖ViewGroup的checkLayoutParams()验证子view的LayoutParams是否合法。
(5)覆盖两个重载版本的generateLayoutParams()方法。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值