Android 实现控件流式布局

要点如下:

 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;
		}
	}

}

效果图:

发布了122 篇原创文章 · 获赞 139 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/dreams_deng/article/details/105489276