要點如下:
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);
// 測量每個孩子
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. 如果當前高度小於父容器高度,那麼使用當前高度
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;
}
}
}
效果圖: