ViewGroup 自定义演示

第一部分:利用系统属性自定义ViewGroup

1、ViewGroup的职责是啥?
ViewGroup相当于一个放置View的容器,ViewGroup的职能为:给childView计算出建议的宽和高和测量模式 ;决定childView的位置;为什么只是建议的宽和高,而不是直接确定呢,因为childView宽和高可以设置为wrap_content,这样只有childView才能计算出自己的宽和高。
2、View的职责是啥?
View的职责,根据测量模式和ViewGroup给出的建议的宽和高,计算出自己的宽和高;同时还有个更重要的职责是:在ViewGroup为其指定的区域内绘制自己的形态。
总结: 根据ViewGroup传人的测量值和模式,View对自己宽高进行确定(onMeasure中完成),然后在onDraw中完成对自己的绘制。
ViewGroup需要给View传入view的测量值和模式(onMeasure中完成),而且对于此ViewGroup的父布局,自己也需要在onMeasure中完成对自己宽和高的确定。此外,需要在onLayout中完成对其childView的位置的指定。
举例1: 定义一个ViewGroup,内部可以传入0到4个childView,分别依次显示在左上角,右上角,左下角,右下角。利用系统的 MarginLayoutParams,因为只需要ViewGroup能够支持margin即可



public class ViewGroupTest1 extends ViewGroup {
    public ViewGroupTest1(Context context) {
        super(context);
    }

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

    public ViewGroupTest1(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    //该方法是用来设置ViewGroup 布局参数  指定了其LayoutParams为MarginLayoutParams
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(),attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //对子view 进行测量
        measureChildren(widthMeasureSpec,heightMeasureSpec);
        /**
         * 如果ViewGroup是wrap_content时,需要对ViewGroup采用自定义的测试方式进行测量它的宽和高
         */
        int width = 0;
        int height = 0;

        int cWidth = 0;
        int cHeight = 0;
        MarginLayoutParams cParams = null;
        // 用于计算左边两个childView的高度
        int lHeight = 0;
        // 用于计算右边两个childView的高度,最终高度取二者之间大值
        int rHeight = 0;
        // 用于计算上边两个childView的宽度
        int tWidth = 0;
        // 用于计算下面两个childiew的宽度,最终宽度取二者之间大值
        int bWidth = 0;
        int count = getChildCount();
        for (int i=0;i<count;i++){
            View viewChildren = getChildAt(i);
            cWidth = viewChildren.getMeasuredWidth();
            cHeight = viewChildren.getMeasuredHeight();
            cParams = (MarginLayoutParams) viewChildren.getLayoutParams();
            // 上面两个childView
            if (i == 0 || i == 1)
            {
                tWidth += cWidth + cParams.leftMargin + cParams.rightMargin;
            }

            if (i == 2 || i == 3)
            {
                bWidth += cWidth + cParams.leftMargin + cParams.rightMargin;
            }

            if (i == 0 || i == 2)
            {
                lHeight += cHeight + cParams.topMargin + cParams.bottomMargin;
            }

            if (i == 1 || i == 3)
            {
                rHeight += cHeight + cParams.topMargin + cParams.bottomMargin;
            }
        }
        width = Math.max(tWidth, bWidth);
        height = Math.max(lHeight, rHeight);
        boolean isWidthExActly = widthMode == MeasureSpec.EXACTLY;
        boolean isHeightWxactly = heightMode == MeasureSpec.EXACTLY;
        Log.i("niuniu " , " widthMode  " + isWidthExActly + "  widthSize " + widthSize + "   width " +width);
        Log.i("niuniu " , " heightMode  " + isHeightWxactly + "  heightSize " + heightSize + "   height " +height);
        //将得到的宽高 通过setMeasuredDimension方法设置进去,完成测量工作.
        setMeasuredDimension(widthMode == MeasureSpec.EXACTLY? widthSize:width,heightMode == MeasureSpec.EXACTLY?heightSize:height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int cCount = getChildCount();
        int cWidth = 0;
        int cHeight = 0;
        MarginLayoutParams cParams = null;

        /**
         * 根据childView的宽和高,以及margin,计算childView在GruopView的位置(l, t, r, b) 并使用layout进行布局
         */
        for (int i = 0; i < cCount; i++)
        {
            View childView = getChildAt(i);
            cWidth = childView.getMeasuredWidth();
            cHeight = childView.getMeasuredHeight();
            cParams = (MarginLayoutParams) childView.getLayoutParams();
//            Log.i("niuniu", " cParams.leftMargin :" +cParams.leftMargin + " cParams.topMargin " +
//                    cParams.topMargin + "  cParams.rightMargin:  " +cParams.rightMargin + "  cParams.bottomMargin  " + cParams.bottomMargin);

            int cl = 0, ct = 0, cr = 0, cb = 0;

            switch (i)
            {
                case 0:
                    cl = cParams.leftMargin;
                    ct = cParams.topMargin;
                    break;
                case 1:
                    cl = getWidth() - cWidth - cParams.rightMargin;
                    ct = cParams.topMargin;
                    break;
                case 2:
                    cl = cParams.leftMargin;
                    ct = getHeight() - cHeight - cParams.bottomMargin;
                    break;
                case 3:
                    cl = getWidth() - cWidth- cParams.rightMargin;
                    ct = getHeight() - cHeight - cParams.bottomMargin;
                    break;
            }
            cr = cl + cWidth;
            cb = cHeight + ct;
            childView.layout(cl, ct, cr, cb);
        }
    }
}

//activity_main.xml 中引用 
<com.example.nft.myapplication.ViewGroupTest1
        android:layout_width="match_parent"
        android:layout_height="500dp"
        android:id="@+id/viewGruop1"
        android:layout_marginTop="15dp">
        <TextView
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:layout_marginLeft="20dp"
            android:layout_marginBottom="800dp"
            android:textSize="50dp"
            android:text="1"
            android:background="#FF4444"
            android:gravity="center"
            android:textStyle="bold"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:textSize="50dp"
            android:text="2"
            android:background="#00ff00"
            android:gravity="center"
            android:textStyle="bold"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="45dp"
            android:textSize="50dp"
            android:text="3"
            android:background="#0044ff"
            android:gravity="center"
            android:textStyle="bold"/>
        <TextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_marginHorizontal="50dp"
            android:textSize="50dp"
            android:text="4"
            android:background="#ff6600"
            android:gravity="center"
            android:textStyle="bold"/>
    </com.example.nft.myapplication.ViewGroupTest1>

对子view进行测量 也可以使用measureChild方法

