自定义viewgroup总结

来源:互联网 发布:seo建站公司 编辑:程序博客网 时间:2024/05/21 17:28

自定义viewgroup子类实现时必须实现调用其子view的measure和layout方法来测量和布局其子view

前一篇文章主要讲了自定义View为什么要重载onMeasure()方法(见 http://www.linuxidc.com/Linux/2014-12/110164.htm),那么,自定义ViewGroup又都有哪些方法需要重载或者实现呢 ?

Android开发中,对于自定义View,分为两种,一种是自定义控件(继承View类),另一种是自定义布局容器(继承ViewGroup)。如果是自定义控件,则一般需要重载两个方法,一个是onMeasure(),用来测量控件尺寸,另一个是onDraw(),用来绘制控件的UI。而自定义布局容器,则一般需要实现/重载三个方法,一个是onMeasure(),也是用来测量尺寸;一个是onLayout(),用来布局子控件;还有一个是dispatchDraw(),用来绘制UI。

绘制VIew本身的内容,通过调用View.onDraw(canvas)函数实现

绘制自己的孩子通过dispatchDraw(canvas)实现

 

    View组件的绘制会调用draw(Canvas canvas)方法,draw过程中主要是先画Drawable背景,对 drawable调用setBounds()然后是draw(Canvas c)方法.有点注意的是背景drawable的实际大小会影响view组件的大小,drawable的实际大小通过getIntrinsicWidth()和getIntrinsicHeight()获取,当背景比较大时view组件大小等于背景drawable的大小

     画完背景后,draw过程会调用onDraw(Canvas canvas)方法,然后就是dispatchDraw(Canvas canvas)方法, dispatchDraw()主要是分发给子组件进行绘制,我们通常定制组件的时候重写的是onDraw()方法。值得注意的是ViewGroup容器组件的绘制,当它没有背景时直接调用的是dispatchDraw()方法, 而绕过了draw()方法,当它有背景的时候就调用draw()方法,而draw()方法里包含了dispatchDraw()方法的调用。因此要在ViewGroup上绘制东西的时候往往重写的是dispatchDraw()方法而不是onDraw()方法,或者自定制一个Drawable,重写它的draw(Canvas c)和 getIntrinsicWidth(), 

getIntrinsicHeight()方法,然后设为背景。


本文主要分析自定义ViewGroup的onLayout()方法的实现。

ViewGroup类的onLayout()函数是abstract型,继承者必须实现,由于ViewGroup的定位就是一个容器,用来盛放子控件的,所以就必须定义要以什么的方式来盛放,比如LinearLayout就是以横向或者纵向顺序存放,而RelativeLayout则以相对位置来摆放子控件,同样,我们的自定义ViewGroup也必须给出我们期望的布局方式,而这个定义就通过onLayout()函数来实现。

我们通过实现一个水平优先布局的视图容器来更加深入地了解onLayout()的实现吧,效果如图所示(黑色方块为子控件,白色部分为自定义布局容器)。该容器的布局方式是,首先水平方向上摆放子控件,水平方向放不下了,则另起一行继续水平摆放。

Android开发实践:自定义ViewGroup的onLayout()分析

1.  自定义ViewGroup的派生类

第一步,则是自定ViewGroup的派生类,继承默认的构造函数。

public class CustomViewGroup extends ViewGroup {
 
  public CustomViewGroup(Context context) {
      super(context);   
    }
 
  public CustomViewGroup(Context context, AttributeSet attrs) {
      super(context, attrs);   
    }
   
  public CustomViewGroup(Context context, AttributeSet attrs, intdefStyle) {
      super(context, attrs, defStyle);
    }
}

2.  重载onMeasure()方法

为什么要重载onMeasure()方法这里就不赘述了,上一篇文章已经讲过,这里需要注意的是,自定义ViewGroup的onMeasure()方法中,除了计算自身的尺寸外,还需要调用measureChildren()函数来计算子控件的尺寸。

onMeasure()的定义不是本文的讨论重点,因此这里我直接使用默认的onMeasure()定义,当然measureChildren()是必须得加的。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec); 
}

3.  实现onLayout()方法
onLayout()函数的原型如下:

//@param changed 该参数指出当前ViewGroup的尺寸或者位置是否发生了改变
//@param left top right bottom 当前ViewGroup相对于其父控件的坐标位置
protected void onLayout(boolean changed,int left, int top, int right, int bottom);

由于我们希望优先横向布局子控件,那么,首先,我们知道总宽度是多少,这个值可以通过getMeasuredWidth()来得到,当然子控件的宽度也可以通过子控件对象的getMeasuredWidth()来得到。

