您的位置:首页 > 其它

自定义ViewGroup练习一

2017-06-08 18:51 190 查看

学习目标

熟悉并掌握自定义ViewGroup控件流程与开发细节



概述

ViewGroup
是 View 的容器类,我们常用的
LinearLayout
RelativeLayout
等都是 ViewGroup 的子类。因为 ViewGroup 有很多子 View,所以它的整个绘制过程相对于 View 会复杂一点,但还是三个步骤 measure,layout,draw。

Measure

Measure
过程还是测量 ViewGroup 的大小。

如果
layout_width
layout_height
是 match_parent 或具体的
xxdp
就很简单了,直接调用
setMeasuredDimension()
方法,设置 ViewGroup 的宽高即可;

如果是
wrap_content
,我们需要遍历所有的子 View,然后对每个子 View 进行测量,然后根据子 View 的排列规则,计算出最终 ViewGroup 的大小。

过程中用到
getChildCount()
方法,返回子 View 的数量,
measureChild()
方法,调用子 View 的测量方法。

Layout

Layout
过程其实就是对子 View 的位置进行排列。

其中
child.layout(left,top,right,bottom)
方法可以对子 View 的位置进行设置。

Draw

在该阶段,就是按照子 View 的排列顺序,调用子 View 的
onDraw()
方法。

因为
ViewGroup
只是 View 的容器,本身一般不需要
draw
额外的修饰,所以在 onDraw() 方法里面,只需要调用 ViewGroup 的 onDraw() 默认实现方法即可。

还有一个很重要的概念 LayoutParams

LayoutParams
存储了子 View 在加入 ViewGroup 中时的一些参数信息。

在继承 ViewGroup 类时,一般也需要新建一个新的 LayoutParams 类,就想 SDK 中我们所熟悉的 LinearLayout.LayoutParams,RelativeLayout.LayoutParams 类等一样。

具体操作步骤如下

在自定义的 ViewGroup 子类中,新建一个 LayoutParams 类继承与
ViewGroup.LayoutParams


/***
*
* LayoutParams 存储了子 view 在加入 ViewGroup 中时的一些参数信息
* 在继承 ViewGroup 类时,一般也需要新建一个新的 LayoutParams 类
* */

class LayoutParams extends  ViewGroup.LayoutParams {

public int left = 0;
public int top = 0;

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

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

public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}


有了新的 LayoutParams,接下来就是如何让我们自定义的 ViewGroup 使用我们自定义的 LayoutParams 类来添加子 View。

ViewGroup 中同样提供了几个方法供我们重写,我们只要重写这些方法然后返回我们自定义的 LayoutParams 对象即可。

/***
*
* 有了新的 LayoutParams 类,就要让新自定义的 ViewGroup 使用我们自定义的 LayoutParaams 类
* 来添加子 view ,ViewGroup 提供了下面几个方法,我们重写返回我们自己的 LayoutParams 对象即可
*
* */

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

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

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

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


应用实例

首先在 values/attrs 文件下定义新的自定义控件属性

这里随便定义了图片间的横向和竖向间隔

<declare-styleable name="ninephotoview">
<attr name="ninephoto_hspace" format="dimension"/>
<attr name="ninephoto_vspace" format="dimension"/>
</declare-styleable>


在新建的自定义 ViewGroup 子类中,这里贴出主要代码

public NinePhotoView(Context context, AttributeSet attrs, int defStyleAttr) {

......

// 初始状态新建一个子 view 作为添加图片的标识
addPhotoView = new View(context);
addView(addPhotoView);
// 记录 ViewGroup 容器中 view 的数量
mImageResArrayList.add(1);
}

/**
*  Measure 过程还是测量 ViewGroup 的大小
*  如果layout_widht和layout_height是match_parent或具体的xxxdp,就很简单了,直接调用
*  setMeasuredDimension()方法,设置ViewGroup的宽高即可
*
*  如果是 wrap_content,就比较麻烦了,我们需要遍历所有的子View,然后对每个子View进行测量,
*  然后根据子View的排列规则,计算出最终ViewGroup的大小。
*
*
*  子 view 四个一排,而且都是正方形,所以通过循环很好的得到所有子 view 的位置
*  把子 view 的左上角坐标存储到我们自定义的 LayoutParams 的 left 和 top 二个字段中,Layout 阶段会使用
*  最后算出整个 ViewGroup 的宽高
* */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

// 获取容器宽高 size
int rw = MeasureSpec.getSize(widthMeasureSpec);
int rh = MeasureSpec.getSize(heightMeasureSpec);

// 控制控件图片都在屏幕范围内显示完整
childWidth = rw / 5; // 5:也可以是其它值,自己调整看效果
childHeight = childWidth;

// 获得子 view 数量
int childCount = this.getChildCount();
// 遍历子 view, 将子 view 的left,top 存入子 view 的 LayoutParams
for (int i = 0; i < childCount; i++) {
View child = this.getChildAt(i);
NinePhotoView.LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
//            layoutParams.left = (i % 3) * (childWidth + hSpace);
//            layoutParams.top = (i / 3) * (childWidth + vSpace);

// 以微信 3 行 4 列为例
// 设置每个子 view 的 left 和 top
// 横向排列的每列 left 扩大一倍
layoutParams.left =
d642
(i % 4) * childWidth;
// 竖直方向的每行 top 扩大一倍
layoutParams.top = (i / 4) * childHeight;
// 还可以加入图片间距等
}
// 这样就把每张图片的 left 和 top 位置保存到了 layoutParams