  int count = getChildCount();
  for (int i = 0; i < count; i++) {
    final View child = getChildAt(i);
    measureChild(child, widthMeasureSpec, heightMeasureSpec);
  }

而使用measureChildren() 方法来简化上面的代码,这个方法将自动遍历所有子view并让它们测量自己,还可以忽略那些visibility 设置为gone的子view,因此它支持visibility gone标志.

第二部分 自定义ViewGroup 定义自己的属性

当使用不同的布局方式时,子view得布局属性就不太一样,比如当父布局是LinearLayout时,子view可以使用父布局属性如layout_weight、weightSum、layout_gravity等;当使用的是RelativeLayout时,其子view就能使用属于父布局的有效属性layout_centerInParent等;因此不同的布局容器,有不同的布局属性, 当需要我们的自定义容器需要定义自己的布局属性时,就必须使用LayoutParams来实现.
先来简单看看viewGroup的addView方法

public void addView(View child, int index) {
        ...
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
            params = generateDefaultLayoutParams();
         ....
        }
        addView(child, index, params);
    } 
public void addView(View child, int index, LayoutParams params) {
     ...
    addViewInner(child, index, params, false);
}

private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {
  ...

    if (!checkLayoutParams(params)) {
        params = generateLayoutParams(params);
    }

    if (preventRequestLayout) {
        child.mLayoutParams = params;
    } else {
        child.setLayoutParams(params);
    }
}

首先是checkLayoutParams,目的是检测这个参数是否为空,如果为空的话就给它生成一个普通的LayoutParams; 实现布局参数转换成自定义的参数,如下三个方法就显得尤为重要了。

    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return  p != null;
    }
  protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
      return p;
  }
  public LayoutParams generateLayoutParams(AttributeSet attrs) {
      return new LayoutParams(getContext(), attrs);
  }

比如 FrameLayout.LayoutParams中就自定义了一个Gravity属性,FrameLayout实现了addView的这三个方法

public static class LayoutParams extends MarginLayoutParams {

    public int gravity = -1;

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

        TypedArray a = c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.FrameLayout_Layout);
        gravity = a.getInt(com.android.internal.R.styleable.FrameLayout_Layout_layout_gravity, -1);
        a.recycle();
    }

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

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

    ....
    public LayoutParams(LayoutParams source) {
        super(source);
        this.gravity = source.gravity;
    }
}
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new FrameLayout.LayoutParams(getContext(), attrs);        
    }

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

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