这样,就不复杂了,具体的实现代码如下所示:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
   
    int mViewGroupWidth  = getMeasuredWidth();  //当前ViewGroup的总宽度     
 
    int mPainterPosX = left;  //当前绘图光标横坐标位置
    int mPainterPosY = top;  //当前绘图光标纵坐标位置 
   
    int childCount = getChildCount();       
    for ( int i = 0; i < childCount; i++ ) {
       
        View childView = getChildAt(i);
 
        int width  = childView.getMeasuredWidth();
        int height = childView.getMeasuredHeight();           
                   
        //如果剩余的空间不够,则移到下一行开始位置
        if( mPainterPosX + width > mViewGroupWidth ) {             
            mPainterPosX = left;
            mPainterPosY += height;
        }                   
       
        //执行ChildView的绘制
        childView.layout(mPainterPosX,mPainterPosY,mPainterPosX+width, mPainterPosY+height);
       
        //记录当前已经绘制到的横坐标位置
        mPainterPosX += width;
    }     
}

4. 布局文件测试

下面我们就尝试写一个简单的xml文件,来测试一下我们的自定义ViewGroup,我们把子View的背景颜色都设置为黑色,方便我们辨识。

<com.titcktick.customview.CustomViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <View
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_margin="10dp"
        android:background="@android:color/black"/>
   
    <View
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_margin="10dp"
        android:background="@android:color/black"/>   
       
    <View
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_margin="10dp"
        android:background="@android:color/black"/>
   
    <View
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_margin="10dp"
        android:background="@android:color/black"/>   
 
</com.titcktick.customview.CustomViewGroup>

5. 添加layout_margin

为了让核心逻辑更加清晰,上面的onLayout()实现我隐去了margin的计算,这样就会导致子控件的layout_margin不起效果,所以上述效果是子控件一个个紧挨着排列,中间没有空隙。那么,下面我们来研究下如何添加margin效果。

其实,如果要自定义ViewGroup支持子控件的layout_margin参数,则自定义的ViewGroup类必须重载generateLayoutParams()函数,并且在该函数中返回一个ViewGroup.MarginLayoutParams派生类对象,这样才能使用margin参数。

ViewGroup.MarginLayoutParams的定义关键部分如下,它记录了子控件的layout_margin值:

public static class MarginLayoutParams extends ViewGroup.LayoutParams {       
    public int leftMargin;
    public int topMargin;
    public int rightMargin;
    public int bottomMargin;
}

你可以跟踪源码看看,其实XML文件中View的layout_xxx参数都是被传递到了各种自定义ViewGroup.LayoutParams派生类对象中。例如LinearLayout的LayoutParams定义的关键部分如下:

public class LinearLayout extends ViewGroup {
 
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
 
    public float weight;
    public int gravity = -1;
 
    public LayoutParams(Context c, AttributeSet attrs) {
 
            super(c, attrs);
 
            TypedArray a = c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
            weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
            gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);
 
            a.recycle();
        }
    }
 
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LinearLayout.LayoutParams(getContext(), attrs);
    }
}

这样你大概就可以理解为什么LinearLayout的子控件支持weight和gravity的设置了吧,当然我们也可以这样自定义一些属于我们ViewGroup特有的params,这里就不详细讨论了,我们只继承MarginLayoutParams来获取子控件的margin值。

public class CustomViewGroup extends ViewGroup {
 
    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);           
        }     
    }
 
    @Override 
    public LayoutParams generateLayoutParams(AttributeSet attrs) { 
        return new CustomViewGroup.LayoutParams(getContext(), attrs); 
    }
 
}

这样修改之后,我们就可以在onLayout()函数中获取子控件的layout_margin值了,添加了layout_margin的onLayout()函数实现如下所示:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
 
    int mViewGroupWidth  = getMeasuredWidth();  //当前ViewGroup的总宽度
    int mViewGroupHeight = getMeasuredHeight(); //当前ViewGroup的总高度
 
    int mPainterPosX = left; //当前绘图光标横坐标位置
    int mPainterPosY = top;  //当前绘图光标纵坐标位置 
   
    int childCount = getChildCount();       
    for ( int i = 0; i < childCount; i++ ) {
       
        View childView = getChildAt(i);
 
        int width  = childView.getMeasuredWidth();
        int height = childView.getMeasuredHeight();           
 
        CustomViewGroup.LayoutParams margins = (CustomViewGroup.LayoutParams)(childView.getLayoutParams());
       
        //ChildView占用的width  = width+leftMargin+rightMargin
        //ChildView占用的height = height+topMargin+bottomMargin
        //如果剩余的空间不够,则移到下一行开始位置
        if( mPainterPosX + width + margins.leftMargin + margins.rightMargin > mViewGroupWidth ) {             
            mPainterPosX = left;
            mPainterPosY += height + margins.topMargin + margins.bottomMargin;
        }                   
       
        //执行ChildView的绘制
        childView.layout(mPainterPosX+margins.leftMargin, mPainterPosY+margins.topMargin,mPainterPosX+margins.leftMargin+width, mPainterPosY+margins.topMargin+height);
       
        mPainterPosX += width + margins.leftMargin + margins.rightMargin;
    }     
}

