转载请注明出处:http://blog.csdn.net/teisun/article/details/45560095
前言
做了快4年的android开发没写过什么技术文章,最近工作空档较多所以想起来写一两篇博文分享出来并且提升自己,文采拙劣,欢迎拍砖。
View的绘制过程
ViewGroup继承View,ViewGourp可以包含很多个View,View的绘制过程分三个步骤:
- onMeasure 计算,可以把View想象成一张无限大的画布,那么你要画出来的内容的尺寸有多大? onMeasure就是用于计算内容尺寸边界的。
- onLayout 布局,内容是由元素组成的,决定好画布的尺寸后各个子View要放在画布的什么位置呢?onLayout就是让你决定各个子View要放在画布的什么位置。
- onDraw 画,决定好子View的位置后,那我们的子View长什么样子呢?是画鸡、画猫还是画狗呢,只有各个子View自己知道,dispatchDraw 通知各个子View在指定的位置draw出自己。
onMeasure
onMeasure函数的widthMeasureSpec与heightMeasureSpec并非只是个简单的数字而是一种特殊的值,是期望类型值与尺寸值相加得到的,而android中的View.MeasureSpec类提供了对这种特殊值得操作方法:getSize,getMode ,makeMeasureSpec。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//getSize 获取控件宽度
int with = MeasureSpec.getSize(widthMeasureSpec);
//getMode 获取父View对其宽度的期望值类型
int mode = MeasureSpec.getMode(widthMeasureSpec);
//makeMeasureSpec 根据给定的尺寸和期望类型生成一个MeasureSpec值,一般用于指定子View的尺寸
View child = getChildAt(0);
LayoutParams lp=(LayoutParams)child.getLayoutParams();
//MeasureSpec.AT_MOST:表示子View最多只能是lp.width指定的大小,开发人员和系统都应该按照这个规则去设置子View的大小,当然开发人员也可以任性地自己去设置想要的大小。
int wSpec=MeasureSpec.makeMeasureSpec(lp.width,MeasureSpec.AT_MOST);
int hSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.AT_MOST);
//MeasureSpec.EXACTLY:表示子View只能是lp.width指定的大小,开发人员和系统都应该按照这个尺寸去设置子View的大小,当然开发人员也可以任性地自己去设置想要的大小。
int wSpec=MeasureSpec.makeMeasureSpec(lp.width,MeasureSpec.EXACTLY);
int hSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
//MeasureSpec.UNSPECIFIED:表示子View的尺寸大小没有任何限制.
int wSpec=MeasureSpec.makeMeasureSpec(lp.width,MeasureSpec.UNSPECIFIED);
int hSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.UNSPECIFIED);
child.measure(wSpec, hSpec);
}
更多关于MeasureSpec与android中View的Flag设计会在其他文章中说明这里不再展开。
onLayout
官方解释:
Called from layout when this view should assign a size and position to each of its children.
也就是说当需要为每一个子View指定尺寸和位置时会被调用。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int mTotalHeight = 0;
// 遍历子View
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 获取在onMeasure中计算的视图尺寸
int measureHeight = childView.getMeasuredHeight();
int measuredWidth = childView.getMeasuredWidth();
//告诉ViewGroup子View要的大小和位置
childView.layout(left, mTotalHeight, measuredWidth, mTotalHeight + measureHeight);
mTotalHeight += measureHeight;
}
}
onDraw
onDraw 方法一般自定义View的时候会用到,下一篇文章再详细分析。
案例:LineWrapLayout 实现自动换行的自定义ViewGroup
android提供的Linearlayout不可以自动换行显示,那么我们自己写一个,先看效果:
实现思路
- 什么时候应该换行?
答:一行内所有子View的宽度和View之间的间隔相加>=ViewGroup的宽度时就应该换行,而且最后一个View就应该被排列到下一行。 - 如何计算ViewGroup的高度?
答:ViewGroup的上下Padding+每一行高度+行之间纵向间隔 - 可以设置子View与子View之间的横向纵向间隔
- onMeasure函数中实现ViewGroup的宽高计算逻辑与每一个子View的宽高。
- onLayout函数中实现各个子View的排列逻辑
好了,想太多没用有个大概的实现思路就可以动手写了。
主要代码
我想到的是使用xml文件配置子View之间的间隔这就需要用到自定义属性,在values文件夹下创建attrs.xml,代码:
//name写自定义控件的类名,每一个attr指定属性名和属性类型
<declare-styleable name="LineWrapLayout">
//横向间隔
<attr name="horizontal_spacing" format="dimension" />
//纵向间隔
<attr name="vertical_spacing" format="dimension" />
</declare-styleable>
使用:
<com.vclubs.ui.component.LineWrapLayout
//使用自定义属性必须加这一句
xmlns:app="http://schemas.android.com/apk/res-auto"
//使用自定义属性指定子View之间的横向纵向间隔
app:horizontal_spacing="@dimen/dp15"
app:vertical_spacing="@dimen/dp15"
android:id="@+id/pic_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/dp40"
android:layout_marginRight="@dimen/dp40"
android:orientation="horizontal"
android:paddingLeft="@dimen/dp20"
android:paddingRight="@dimen/dp20"
android:paddingTop="@dimen/dp20"
>
</com.vclubs.ui.component.LineWrapLayout>
代码中获取自定义属性的值:
public LineWrapLayout(Context context, AttributeSet attrs) {
super(context, attrs);
//在构造函数中获得自定义属性的值,ViewGroup有三个构造函数最好都实现一下
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.LineWrapLayout);
//得到横向间隔
hSpacing = a.getDimensionPixelSize(R.styleable.LineWrapLayout_horizontal_spacing, 15);
//得到纵向间隔
vSpacing = a.getDimensionPixelSize(R.styleable.LineWrapLayout_vertical_spacing, 15);
a.recycle();
}
在onMeasure函数中计算ViewGroup尺寸与各个子View的尺寸:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//得到ViewGroup的初始宽高
final int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec)
+ getPaddingBottom()+getPaddingTop();
final int count = getChildCount();
int line_height = 0;
//获取第一个子View的起始点位置
int xpos = getPaddingLeft();
int ypos = getPaddingTop();
//计算每一个子View的尺寸,并算出ViewGroup的高度
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
final LayoutParams lp = child.getLayoutParams();
//算出子View宽的MeasureSpec值
int wSpec = MeasureSpec.makeMeasureSpec(
lp.width, MeasureSpec.AT_MOST);
//算出子View高的MeasureSpec值
int hSpec = MeasureSpec.makeMeasureSpec(
lp.height, MeasureSpec.AT_MOST);
//让子View记住自己宽高的MeasureSpec值,子View的
//onMeasure(int widthMeasureSpec,int heightMeasureSpec)
//函数传入的就是这里算出来的这两个值
child.measure(wSpec, hSpec);
//设置完MeasureSpec值后调用View.getMeasuredWidth()函数算出View的宽度
final int childw = child.getMeasuredWidth();
//记录最大行高(子View的高度有可能不一样,行高取最大高度)
line_height = Math.max(line_height,
child.getMeasuredHeight() + vSpacing);
if (xpos + childw > width) {
//初始坐标的x偏移值+子View宽度>ViewGroup宽度 就换行
xpos = getPaddingLeft();//坐标x偏移值归零
ypos += line_height;//坐标y偏移值再加上本行的行高也就是换行
}
//算出下一个子View的起始点x偏移值
xpos += childw + hSpacing;
}
this.line_height = line_height;
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
//对高度期望值没有限制
height = ypos + line_height;
} else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
//达不到指定高度则缩小高度
if (ypos + line_height < height) {
height = ypos + line_height;
}
} else {
height = ypos + line_height;
}
//设置ViewGroup宽高值
setMeasuredDimension(width, height);
}
在onLayout函数中设置各个子View的位置,有了上面的基础下面的代码应该很好理解了,由读者自行理解吧就不注释了。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
final int width = r - l;
int xpos = getPaddingLeft();
int ypos = getPaddingTop();
//设置每一个子View的位置,左上角xy坐标与右下角xy坐标确定View的位置
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final int childw = child.getMeasuredWidth();
final int childh = child.getMeasuredHeight();
if (xpos + childw > width) {
xpos = getPaddingLeft();
ypos += line_height;
}
child.layout(xpos, ypos, xpos + childw, ypos + childh);
xpos += childw + hSpacing;
}
}
}
结束
大概内容已经写完了以后再完善,源码:
https://github.com/teisun/Android-LineWrapLayout
http://download.csdn.net/detail/teisun/8755303