举例2 自定义Group中添加layout_bg,layout_orientation 属性,供子view来使用.


public class ViewGroupTest2 extends ViewGroup {
    private int orientation = 0;
    public ViewGroupTest2(Context context) {
        super(context);
    }

    public ViewGroupTest2(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray  = context.obtainStyledAttributes(attrs,R.styleable.ViewGroupTest2);
        orientation = typedArray.getInt(R.styleable.ViewGroupTest2_layout_orientation,0);
        typedArray.recycle();

    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec,heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;
        if(widthMode == MeasureSpec.EXACTLY){
            width = widthSize;
        } else{
            width = widthSize+500;
        }

        if (heightMode == MeasureSpec.EXACTLY){
            height = heightSize;
        }else {
            height = heightSize+600;
        }

        setMeasuredDimension(width,height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int count = getChildCount();
        int width ;
        int height ;
        int distance = 0;
        MyLayoutParams params = null;
        for (int i = 0;i<count;i++){
             View view = getChildAt(i);
             params = (MyLayoutParams) view.getLayoutParams();
             view.setBackgroundColor(params.color);
             width = view.getMeasuredWidth();
             height = view.getMeasuredHeight();

            int cl = 0, ct = 0, cr = 0, cb = 0;
            if(orientation == 0){ // 水平一次排列
                cl = distance;
                ct = 80;
            } else { //垂直依次排列
                ct = distance;
                cl = 80;
            }
            cr = cl + width;
            cb = height + ct;
            view.layout(cl, ct, cr, cb);
            // 计算下一个子view的左边距离 或者顶部距离
            if (orientation == 0){
                distance += width +20;
            } else {
                distance += height+40;
            }

        }
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        Log.i("niuniu", " generateLayoutParams attrs ");
        return new MyLayoutParams(getContext(),attrs);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        Log.i("niuniu", " generateLayoutParams  p ");
        return new MyLayoutParams(p);
    }

    @Override
    protected boolean checkLayoutParams(LayoutParams p) {
        boolean params  = p instanceof  LayoutParams;
        Log.i("niuniu", " checkLayoutParams  params " + params);
        return params;
    }
}

//创建自己的LayoutParams 并获取父容器所支持的属性

public class MyLayoutParams extends ViewGroup.LayoutParams {

    public int color ;
    public MyLayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        Log.i("niuniu", " MyLayoutParams attrs ");
        TypedArray ta = c.obtainStyledAttributes(attrs,R.styleable.MyParams);
        color = ta.getColor(R.styleable.MyParams_layout_bg, Color.DKGRAY);
        ta.recycle();
    }

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

    public MyLayoutParams(ViewGroup.LayoutParams source) {
        super(source);
    }
    public MyLayoutParams(MyLayoutParams source) {
        super(source);
        Log.i("niuniu", " MyLayoutParams source ");
    }
}
activity_main.xml中引入该父容器 
    <com.example.nft.myapplication.ViewGroupTest2
        xmlns:viewGroupTest2 = "http://schemas.android.com/apk/res/com.example.nft.myapplication"
        android:layout_width="wrap_content"
        android:layout_height="800dp"
        android:id="@+id/viewGroup2"
        android:layout_marginTop="15dp"
        viewGroupTest2:layout_orientation = "horital"
        >
        <TextView
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:textStyle="bold"
            android:text="1"
            android:textSize="24dp"
            viewGroupTest2:layout_bg="#FF4444"/>
        <TextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:textStyle="bold"
            android:text="2"
            android:textSize="24dp"
            viewGroupTest2:layout_bg="#bb6cc0"/>
        <TextView
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:textStyle="bold"
            android:text="3"
            android:textSize="24dp"
            viewGroupTest2:layout_bg="#66ff00"/>
        <TextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:textStyle="bold"
            android:text="4"
            android:textSize="24dp"
            viewGroupTest2:layout_bg="#6f60f0"/>
    </com.example.nft.myapplication.ViewGroupTest2>

log 输出:
generateLayoutParams attrs ;
MyLayoutParams attrs ;
checkLayoutParams params true
总结 :
在xml中引入这个ViewGroupTest2 布局,会调用public 的generateLayoutParams(atters)方法来给子view生成自定义的布局参数MyLayoutParam.

猜你喜欢

转载自blog.csdn.net/dakaniu/article/details/78743501