第一部分:利用系统属性自定义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.