• 注册
当前位置:1313e > 默认分类 >正文

Android自定义ViewGroup

视图分类就两类,View和ViewGroup。ViewGroup是View的子类,ViewGroup可以包含所有的View(包括ViewGroup),View只能自我描绘,不能包含其他View。

然而系统定义的ViewGroup毕竟功能有限,不能满足我们所有的需求,很简单的道理,别人不可能为你考虑所有的细节。所以我们需要自定义ViewGroup。


一个非常简单的视图包含关系:ViewGroup1->ViewGroup2->…->ViewGroupi->…->ViewGroupn->View(->为包含)

其中包含了两个极为重要的流程:尺寸测量位置摆放。尺寸测量完后再走位置摆放。

1、尺寸测量

View有一个尺寸测量方法onMeasure(int widthMeasureSpec, int heightMeasureSpec),这个方法负责设置自己的大小,以及发送建议的大小到他的children(如果他有的话)。

2、位置摆放

只有ViewGroup才有位置摆放一说。View有一个摆放方法onLayout(boolean changed, int left, int top, int right, int bottom),此方法负责摆放他的children的位置。


起码得有一个开头的ViewGroup,让其下发建议的大小下去到他的children里面。查看Activity源代码可以看到以下一个方法setContentView(View v):

@Override
public void setContentView(View v) {ensureSubDecor();ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);contentParent.removeAllViews();contentParent.addView(v);mOriginalWindowCallback.onContentChanged();
}

这个方法不要太熟悉,就是我们平时在onCreate中用的设置布局的方法。可以看到用了一个ID为android.R.id.content的视图contentParent来添加我们传入的v。contentParent是一个大小固定的ViewGroup,因为屏幕大小和状态栏的大小是固定的。我们可以把这个contentParent当作一个开头的ViewGroup。


然后就是如何传递建议的大小。

onMeasure(int widthMeasureSpec, int heightMeasureSpec)中的measureSpec(widthMeasureSpec或heightMeasureSpec)包含了两个信息:测量模式specMode和测量大小specSize。获取方法:

int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

specMode有三个模式:

1、View.EXACTLY:建议恰好以specSize大小设置

2、View.AT_MOST:建议最多以specSize大小设置

3、View.UNSPECIFIED:没有建议,任意发挥

比如一个RelativeLayout,其中包括一个View:

1、这个View设置的layout_width为100dp,那么RelativeLayout就会把View.EXACTLY和100dp组合成一个widthMeasureSpec发送到View的onMeasure方法里。

2、这个View设置的layout_width为wrap_content,那么RelativeLayout就会把View.AT_MOST和自身可用宽度组合成一个widthMeasureSpec发送到View的onMeasure方法里。

3、View.UNSPECIFIED多用于大小不可控的地方,比如ScrollView中的子视图View,ScrollView会把View.UNSPECIFIED和大小值为0组合成一个heightMeasureSpec发送到View的onMeasure方法里。

测量模式和测量大小只是建议性,至于children会不会采纳就是他们的事情了。


接下来就是如何摆放位置。

onLayout(boolean changed, int left, int top, int right, int bottom)中的left、top、right、bottom是指相对于父视图的偏移值,我们可以利用这些值来指出它的大小,width=right-left;height=bottom-top。

然后就循环children把他们摆放好。


下面进行实战

1、简单的自定义:

_thumb21

外面一个ViewGroup,里面动态添加View,View的排序为从左到右。

布局为:

<com.besttimer.study.myviewgrouptest.CustomViewGroupandroid:layout_width="300dp"android:layout_height="100dp"><com.besttimer.study.myviewgrouptest.CustomViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="#ffff0000" /><com.besttimer.study.myviewgrouptest.CustomViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="#ff00ff00" /><com.besttimer.study.myviewgrouptest.CustomViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="#ff0000ff" />
com.besttimer.study.myviewgrouptest.CustomViewGroup>

CustomViewGroup的测量方法为:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int measuredWidth = this.getMeasureSize(widthMeasureSpec, 100);int measuredHeight = this.getMeasureSize(heightMeasureSpec, 100);int childCount = this.getChildCount();int childWidth = measuredWidth / childCount;int childHeight = measuredHeight;for (int index = 0; index < childCount; index++) {View childView = this.getChildAt(index);//以MeasureSpec.EXACTLY定义测量值int widthMeasureSpec_child = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);int heightMeasureSpec_child = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);//此方法将会调用childView.onMeasure(int widthMeasureSpec, int heightMeasureSpec)
        childView.measure(widthMeasureSpec_child, heightMeasureSpec_child);}//必须要设置的大小,指定其在父视图中的大小this.setMeasuredDimension(measuredWidth, measuredHeight);}/*** 获取测量大小** @param measureSpec* @param defaultValue 默认大小* @return*/
private int getMeasureSize(int measureSpec, int defaultValue) {int result = defaultValue;int specMode = MeasureSpec.getMode(measureSpec);//测量模式int specSize = MeasureSpec.getSize(measureSpec);//测量大小switch (specMode) {//如果是无建议的测量模式,则取默认值case MeasureSpec.UNSPECIFIED:result = defaultValue;break;//建议最多以specSize大小设置case MeasureSpec.AT_MOST://建议恰好以specSize大小设置case MeasureSpec.EXACTLY:result = specSize;break;}return result;
}