// 使用默认容器宽高
//            setMeasuredDimension(widthMeasureSpec,heightMeasureSpec);
// 定义控件的宽高
int nineWidth = rw;
int nineHeight = rh;

Map<Integer,Integer> line = new HashMap<>();
for (int i = 1; i < 10; i++) {
if (i < 5) {
line.put(i,1);
} else if (i < 9) {
line.put(i,2);
} else {
line.put(i,3);
}

}

nineHeight = (line.get(childCount)) * childHeight;

setMeasuredDimension(nineWidth,nineHeight);
}

......

// 对子 view 进行位置排列
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 获得子 view 数量
int childCount = this.getChildCount();
// 遍历子 view,取出存储在子 view 的 LayoutParams 中的 left 和 top

for (int i =0; i < childCount; i++) {
View child  = this.getChildAt(i);
NinePhotoView.LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
// 对子 view 位置进行设置
child.layout(layoutParams.left,layoutParams.top,layoutParams.left + childWidth,
layoutParams.top + childHeight);

if (i == mImageResArrayList.size() - 1 ) {

// 需要注意的是,当选到最大上传图片数的倒数第二张图片时,会继续产生添加图片的占位符,导致
// 子 view 数量已经达到最大上传图片数,这时点击最后的添加图片图标时,我们的操作不能再是生成子 view ,
// 而是将最后一张的上传图标换成我们的图片
// 如果不注意上面这个问题,会出现选择添加倒数第二张图片时,最后一张也被添加。因为
if (mImageResArrayList.size() > MAX_PHOTO_NUMBER) {
child.setBackgroundResource(constImageIds[i]);
child.setOnClickListener(null);
} else {
child.setBackgroundResource(R.drawable.add);
child.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
addPhotoBtnClick();
}
});
}
}
else {
child.setBackgroundResource(constImageIds[i]);
child.setOnClickListener(null);
}
}

}

private void addPhotoBtnClick() {
final CharSequence[] items = { "拍照", "从相册选择" };

AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setItems(items, new DialogInterface.OnClickListener() {

@Override
public void onClick(DialogInterface arg0, int arg1) {
addPhoto();
}

});
builder.show();
}

private void addPhoto() {
// 在数量范围内添加图片
if (mImageResArrayList.size() < MAX_PHOTO_NUMBER) {
View newChild = new View(getContext());
addView(newChild);
// 每添加一个子 view ,计数加1
}
mImageResArrayList.add(1);
// 重新调用 onLayout() 进行重新排列
requestLayout();
// 重新绘制 View
invalidate();
}

/***
*
* 有了新的 LayoutParams 类,就要让新自定义的 ViewGroup 使用我们自定义的 LayoutParaams 类
* 来添加子 view ,ViewGroup 提供了下面几个方法,我们重写返回我们自己的 LayoutParams 对象即可
*
* */

......

/***
*
* LayoutParams 存储了子 view 在加入 ViewGroup 中时的一些参数信息
* 在继承 ViewGroup 类时,一般也需要新建一个新的 LayoutParams 类
* */

class LayoutParams extends  ViewGroup.LayoutParams {

public int left = 0;
public int top = 0;

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

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

public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
}


过程中出现的问题

在添加图片点击到倒数第二张的添加图标的时候,最后一张也跟着出来了。

因为图片控制在 9 张,那么当成功添加 8 张图片的时候,ViewGroup 容器里已经有 9 个字 View(包括了图片添加图标),当在点击第 9 个子 View 时,不能再添加子 View ,我们要把 最后的图标换成我们的图片。这个逻辑主要是在
onLayout()
方法中我们需要注意。还是把代码单独贴出来重视一下

// 对子 view 进行位置排列
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 获得子 view 数量
int childCount = this.getChildCount();
// 遍历子 view,取出存储在子 view 的 LayoutParams 中的 left 和 top

for (int i =0; i < childCount; i++) {
View child  = this.getChildAt(i);
NinePhotoView.LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
// 对子 view 位置进行设置
child.layout(layoutParams.left,layoutParams.top,layoutParams.left + childWidth,
layoutParams.top + childHeight);

if (i == mImageResArrayList.size() - 1 ) {

// 需要注意的是,当选到最大上传图片数的倒数第二张图片时,会继续产生添加图片的占位符,导致
// 子 view 数量已经达到最大上传图片数,这时点击最后的添加图片图标时,我们的操作不能再是生成子 view ,
// 而是将最后一张的上传图标换成我们的图片
// 如果不注意上面这个问题,会出现选择添加倒数第二张图片时,最后一张也被添加。因为
if (mImageResArrayList.size() > MAX_PHOTO_NUMBER) {
child.setBackgroundResource(constImageIds[i]);
child.setOnClickListener(null);
} else {
child.setBackgroundResource(R.drawable.add);
child.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
addPhotoBtnClick();
}
});
}
}
else {
child.setBackgroundResource(constImageIds[i]);
child.setOnClickListener(null);
}
}

}


还有在设置 ViewGroup 宽高的时候,要根据子 View 的宽高和具体要求来具体设置,还是单独拿出代码重视一下

// 定义控件的宽高
int nineWidth = rw;
int nineHeight = rh;

Map<Integer,Integer> line = new HashMap<>();
for (int i = 1; i < 10; i++) {
if (i < 5) {
line.put(i,1);
} else if (i < 9) {
line.put(i,2);
} else {
line.put(i,3);
}

}

nineHeight = (line.get(childCount)) * childHeight;

setMeasuredDimension(nineWidth,nineHeight);


Github 源码下载

重要参考:

教你搞定Android自定义ViewGroup
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  控件