最近项目要求在手机界面上展示一系列标签,标签的内容长度不一,当屏幕的可用宽度不够显示下一个标签时另起一行显示。就像下图所示:
按照Android传统的布局方式,根本不能满足,如果你不怕蛋疼,可以根据不同的手机分辨率一一计算每个标签的长宽然后精准绝对布局。所幸的是我以前做过此类的功能需求,用过一个开源的FlowLayout,所以直接拿来主义,套上老牛车跑起来发现每次换行后的第一个标签总是若隐若现。无奈硬着头皮深入到代码内部打探虚实,一入代码深似海啊,楼主的世界观颠覆了,深埋于心中的三年疑惑终于得到了解答。所以就有了楼主的这篇洋洋洒洒、略显枯燥的文章。
public class FlowLayout extends ViewGroup
FlowLayout继承ViewGroup,主要重写了它的两个方法onMeasure和onLayout。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int width = 0;
int height = getPaddingTop() + getPaddingBottom();
int lineWidth = 0;
int lineHeight = 0;
int childCount = getChildCount();
for(int i = 0; i < childCount; i++) {
View child = getChildAt(i);
boolean lastChild = i == childCount - 1;
if(child.getVisibility() == View.GONE) {
if(lastChild) {
width = Math.max(width, lineWidth);
height += lineHeight;
}
continue;
}
measureChildWithMargins(child, widthMeasureSpec, lineWidth, heightMeasureSpec, height);
.......
}
width += getPaddingLeft() + getPaddingRight();
setMeasuredDimension(
(modeWidth == MeasureSpec.EXACTLY) ? sizeWidth : width,
(modeHeight == MeasureSpec.EXACTLY) ? sizeHeight : height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
float horizontalGravityFactor;
switch ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)) {
case Gravity.LEFT:
default:
horizontalGravityFactor = 0;
break;
case Gravity.CENTER_HORIZONTAL:
horizontalGravityFactor = .5f;
break;
case Gravity.RIGHT:
horizontalGravityFactor = 1;
break;
}
for(int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if(child.getVisibility() == View.GONE) {
continue;
}
LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.bottomMargin + lp.topMargin;
if(lineWidth + childWidth > width) {
mLineHeights.add(lineHeight);
mLines.add(lineViews);
mLineMargins.add((int) ((width - lineWidth) * horizontalGravityFactor) + getPaddingLeft());
linesSum += lineHeight;
lineHeight = 0;
lineWidth = 0;
lineViews = new ArrayList();
}
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
lineViews.add(child);
}
mLineHeights.add(lineHeight);
mLines.add(lineViews);
mLineMargins.add((int) ((width - lineWidth) * horizontalGravityFactor) + getPaddingLeft());
linesSum += lineHeight;
int verticalGravityMargin = 0;
switch ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) ) {
case Gravity.TOP:
default:
break;
case Gravity.CENTER_VERTICAL:
verticalGravityMargin = (height - linesSum) / 2;
break;
case Gravity.BOTTOM:
verticalGravityMargin = height - linesSum;
break;
}
......
}
友情提示:不用细看上面的代码,我只截取了部分。当你弄明白了Android布局的内部机制(这里只讲onMeasure、onLayout),Android世界的所有UI布局需求,你都能做到一弹即破。
可以说重载onMeasure(),onLayout(),onDraw()三个函数构建了自定义View的外观形象。再加上onTouchEvent()等重载视图的行为,可以构建任何我们需要的可感知到的自定义View。
我们知道,不管是自定义View还是系统提供的TextView这些,它们都必须放置在LinearLayout等一些ViewGroup中,因此理论上我们可以很好的理解onMeasure(),onLayout(),onDraw()这三个函数:
"http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin" >
"200dp"
android:layout_height="wrap_content"
android:paddingTop="20dp"
android:layout_marginTop="30dp"
android:background="@android:color/darker_gray" >
"match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:layout_marginTop="15dp"
android:background="@android:color/holo_red_light"
/>
</LinearLayout>
LinearLayout>
public class MyView extends View {
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.d("MyView","------", new Throwable());// 查看onMeasure()的调用堆栈
int speHeightSize = MeasureSpec.getSize(heightMeasureSpec);
int speHeightMode = MeasureSpec.getMode(heightMeasureSpec);
Log.d("MyView_Height", "---speHeightSize = " + speSize + "");
Log.d("MyView_Height", "---speHeightMode = " + speMode + "");
if(speHeightMode == MeasureSpec.AT_MOST){
Log.d("MyView_Height", "---AT_MOST---");
}
if(speHeightMode == MeasureSpec.EXACTLY){
Log.d("MyView_Height", "---EXACTLY---");
}
if(speHeightMode == MeasureSpec.UNSPECIFIED){
Log.d("MyView_Height", "---UNSPECIFIED---");
}
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), speSize);
}
}
widthMeasureSpecD/MyView ( 3506): java.lang.Throwable
D/MyView ( 3506): at com.sean.myview.MyView.onMeasure(MyView.java:18)
D/MyView ( 3506): at android.view.View.measure(View.java:15775)
D/MyView ( 3506): at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4942)
D/MyView ( 3506): at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1411)
D/MyView ( 3506): at android.widget.LinearLayout.measureHorizontal(LinearLayout.java:1059)
D/MyView ( 3506): at android.widget.LinearLayout.onMeasure(LinearLayout.java:590)
D/MyView ( 3506): at android.view.View.measure(View.java:15775)
D/MyView ( 3506): at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4942)
D/MyView ( 3506): at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1411)
D/MyView ( 3506): at android.widget.LinearLayout.measureHorizontal(LinearLayout.java:1059)
D/MyView ( 3506): at android.widget.LinearLayout.onMeasure(LinearLayout.java:590)
D/MyView ( 3506): at android.view.View.measure(View.java:15775)
D/MyView ( 3506): at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4942)
D/MyView ( 3506): at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
D/MyView ( 3506): at android.view.View.measure(View.java:15775)
D/MyView ( 3506): at android.widget.LinearLayout.measureVertical(LinearLayout.java:850)
D/MyView ( 3506): at android.widget.LinearLayout.onMeasure(LinearLayout.java:588)
D/MyView ( 3506): at android.view.View.measure(View.java:15775)
D/MyView ( 3506): at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:4942)
D/MyView ( 3506): at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
......
D/MyView_Height ( 3506): ---speHeightSize = 940
D/MyView_Height ( 3506): ---speHeightMode = -2147483648
D/MyView_Height ( 3506): ---AT_MOST---
通过onMeasure堆栈的调用顺序我们可以得知:
MyView的可测量的高度是由它的父控件逐步传递过来的,它的父控件每次调用自身onMeasure计算出剩余可用的高度后传递给子控件,就这样逐级传递过来的。这里为什么是可测量的呢?
影响speHeightSize的因素为:父视图的layout_height和paddingTop以及自身的layout_marginTop。但是我们不要忘记有weight时的影响。
speHeightMode共有三种情况,取值分别为MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, MeasureSpec.AT_MOST。
MeasureSpec.EXACTLY是精确尺寸,当我们将控件的layout_width或layout_height指定为具体数值时如andorid:layout_width=”50dip”,或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。
MeasureSpec.AT_MOST是最大尺寸,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。
MeasureSpec.UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView,通过measure方法传入的模式。