CustomViewGroup的摆放方法为:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {int childCount = this.getChildCount();int childL = 0;int childT = 0;for (int index = 0; index < childCount; index++) {View childView = this.getChildAt(index);//经过onMeasure计算后,在此方法中已经可以获取大小了int childMeasureWidth = childView.getMeasuredWidth();int childMeasureHeight = childView.getMeasuredHeight();//并排摆放childView.layout(childL, childT, childL + childMeasureWidth, childT + childMeasureHeight);childL += childMeasureWidth;}}

CustomView的测量方法为:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//View中只需要设置自己的大小,毕竟没有childrenint measuredWidth = this.getMeasureSize(widthMeasureSpec, 0);int measuredHeight = this.getMeasureSize(heightMeasureSpec, 0);this.setMeasuredDimension(measuredWidth, measuredHeight);}/*** 获取测量大小** @param measureSpec* @param defaultValue 默认大小* @return*/
private int getMeasureSize(int measureSpec, int defaultValue) {int result = defaultValue;int specMode = MeasureSpec.getMode(measureSpec);//测量模式int specSize = MeasureSpec.getSize(measureSpec);//测量大小switch (specMode) {//如果是无建议的测量模式,则取默认值case MeasureSpec.UNSPECIFIED:result = defaultValue;break;//建议最多以specSize大小设置case MeasureSpec.AT_MOST://建议恰好以specSize大小设置case MeasureSpec.EXACTLY:result = specSize;break;}return result;
}

2、添加declare-styleable

像LinearLayout、RelativeLayout这些都有padding这些属性,我们也可以添加。效果图:

_thumb

新建一个xxx.xml

xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="CustomViewGroup_attrs"><attr name="padding" format="dimension" />declare-styleable>
resources>

其中format有几类,现用到尺寸dimension类型。

在CustomViewGroup中增加一个初始化方法:

private int padding = 0;private void init(Context context, AttributeSet attrs) {TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomViewGroup_attrs);//获取padding值this.padding = (int) a.getDimension(R.styleable.CustomViewGroup_attrs_padding, 0);//记得回收
    a.recycle();
}

修改CustomViewGroup的一些方法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {...int childCount = this.getChildCount();int childWidth = (measuredWidth - this.padding * 2) / childCount;int childHeight = measuredHeight - this.padding * 2;...}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {int childCount = this.getChildCount();int childL = this.padding;int childT = this.padding;...}

3、添加LayoutParams

TextView、ImageView这些在RelativeLayout中都有layout_centerVertical垂直居中选项,这是RelativeLayout的LayoutParams里面的属性,实际上layout_width、layout_height也是LayoutParams里面的属性。

垂直居中效果图:

_thumb2

修改布局文件

<com.besttimer.study.myviewgrouptest.CustomViewGroupandroid:layout_width="300dp"android:layout_height="100dp"android:background="#ff000000"app:padding="10dp"><com.besttimer.study.myviewgrouptest.CustomViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="#ffff0000" /><com.besttimer.study.myviewgrouptest.CustomViewandroid:layout_width="wrap_content"android:layout_height="50dp"android:background="#ff00ff00"app:layout_centerVertical="true" /><com.besttimer.study.myviewgrouptest.CustomViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="#ff0000ff" />
com.besttimer.study.myviewgrouptest.CustomViewGroup>

修改xxx.xml文件

xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="CustomViewGroup_attrs"><attr name="padding" format="dimension" /><attr name="layout_centerVertical" format="boolean" />declare-styleable>
resources>

为CustomViewGroup添加自定义LayoutParams

public static class LayoutParams extends ViewGroup.LayoutParams {public LayoutParams(Context c, AttributeSet attrs) {super(c, attrs);this.init(c, attrs);}private boolean layout_centerVertical = false;//是否垂直居中private void init(Context c, AttributeSet attrs) {TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CustomViewGroup_attrs);//获取layout_centerInParent值this.layout_centerVertical = a.getBoolean(R.styleable.CustomViewGroup_attrs_layout_centerVertical, false);//记得回收
        a.recycle();}}@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {return new LayoutParams(this.getContext(), attrs);
}

修改测量和摆放方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {...for (int index = 0; index < childCount; index++) {View childView = this.getChildAt(index);//以MeasureSpec.EXACTLY定义测量值int childNewHeight = childHeight;CustomViewGroup.LayoutParams layoutParams = (LayoutParams) childView.getLayoutParams();//如果大于0说明传了一个具体的数值if (layoutParams.height > 0) {childNewHeight = layoutParams.height;}int widthMeasureSpec_child = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);int heightMeasureSpec_child = MeasureSpec.makeMeasureSpec(childNewHeight, MeasureSpec.EXACTLY);//此方法将会调用childView.onMeasure(int widthMeasureSpec, int heightMeasureSpec)
        childView.measure(widthMeasureSpec_child, heightMeasureSpec_child);}...}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {...for (int index = 0; index < childCount; index++) {View childView = this.getChildAt(index);//经过onMeasure计算后,在此方法中已经可以获取大小了int childMeasureWidth = childView.getMeasuredWidth();int childMeasureHeight = childView.getMeasuredHeight();//并排摆放CustomViewGroup.LayoutParams layoutParams = (LayoutParams) childView.getLayoutParams();int childNewT = childT;if (layoutParams.layout_centerVertical) {childNewT += (b - t - this.padding * 2 - childMeasureHeight) / 2;}childView.layout(childL, childNewT, childL + childMeasureWidth, childNewT + childMeasureHeight);childL += childMeasureWidth;}}

其中layout_width和layout_height是ViewGroup.LayoutParams的属性,已经实现好了获取逻辑,直接用就是了。

源代码地址:http://files.cnblogs.com/files/linyibiao/AndroidProject.zip

转载于:https://www.cnblogs.com/linyibiao/p/5317593.html

本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 162202241@qq.com 举报,一经查实,本站将立刻删除。

最新评论

欢迎您发表评论:

请登录之后再进行评论

登录