1. 测量规则:
如果父控件是ViewGroup, 调用Measure方法,如果子控件是View,那么调用重写onMeasure测量,调用setMeasureDimension设置宽高
子控件onMeasure 中, 必须知道 父控件的 测量规则
// 1. 测量的时候测量多次
// 父容器 给当前 视图的 widthMeasureSpec, heightMeasureSpec 宽度和 高度
int size=MeasureSpec.getSize(widthMeasureSpec);
int mode= MeasureSpec.getMode(widthMeasureSpec);
switch (mode){
case MeasureSpec.AT_MOST:
// 父容器指定一个大小,view的大小不能大于这个值
Log.e(TAG,"AT_MOST");
break;
case MeasureSpec.UNSPECIFIED:
// 父容器对不对 view 有任何限制, 要多大给多大
Log.e(TAG,"UNSPECIFIED");
break;
case MeasureSpec.EXACTLY:
// 父容器已经检测出 view所需要的大小
Log.e(TAG,"EXACTLY");
break;
}
// 父视图个子视图的高度
int sizeHeight= MeasureSpec.getSize(heightMeasureSpec);
// 父视图给子视图 模式
int modeHeight =MeasureSpec.getMode(heightMeasureSpec);
2. 已知父控件模式,计算子控件测量规则 int childeWidthMode;
int childeHeightMode;
// 为了测量每个孩子 需要指定每个孩子测量规则
// 子View是包裹内容
// 如果父View是 精确值,那么子View, AT_MOST
// 否则 父 View 模式== 子View 模式
childeWidthMode=widthMode==MeasureSpec.EXACTLY?MeasureSpec.AT_MOST:widthMode;
childeHeightMode=heightMode==MeasureSpec.EXACTLY?MeasureSpec.AT_MOST:heightMode; // 要测量每个孩子,必须知道每个孩子的测量规则
int childWidthMeasureSpec=MeasureSpec.makeMeasureSpec(childeWidthMode, width);
int childHeightMeasureSpec=MeasureSpec.makeMeasureSpec(childeHeightMode, height);
3. 把每一行数据一个 List 中
4. 间隙处理
比如刚好摆放好2个控件,摆放第三个控件的时候放不下了
那么把空下的空隙,分给 上面的 2个控件
5. 问题 TextView 无法居中
核心代码:
1. MainActivity
package mk.denganzhi.com.flowlayout;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.StateListDrawable;
import android.support.annotation.MainThread;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import org.w3c.dom.Text;
import java.util.Random;
import java.util.UUID;
public class MainActivity extends AppCompatActivity {
ScrollView scrollView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Drawable pressDrawable= DrawableUtils.createShape(getResources(),0xffcecece);
scrollView= (ScrollView) findViewById(R.id.footer);
Random random=new Random();
Flowlayout layout=new Flowlayout(this);
int padding=UiUtils.dip2px(getResources(),13);
layout.setPadding(padding, padding, padding, padding);
for(int i=0;i<17;i++){
// 创建一个Shape,Shape中有一个Selector
GradientDrawable drawable=new GradientDrawable();
drawable.setCornerRadius(UiUtils.dip2px(getResources(),5));
int nextInt1= random.nextInt(256);
int nextInt2= random.nextInt(256);
int nextInt3= random.nextInt(256);
// drawable.setColor(Color.rgb(nextInt1,nextInt2,nextInt3));
TextView textView=new TextView(this);
textView.setTextColor(Color.WHITE);
textView.setText("鸽子鸽子:"+i);
textView.setClickable(true);
textView.setGravity(Gravity.CENTER);
int textPaddingV= UiUtils.dip2px(getResources(),4);
int textPaddingH= UiUtils.dip2px(getResources(),7);
textView.setPadding(textPaddingH,textPaddingV,textPaddingH,textPaddingV);
GradientDrawable createShape= DrawableUtils.createShapeRGB(getResources(),Color.rgb(nextInt1,nextInt2,nextInt3));
StateListDrawable stateListDrawable= DrawableUtils.createSelectorDrawable(pressDrawable,createShape);
// textView.setBackgroundDrawable(drawable);
textView.setBackgroundDrawable(stateListDrawable);
// footer.addView(textView,new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT));
// textView.setGravity(Gravity.CENTER);
layout.addView(textView,new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, -2));// -2 包裹内容
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
TextView finalTextView= (TextView)view;
Toast.makeText(MainActivity.this,finalTextView.getText(),Toast.LENGTH_LONG).show();
}
});
}
scrollView.addView(layout,new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT));
}
}
2. DrawableUtils
package mk.denganzhi.com.flowlayout;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.StateListDrawable;
public class DrawableUtils {
public static GradientDrawable createShape(Resources context,int color){
GradientDrawable drawable=new GradientDrawable();
drawable.setCornerRadius(dip2px(context,5));//设置4个角的弧度
drawable.setColor(color);// 设置颜色
return drawable;
}
public static GradientDrawable createShapeRGB(Resources context,int rgb){
GradientDrawable drawable=new GradientDrawable();
drawable.setCornerRadius(dip2px(context,5));//设置4个角的弧度
drawable.setColor(rgb);
return drawable;
}
public static StateListDrawable createSelectorDrawable(Drawable pressedDrawable,Drawable normalDrawable){
// <selector xmlns:android="http://schemas.android.com/apk/res/android" android:enterFadeDuration="200">
// <item android:state_pressed="true" android:drawable="@drawable/detail_btn_pressed"></item>
// <item android:drawable="@drawable/detail_btn_normal"></item>
// </selector>
StateListDrawable stateListDrawable=new StateListDrawable();
stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, pressedDrawable);// 按下显示的图片
stateListDrawable.addState(new int[]{}, normalDrawable);// 抬起显示的图片
return stateListDrawable;
}
/** dip转换px */
public static int dip2px(Resources context, int dip) {
final float scale = context.getDisplayMetrics().density;
return (int) (dip * scale + 0.5f);
}
/** px转换dip */
public static int px2dip(Resources context,int px) {
final float scale = context.getDisplayMetrics().density;
return (int) (px / scale + 0.5f);
}
}
3. Flowlayout
package mk.denganzhi.com.flowlayout;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.w3c.dom.Text;
public class Flowlayout extends ViewGroup {
private int horizontolSpacing=UiUtils.dip2px(getResources(),13);
private int verticalSpacing= UiUtils.dip2px(getResources(),13);
public Flowlayout(Context context) {
super(context);
}
public Flowlayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
private Line currentline;// 当前的行
private int useWidth=0;// 当前行使用的宽度
private List<Line> mLines=new ArrayList<Line>();
private int width;
public Flowlayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 测量 当前控件Flowlayout
// 父类是有义务测量每个孩子的
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
// MeasureSpec.EXACTLY;
// MeasureSpec.AT_MOST;
// MeasureSpec.UNSPECIFIED;
mLines.clear();
currentline=null;
useWidth=0;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec); // 获取当前父容器(Flowlayout)的模式
// 获取父控件的测量模式,传递给孩子
// 计算孩子 宽度、高度的时候去掉 padding,
// 计算总的 宽度、高度的时候把padding 加上
width = MeasureSpec.getSize(widthMeasureSpec)-getPaddingLeft()-getPaddingRight();
int height = MeasureSpec.getSize(heightMeasureSpec)-getPaddingBottom()-getPaddingTop(); // 获取到宽和高
int childeWidthMode;
int childeHeightMode;
// 为了测量每个孩子 需要指定每个孩子测量规则
// 子View是包裹内容
// 如果父View是 精确值,那么子View, AT_MOST
// 否则 父 View 模式== 子View 模式
childeWidthMode=widthMode==MeasureSpec.EXACTLY?MeasureSpec.AT_MOST:widthMode;
childeHeightMode=heightMode==MeasureSpec.EXACTLY?MeasureSpec.AT_MOST:heightMode;
// 要测量每个孩子,必须知道每个孩子的测量规则
int childWidthMeasureSpec=MeasureSpec.makeMeasureSpec(childeWidthMode, width);
int childHeightMeasureSpec=MeasureSpec.makeMeasureSpec(childeHeightMode, height);
currentline=new Line();// 创建了第一行
int childCount= getChildCount();
for(int i=0;i<childCount;i++) {
View child=getChildAt(i);
System.out.println("孩子的数量:"+childCount);
// 测量每个孩子, 孩子的模式是wrap_content
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
int measuredWidth = child.getMeasuredWidth();
useWidth+=measuredWidth;// 让当前行加上使用的长度
if(useWidth<=width){
useWidth+=horizontolSpacing;
if(useWidth>width){
//换行
newLine(child,measuredWidth);
}else{
currentline.addChild(child);//这时候证明当前的孩子是可以放进当前的行里,放进去
}
}else{
//换行,一个TextView就超过范围了,强制放下
if(currentline.getChildCount()<1){
currentline.addChild(child); // 保证当前行里面最少有一个孩子
}
newLine(child,measuredWidth);
}
}
// 如果是最后一行,没有走 newLine(); 这里补上
if(!mLines.contains(currentline)){
mLines.add(currentline);// 添加最后一行
}
int totalheight=0;
for(Line line:mLines){
totalheight+=line.getHeight();
}
totalheight+=verticalSpacing*(mLines.size()-1)+getPaddingTop()+getPaddingBottom();
System.out.println(totalheight);
//resolveSize(totalheight, heightMeasureSpec)
// totalheight: 计算高度
// heightMeasureSpec: 本身体高度
// 哪个合适用哪个
// 1. 如果当前高度小于父容器高度,那么使用当前高度
// height: 是 wrap_content
// width: match_parent
setMeasuredDimension(width+getPaddingLeft()+getPaddingRight(),resolveSize(totalheight, heightMeasureSpec));
}
private void newLine(View child,int measuredWidth) {
mLines.add(currentline);// 记录之前的行
currentline=null;
currentline=new Line(); // 创建新的一行
currentline.addChild(child);
useWidth=measuredWidth;
}
private class Line{
int height=0; //当前行的高度
int lineWidth=0;
private List<View> children=new ArrayList<View>();
/**
* 添加一个孩子
* @param child
*/
public void addChild(View child) {
children.add(child);
if(child.getMeasuredHeight()>height){
// 如果有3个孩子,每个孩子宽度和高度不一致,那么取最大孩子的高度
height=child.getMeasuredHeight();
}
lineWidth+=child.getMeasuredWidth();
}
public int getHeight() {
return height;
}
/**
* 返回孩子的数量
* @return
*/
public int getChildCount() {
return children.size();
}
public void layout(int l, int t) {
lineWidth+=horizontolSpacing*(children.size()-1);
int surplusChild=0;
// 间隙处理
// 比如刚好摆放好2个控件,摆放第三个控件的时候放不下了
// 那么把空下的空隙,分给 上面的 2个控件
int surplus=width-lineWidth;
Log.e("Tag","width:"+children.size()+" surplus:"+ surplus);
//
if(surplus>0 && children.size()>0){
//
surplusChild=surplus/children.size();
}
for(int i=0;i<children.size();i++){
View child=children.get(i);
// getMeasuredWidth() 控件实际的大小
// getWidth() 控件显示的大小
// 比如父控件中有一个ImageView,200*200, 但是父控件只有 100*100大小
// 那么 getMeasuredWidth 控件实际的大小:200
// 控件显示的大小:getWidth是 100
child.layout(l, t, l+child.getMeasuredWidth()+surplusChild, t+child.getMeasuredHeight());
l+=child.getMeasuredWidth()+surplusChild;
l+=horizontolSpacing;
TextView textView=(TextView)child;
textView.setGravity(Gravity.CENTER);
}
}
}
// 分配每个孩子的位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
l+=getPaddingLeft();
t+=getPaddingTop();
for(int i=0;i<mLines.size();i++){
Line line=mLines.get(i);
line.layout(l,t); //交给每一行去分配
t+=line.getHeight()+verticalSpacing;
}
}
}
??? 为什么 onMeause 方法会被调用2次
ViewRootImpl 源码:
if (!cancelDraw) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
} else {
if (isViewVisible) {
// Try again
// onMeasure 会被调用 2次,
// 为什么调用2次,比如子元素宽度设置200,但是父元素宽度只有100
// 第一个调用的时候 子元素宽度为200, 子元素宽度不可能操作父元素宽度
// 那么第二次调用的时候子元素宽度显示100,实际值
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}
效果图: