Android知识梳理之自定义ViewGroup

来源:互联网 发布:入门单反推荐2017知乎 编辑:程序博客网 时间:2024/03/29 18:45

         在我们进行android开发的时候虽然官方提供了形形色色的控件,但是有的时候根据不同业务需求我们找不到官方的控件支持,那么这个时候就需要我们自己去定义控件来适应不同的需求了.本篇将和大家一起探讨自定义ViewGrop 的相关知识. 

  转载请注明出处: http://blog.csdn.net/unreliable_narrator?viewmode=contents


首先我们先来看看官方文档是如何进行描述的:


        翻译过来的大体意思就是:一个ViewGroup是一种特殊的视图可以包含其他视图(称为孩子)的视图组基类的布局和视图的容器。这个类也定义了viewgroup.layoutparams类作为基类的布局参数。也就是说ViewGroup实际上就是存放一些控件的容器,比如官方自带的一些Linerlayout,RelativeLayout等等.我们先来讲讲ViewGroup中两个重要的方法:onLayout和onMeasure,onLayout是必须重写实现的.

onmeuse()方法:
            测量自己的大小,为正式布局提供建议。(注意,只是建议,至于用不用,要看onLayout); 
            定义:如果layout_widhtlayout_heightmatch_parent或具体的xxxdp,那就非常简单了,直接调用setMeasuredDimension()方法,设置ViewGroup的宽高即可.But如果是wrap_content,就比较麻烦了,我们需要遍历所有的子View,然后对每个子View进行测量,然后根据子View的排列规则,计算出最终ViewGroup的大小.如果不重写 onMeasure() 方法,系统则会不知道该默认多大尺寸,就会默认填充整个父布局,所以,重写 onMeasure() 方法的目的,就是为了能够给 View 一个 wrap_content 属性下的默认大小
            调用此方法会传进来的两个参数:int widthMeasureSpec, int heightMeasureSpec .他们是父类传递过来给当前view的一个建议值,即想把当前view的尺寸设置为宽widthMeasureSpec,高heightMeasureSpec 虽然表面上看起来他们是int类型的数字,其实他们是由mode+size两部分组成的。 widthMeasureSpec和heightMeasureSpec转化成二进制数字表示,他们都是30位的。前两位代表mode(测量模式),后面28位才是他们的实际数值(size)。 
           MeasureSpec.getMode()获取模式
      MeasureSpec.getSize()获取尺寸  
  模式:
        EXACTLY:表示设置了精确的值,一般当childView设置其宽、高为精确值(也就是我们在布局文件中设定的值如50dp)、match_parent时,ViewGroup会将其设置为EXACTLY;

        AT_MOST:表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST;

        UNSPECIFIED:表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
setMeasureDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
//可作为模板代码!
private int measureWidth(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY){//精确值模式,指定具体数值
result = specSize;
}else{
result = 200;//先设置一个默认大小
//最大值模式,layout_width 或 layout_height 为 wrap_content 时,控件大小随控件的内容变化而变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。
if(specMode == MeasureSpec.AT_MOST){
result = Math.min(result, specSize);//取出我们指定的大小和 specSize 中最小的一个来作为最后的测量值
}
//MeasureSpec.UNSPECIFIED 不指定其大小,View 想多大就多大
}
return result;
}

测量子控件相关的方法:
                      getChildCount()                   在自定义的ViewGrop中我们可以得到子view的数目,再循环遍历出子view。
                      getChildAt(int index).       可以拿到index上的子view。
                     通过子控件的childView.getMeasuredWidth()childView.getMeasuredHeight()可以拿到子控件的宽高.但是在是使用这两个方法的时候需要子控件自己去测量自身的控件的大小,有三种方式去进行测量:
                     measureChild(subView, int wSpec, int hSpec)         viewGroup的测量子view的方法针对单个子控件去进行 测量
                     measureChildren(int wSpec, int hSpec);                  viewGroup的测量子view的方法针对于所有的子控件去进行测量.
                     subView.measure(int wSpec, int hSpec);                 子view自身的测量方法
                     measureChildWithMargins(subView, intwSpec, int wUsed, int hSpec, int hUsed);  //某一个子view,多宽,多高, 内部加上了viewGroup的padding值、margin值和传入的宽高wUsed、hUsed  

onLayout()方法:

         onLayout()是实现所有子控件布局的函数。它的作用就是给每一个子控件设置摆放的位置.    
         View的放置都是根据一个矩形空间放置的,onLayout传下来的l,t,r,b分别是放置父控件的矩形可用空间(除去margin和padding的空间)的左上角的left、top以及右下角right、bottom值参数changed表示view有新的尺寸或位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
