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

}

效果圖:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章