6.  总结

费了好大劲,终于算是把自定义ViewGroup的onLayout()相关知识点讲清楚了,如果有任何疑问欢迎留言或者来信lujun.hust@gmail.com交流,如果喜欢本文欢迎转载,但希望能尊重我的劳动成果,给出本文文链接,谢谢。


Class Overview

A ViewGroup is a special view that can contain other views (called children.) The view group is the base class for layouts and views containers. This class also defines theViewGroup.LayoutParams class which serves as the base class for layouts parameters.

Also see ViewGroup.LayoutParams for layout attributes.

Developer Guides

For more information about creating user interface layouts, read the XML Layouts developer guide.

Here is a complete implementation of a custom ViewGroup that implements a simpleFrameLayout along with the ability to stack children in left and right gutters.

import android.content.Context;import android.content.res.TypedArray;import android.util.AttributeSet;import android.view.Gravity;import android.view.View;import android.view.ViewGroup;import android.widget.RemoteViews;/** * Example of writing a custom layout manager.  This is a fairly full-featured * layout manager that is relatively general, handling all layout cases.  You * can simplify it for more specific cases. */@RemoteViews.RemoteViewpublic class CustomLayout extends ViewGroup {    /** The amount of space used by children in the left gutter. */    private int mLeftWidth;    /** The amount of space used by children in the right gutter. */    private int mRightWidth;    /** These are used for computing child frames based on their gravity. */    private final Rect mTmpContainerRect = new Rect();    private final Rect mTmpChildRect = new Rect();    public CustomLayout(Context context) {        super(context);    }    public CustomLayout(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public CustomLayout(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);    }    /**     * Any layout manager that doesn't scroll will want this.     */    @Override    public boolean shouldDelayChildPressedState() {        return false;    }    /**     * Ask all children to measure themselves and compute the measurement of this     * layout based on the children.     */    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int count = getChildCount();        // These keep track of the space we are using on the left and right for        // views positioned there; we need member variables so we can also use        // these for layout later.        mLeftWidth = 0;        mRightWidth = 0;        // Measurement will ultimately be computing these values.        int maxHeight = 0;        int maxWidth = 0;        int childState = 0;        // Iterate through all children, measuring them and computing our dimensions        // from their size.        for (int i = 0; i < count; i++) {            final View child = getChildAt(i);            if (child.getVisibility() != GONE) {                // Measure the child.                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);                // Update our size information based on the layout params.  Children                // that asked to be positioned on the left or right go in those gutters.                final LayoutParams lp = (LayoutParams) child.getLayoutParams();                if (lp.position == LayoutParams.POSITION_LEFT) {                    mLeftWidth += Math.max(maxWidth,                            child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);                } else if (lp.position == LayoutParams.POSITION_RIGHT) {                    mRightWidth += Math.max(maxWidth,                            child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);                } else {                    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());            }        }        // Total width is the maximum width of all inner children plus the gutters.        maxWidth += mLeftWidth + mRightWidth;        // Check against our minimum height and width        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());        // Report our final dimensions.        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),                resolveSizeAndState(maxHeight, heightMeasureSpec,                        childState << MEASURED_HEIGHT_STATE_SHIFT));    }    /**     * Position all children within this layout.     */    @Override    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {        final int count = getChildCount();        // These are the far left and right edges in which we are performing layout.        int leftPos = getPaddingLeft();        int rightPos = right - left - getPaddingRight();        // This is the middle region inside of the gutter.        final int middleLeft = leftPos + mLeftWidth;        final int middleRight = rightPos - mRightWidth;        // These are the top and bottom edges in which we are performing layout.        final int parentTop = getPaddingTop();        final int parentBottom = bottom - top - getPaddingBottom();        for (int i = 0; i < count; i++) {            final View child = getChildAt(i);            if (child.getVisibility() != GONE) {                final LayoutParams lp = (LayoutParams) child.getLayoutParams();                final int width = child.getMeasuredWidth();                final int height = child.getMeasuredHeight();                // Compute the frame in which we are placing this child.                if (lp.position == LayoutParams.POSITION_LEFT) {                    mTmpContainerRect.left = leftPos + lp.leftMargin;                    mTmpContainerRect.right = leftPos + width + lp.rightMargin;                    leftPos = mTmpContainerRect.right;                } else if (lp.position == LayoutParams.POSITION_RIGHT) {                    mTmpContainerRect.right = rightPos - lp.rightMargin;                    mTmpContainerRect.left = rightPos - width - lp.leftMargin;                    rightPos = mTmpContainerRect.left;                } else {                    mTmpContainerRect.left = middleLeft + lp.leftMargin;                    mTmpContainerRect.right = middleRight - lp.rightMargin;                }                mTmpContainerRect.top = parentTop + lp.topMargin;                mTmpContainerRect.bottom = parentBottom - lp.bottomMargin;                // Use the child's gravity and size to determine its final                // frame within its container.                Gravity.apply(lp.gravity, width, height, mTmpContainerRect, mTmpChildRect);                // Place the child.                child.layout(mTmpChildRect.left, mTmpChildRect.top,                        mTmpChildRect.right, mTmpChildRect.bottom);            }        }    }    // ----------------------------------------------------------------------    // The rest of the implementation is for custom per-child layout parameters.    // If you do not need these (for example you are writing a layout manager    // that does fixed positioning of its children), you can drop all of this.    @Override    public LayoutParams generateLayoutParams(AttributeSet attrs) {        return new CustomLayout.LayoutParams(getContext(), attrs);    }    @Override    protected LayoutParams generateDefaultLayoutParams() {        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);    }    @Override    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {        return new LayoutParams(p);    }    @Override    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {        return p instanceof LayoutParams;    }    /**     * Custom per-child layout information.     */    public static class LayoutParams extends MarginLayoutParams {        /**         * The gravity to apply with the View to which these layout parameters         * are associated.         */        public int gravity = Gravity.TOP | Gravity.START;        public static int POSITION_MIDDLE = 0;        public static int POSITION_LEFT = 1;        public static int POSITION_RIGHT = 2;        public int position = POSITION_MIDDLE;        public LayoutParams(Context c, AttributeSet attrs) {            super(c, attrs);            // Pull the layout param values from the layout XML during            // inflation.  This is not needed if you don't care about            // changing the layout behavior in XML.            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CustomLayoutLP);            gravity = a.getInt(R.styleable.CustomLayoutLP_android_layout_gravity, gravity);            position = a.getInt(R.styleable.CustomLayoutLP_layout_position, position);            a.recycle();        }        public LayoutParams(int width, int height) {            super(width, height);        }        public LayoutParams(ViewGroup.LayoutParams source) {            super(source);        }    }}

If you are implementing XML layout attributes as shown in the example, this is the corresponding definition for them that would go inres/values/attrs.xml:

<declare-styleable name="CustomLayoutLP">    <attr name="android:layout_gravity" />    <attr name="layout_position">        <enum name="middle" value="0" />        <enum name="left" value="1" />        <enum name="right" value="2" />    </attr></declare-styleable>

Finally the layout manager can be used in an XML layout like so:

<com.example.android.apis.view.CustomLayout        xmlns:android="http://schemas.android.com/apk/res/android"        xmlns:app="http://schemas.android.com/apk/res/com.example.android.apis"        android:layout_width="match_parent"        android:layout_height="match_parent">    <!-- put first view to left. -->    <TextView            android:background="@drawable/filled_box"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            app:layout_position="left"            android:layout_gravity="fill_vertical|center_horizontal"            android:text="l1"/>    <!-- stack second view to left. -->    <TextView            android:background="@drawable/filled_box"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            app:layout_position="left"            android:layout_gravity="fill_vertical|center_horizontal"            android:text="l2"/>    <!-- also put a view on the right. -->    <TextView            android:background="@drawable/filled_box"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            app:layout_position="right"            android:layout_gravity="fill_vertical|center_horizontal"            android:text="r1"/>    <!-- by default views go in the middle; use fill vertical gravity -->    <TextView            android:background="@drawable/green"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_gravity="fill_vertical|center_horizontal"            android:text="fill-vert"/>    <!-- by default views go in the middle; use fill horizontal gravity -->    <TextView            android:background="@drawable/green"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_gravity="center_vertical|fill_horizontal"            android:text="fill-horiz"/>    <!-- by default views go in the middle; use top-left gravity -->    <TextView            android:background="@drawable/blue"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_gravity="top|left"            android:text="top-left"/>    <!-- by default views go in the middle; use center gravity -->    <TextView            android:background="@drawable/blue"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_gravity="center"            android:text="center"/>    <!-- by default views go in the middle; use bottom-right -->    <TextView            android:background="@drawable/blue"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_gravity="bottom|right"            android:text="bottom-right"/></com.example.android.apis.view.CustomLayout>
0 0
原创粉丝点击