int childLeft = 0;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE) {
final int childWidth = childView.getMeasuredWidth();
childView.layout(childLeft, 0, childLeft + childWidth,
childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
}
注意:如果你要动态添加View到ViewGroup,要把if(changed)这个判断条件去掉,不去会引起让人蛋疼的VIew不显示问题。 
例子一枚:
            如果我们要实现如下图所示的效果:我们可以看到我们自定义的viewgrop里面的三个子控件并排显示,并且自定义的ViewGrop的高度是和子控件里面最高的View是一样高的.ViewGrop的宽度是和所有子控件的宽度相累加的是一样的.

自定义ViewGrop
public class MyView extends ViewGroup {    public MyView(Context context) {        this(context, null);    }    public MyView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        //调用该方法预先对子控件进行测量        measureChildren(widthMeasureSpec, heightMeasureSpec);        //设置控件的宽高        setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));    }    /**     * 返回控件的宽     *     * @param widthMeasureSpec     * @return     */    private int measureWidth(int widthMeasureSpec) {        int specMode = MeasureSpec.getMode(widthMeasureSpec);        int specSize = MeasureSpec.getSize(widthMeasureSpec);        int result = 0;        //判断是否是包裹内容的模式        if (specMode == MeasureSpec.AT_MOST) {            int size = 0;            //将所有的子控件的宽度进行叠加            for (int x = 0; x < getChildCount(); x++) {                View child = getChildAt(x);                int measuredWidth = child.getMeasuredWidth();                size += measuredWidth;            }            result = size;        } else {            result = specSize;        }        return result;    }    /**     * 返回控件的高     *     * @param heightMeasureSpec     * @return     */    private int measureHeight(int heightMeasureSpec) {        int heightMode = MeasureSpec.getMode(heightMeasureSpec);        int heightSize = MeasureSpec.getSize(heightMeasureSpec);        int result = 0;        //判断是否是包裹内容        if (heightMode == MeasureSpec.AT_MOST) {            for (int x = 0; x < getChildCount(); x++) {                View child = getChildAt(x);                int measuredHeight = child.getMeasuredHeight();                //取子控件最大的高度                int min = Math.max(result, measuredHeight);                result = min;            }        } else {            result = heightSize;        }        return result;    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        int childCount = getChildCount();        int left = 0; //左边的距离        View child;        //遍历布局子元素        for (int i = 0; i < childCount; i++) {            child = getChildAt(i);            int width = child.getMeasuredWidth();            child.layout(left, 0, left + width, child.getMeasuredHeight());            left += width;        }    }}
布局文件:
 <com.dapeng.viewgropdemo.MyView        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:background="@android:color/black">        <View            android:layout_width="50dip"            android:layout_height="50dip"            android:background="@android:color/holo_green_dark"></View>        <View            android:layout_width="100dip"            android:layout_height="100dip"            android:background="@android:color/holo_red_light"></View>        <View            android:layout_width="200dip"            android:layout_height="200dip"            android:background="@android:color/holo_orange_light"></View>    </com.dapeng.viewgropdemo.MyView>
getMeasuredWidth()与getWidth()的区别.
接着讲一个很容易出错的问题:getMeasuredWidth()与getWidth()的区别。他们的值大部分时间都是相同的,但意义确是根本不一样的,我们就来简单分析一下。
区别主要体现在下面两点:
(1)首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。
(2) getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过layout(left,top,right,bottom)方法设置的。
LayoutParams
          比如我们有的时候需要给ViewGrop里面的子控件设置margin等等相关的属性值,直接在布局文件里面进行书写完成以后.仍然是没有效果的.因为测量和布局都是我们自己实现的,我们在onLayout()中没有根据Margin来布局,当然不会出现有关Margin的效果。而且默认的generateLayoutParams()函数只会提取layout_width、layout_height的值,只有MarginLayoutParams()才具有提取margin间距的功能.还需要特别注意的是,如果我们在onLayout()中根据margin来布局的话,那么我们在onMeasure()中计算容器的大小时,也要加上margin,不然会导致容器太小,而控件显示不全的问题.
         LayoutParams相当于一个Layout的信息包,它封装了Layout的位置、高、宽等信息。假设在屏幕上一块区域是由一个Layout占领的,如果将一个View添加到一个Layout中,最好告诉Layout用户期望的布局方式,也就是将一个认可的layoutParams传递进去。

参数  1.  AttributeSet attrs                           xml解析inflate时生成和容器类型匹配的布局LayoutParams

          2.  ViewGroup.LayoutParams p      传入viewGroupLayoutParams 然后生成和容器类型匹配的布局LayoutParams

总结:

           这个方法主要是用于被子View调用。

          生成和此容器类型相匹配的布局参数类。

@Overridepublic LayoutParams generateLayoutParams(AttributeSet attrs){return new MarginLayoutParams(getContext(), attrs);} @Overrideprotected LayoutParams generateDefaultLayoutParams(){return new MarginLayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);} @Overrideprotected LayoutParams generateLayoutParams(LayoutParams p){return new MarginLayoutParams(p);}

调用的时候就可以取得相关的参数:
cParams = (MarginLayoutParams) childView.getLayoutParams();int bottomMargin = cParams.bottomMargin;int topMargin = cParams.topMargin;int leftMargin = cParams.leftMargin;int rightMargin = cParams.rightMargin;
LayoutParams继承于Android.View.ViewGroup.LayoutParams.
            View通过LayoutParams类告诉其父视图它想要地大小(即,长度和宽度)。 因此,每个View都包含一个ViewGroup.LayoutParams类或者其派生类,View类依赖于ViewGroup.LayoutParams ViewGroup子类可以实现自定义LayoutParams,自定义LayoutParams提供了更好地扩展性。
          LayoutParams相当于一个Layout的信息包,它封装了Layout的位置、高、宽等信息。假设在屏幕上一块区域是由一个Layout占领的,如果将一个View添加到一个Layout中,最好告诉Layout用户期望的布局方式,也就是将一个认可的layoutParams传递进去。
          可以这样去形容LayoutParams,在象棋的棋盘上,每个棋子都占据一个位置,也就是每个棋子都有一个位置的信息,如这个棋子在4行4列,这里的“4行4列”就是棋子的LayoutParams。
但LayoutParams类也只是简单的描述了宽高,宽和高都可以设置成三种值:
1,一个确定的值;
2,FILL_PARENT,即填满(和父容器一样大小);
3,WRAP_CONTENT,即包裹住组件就好。

           通过继承并且扩展LayoutParams类可以增加更多的属性.
/**
* 使用自定义LayoutParams必须重写下面的四个方法
*/
@Override
protected boolean checkLayoutParams(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);
}
0 0
原创粉丝点击