android 系统提供了五种布局方式:FrameLayout、RelativeLayout、LinearLayout、GridLayout、AbsoluteLayout;大部分Android界面都是通过这五种布局相互嵌套实现的,但是如果有这样的一个需求:控件的个数是通过从服务器获取数据后才知道的,并且希望控件水平排列,当超出屏幕时自动换行。这是就需要自己定义布局了。
流布局FlowLayout——实现了控件位置水平方向一次排列,在控件水平总宽度大于屏幕宽度时,自动切换到下一排显示。
自定义布局需要继承ViewGroup重写onMeasure和onLayout方法,如果布局要支持子控件的layout_margin属性,则自定义的ViewGroup类必须重载generateLayoutParams()函数,并且在该函数中返回一个ViewGroup.MarginLayoutParams派生类对象,这样才能使用margin参数。
public class FlowLayout extends ViewGroup {
/*自定义ViewGroup支持子控件的layout_margin参数,则自定义的ViewGroup类必须重载generateLayoutParams()函数,并且在该函数中返回一个ViewGroup.MarginLayoutParams派生类对象,这样才能使用margin参数。*/
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs)
{
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs)
{
return new FlowLayout.LayoutParams(getContext(), attrs);
}
}
onMeasure方法完成自己大小的测量和遍历调用所有子元素的measure方法;在这个方法里,View可以控制自己的大小。根据需求,我们要在这个方法里做两件事:
- 根据子控件的宽度和margin值,计算布局本身的宽度,当计算的宽度大于屏幕的宽度时,布局的宽度就等于屏幕的宽度。
- 根据每一排子控件的最大高度计算布局的高度
在onMeasure方法中使用了MeasureSpec类,MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,SpecSize是指在某种模式下的规格大小,下面是MeasureSpec的源码:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
MeasureSpec通过将SpecMode和SpecSize导打包成一个int值来避免过多的对象内存分配,其还提供了打包和解包,一组SpecMode和SpecSize可以打包为一个MeasureSpec,而一个MeasureSpec可以通过解包的形式得到原始的SpecMode和SpecSize。
SpecMode有三种模式:
UNSPECIFIED:
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。
EXACTLY:
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于定义View大小的match_parent和具体的数值。
AT_MOST:
父容器指定一个可用大小即SpecSize,View的大小不能大于这个数值,具体是多少看不同View的具体表现,对应于定义View大小的wrap_content.
onMeasure方法代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth=0;
int sreenWidth=0;
int measureHeight=0;
int maxHeigth=0;
final int childCount=getChildCount();
measureChildren(widthMeasureSpec,heightMeasureSpec);
int widthSpaceSize= MeasureSpec.getSize(widthMeasureSpec);
int widthSpaceMoth= MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize= MeasureSpec.getSize(heightMeasureSpec);
int heightSpaceMoth= MeasureSpec.getMode(heightMeasureSpec);
if(childCount==0){
setMeasuredDimension(0,0);
}else{
if(widthSpaceMoth== MeasureSpec.AT_MOST&&heightSpaceMoth== MeasureSpec.AT_MOST){
for(int i=0;i<childCount;i++){
View childView=getChildAt(i);
if(childView.getVisibility()!= View.GONE){
final int childWidth=childView.getMeasuredWidth();
final int childHeight=childView.getMeasuredHeight();
final FlowLayout.LayoutParams lp=(FlowLayout.LayoutParams) childView.getLayoutParams();
if((sreenWidth+childWidth+lp.leftMargin+lp.rightMargin)>widthSpaceSize){
measureWidth=widthSpaceSize;
sreenWidth=childWidth+lp.leftMargin+lp.rightMargin;
maxHeigth=childHeight+lp.topMargin+lp.bottomMargin;
measureHeight+=maxHeigth;
}else{
measureWidth+=childWidth+lp.leftMargin+lp.rightMargin;
sreenWidth+=childWidth+lp.leftMargin+lp.rightMargin;
if((childHeight+lp.topMargin+lp.bottomMargin)>maxHeigth){
measureHeight=measureHeight-maxHeigth+childHeight+lp.topMargin+lp.bottomMargin;
maxHeigth=childHeight+lp.topMargin+lp.bottomMargin;
}
}
}
}
if(measureWidth>widthSpaceSize){
measureWidth=widthSpaceSize;
}
setMeasuredDimension(measureWidth,measureHeight);
}else if(heightSpaceMoth== MeasureSpec.AT_MOST){
for(int i=0;i<childCount;i++){
View childView=getChildAt(i);
if(childView.getVisibility()!= View.GONE){
final int childWidth=childView.getMeasuredWidth();
final int childHeight=childView.getMeasuredHeight();
final FlowLayout.LayoutParams lp=(FlowLayout.LayoutParams) childView.getLayoutParams();
if((sreenWidth+childWidth+lp.leftMargin+lp.rightMargin)>widthSpaceSize){
measureWidth=widthSpaceSize;
sreenWidth=childWidth+lp.leftMargin+lp.rightMargin;
maxHeigth=childHeight+lp.topMargin+lp.bottomMargin;
measureHeight+=maxHeigth;
}else{
measureWidth+=childWidth+lp.leftMargin+lp.rightMargin;
sreenWidth+=childWidth+lp.leftMargin+lp.rightMargin;
if((childHeight+lp.topMargin+lp.bottomMargin)>maxHeigth){
measureHeight=measureHeight-maxHeigth+childHeight+lp.topMargin+lp.bottomMargin;
maxHeigth=childHeight+lp.topMargin+lp.bottomMargin;
}
}
}
}
setMeasuredDimension(widthSpaceSize,measureHeight);
}else if(widthSpaceMoth== MeasureSpec.AT_MOST){
for(int i=0;i<childCount;i++){
View childView=getChildAt(i);
if(childView.getVisibility()!= View.GONE){
int childWidth=childView.getMeasuredWidth();
if((measureWidth+childWidth)>widthSpaceSize){
measureWidth=widthSpaceSize;
}else{
measureWidth+=childWidth;
}
}
}
}
}
}
onLayout方法作用是确定所有子元素的位置,我们要根据需求在水平宽度大于屏幕宽度的时候,通过调用layout方法控制子元素的位置。(注意margin属性)
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int width=right-left;
int childLeft=0;
int childTop=0;
int maxHeight=0;
final int childCount=getChildCount();
for(int i=0;i<childCount;i++){
final View childView=getChildAt(i);
if(childView.getVisibility()!= View.GONE){
int childWidth=childView.getMeasuredWidth();
int childHeight=childView.getMeasuredHeight();
FlowLayout.LayoutParams lp=(FlowLayout.LayoutParams) childView.getLayoutParams();
if((childLeft+childWidth+lp.leftMargin+lp.rightMargin)>width){
childLeft=0;
childTop+=maxHeight;
maxHeight=childHeight+lp.topMargin+lp.bottomMargin;
}else{
if((childHeight+lp.topMargin+lp.bottomMargin)>maxHeight){
maxHeight=childHeight+lp.topMargin+lp.bottomMargin;
}
}
childView.layout(childLeft+lp.leftMargin,childTop+lp.topMargin,childLeft+lp.leftMargin+childWidth,childTop+lp.topMargin+childHeight);
childLeft+=childWidth+lp.leftMargin+lp.rightMargin;
}
}
}
写到这里,自定义布局就完成了,然后我们在XML中使用我们的自定义布局,看一下效果。
xml文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.flowlayout.demo.FlowLayout
android:id="@+id/flowlayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ffffff">
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_margin="10dp"
android:background="@drawable/shape_flow_layout"
android:gravity="center"
android:padding="5dp"
android:text="textView1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_margin="10dp"
android:background="@drawable/shape_flow_layout"
android:gravity="center"
android:padding="5dp"
android:text="textView2" />
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_margin="10dp"
android:background="@drawable/shape_flow_layout"
android:gravity="center"
android:padding="5dp"
android:text="textView3" />
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_margin="10dp"
android:background="@drawable/shape_flow_layout"
android:gravity="center"
android:padding="5dp"
android:text="textView4" />
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_margin="10dp"
android:background="@drawable/shape_flow_layout"
android:gravity="center"
android:padding="5dp"
android:text="textView5" />
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_margin="10dp"
android:background="@drawable/shape_flow_layout"
android:gravity="center"
android:padding="5dp"
android:text="textView6" />
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_margin="10dp"
android:background="@drawable/shape_flow_layout"
android:gravity="center"
android:padding="5dp"
android:text="textView7" />
</com.flowlayout.demo.FlowLayout>
<Button
android:id="@+id/add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:layout_gravity="center_horizontal"
android:text="增加TextView"/>
</LinearLayout>
运行结果: