自定義流式佈局:ViewGroup的測量與佈局

笪笠發表於2021-08-18

1、View生命週期以及View層級

1.1、View生命週期

​ View的主要生命週期如下所示, 包括建立、測量(onMeasure)、佈局(onLayout)、繪製(onDraw)以及銷燬等流程。

自定義流式佈局:ViewGroup的測量與佈局

​ 自定義View主要涉及到onMeasure、onLayout和onDraw這三個過程,其中

​ (1)自定義View(繼承自View類):主要實現onMeasure和onDraw,

​ (2)自定義ViewGroup(繼承自ViewGroup類):主要實現onMeasure和onLayout。

1.2、View層級

​ View層級是一個樹形結構。
自定義流式佈局:ViewGroup的測量與佈局
​ onMeasure、onLayout和onDraw這三個過程都是按照View層級從上到下進行的:(1)ViewGroup主要負責onMeasure和onLayout,確定自身及其子View的大小和放置方式,例如LinearLayout通過onMeasure確定尺寸,通過onLayout對子View進行橫向或者縱向佈局;(2)View主要負責onMeasure和onDraw,例如TextView通過onMeasure確定自身尺寸,通過onDraw繪製文字。

2、View測量與MeasureSpec類

​ View測量中最難的一點就是如何根據View的LayoutParams引數確定其實際的寬高,如:

android:layout_width="10dp"
android:layout_width="match_parent"
android:layout_width="wrap_content"

這三種情況,View的寬度究竟應該是多少?這就要從View的測量過程分析了,

2.1、MeasureSpec類

​ View類的內部類MeasureSpec用來輔助View的測量,使用一個int型變數measureSpec來表示View測量的模式和具體的尺寸(寬和高各一個measureSpec值)。measureSpec共32位,用高兩位表示測量模式mode, 通過MeasureSpec.getMode(measureSpec)計算獲得, 低30位表示尺寸size,通過MeasureSpec.getSize(measureSpec)計算獲得。

​ mode共有三種情況:

MeasureSpec.UNSPECIFIED:不對View大小做限制,系統使用
MeasureSpec.EXACTLY:確切的大小,如:10dp
MeasureSpec.AT_MOST:大小不可超過某數值,最大不能超過其父類

2.2、父View的限制 :測量約束,限制最大寬度、最大高度等

​ View的測量過程受到父View的限制,如對一個ViewGroup測量時,其高度測量模式mode為EXACTLY,高度尺寸size為100dp,其子View的高度測量依據對應的android:layout_height引數來確定:

​ (1)具體尺寸值,如50dp,則該子View高度測量中mode為EXACTLY,尺寸為50dp;

​ (2)match_parent,則該子View高度和其父View高度相同,也是確定的,高度測量中mode為EXACTLY,尺寸為100dp;

​ (3)wrap_content, 則該子View最大高度為100dp, 確切高度需要根據內部邏輯確定,像TextView需要根據文字內容、寬度等綜合確定,於是高度測量中mode為AT_MOST, 尺寸size為100dp。

​ 其他情況類似,如父View的mode分別為AT_MOST、UNSPECIFIED,具體見下表:
自定義流式佈局:ViewGroup的測量與佈局
​ 高度測量中mode和size確定後,可通過MeasureSpec.makeMeasureSpec(size, mode)來確定heightMeasureSpec,widthMeasureSpec使用同樣的方法確定。該方法的具體實現為ViewGroup.getChildMeasureSpec()方法。

2.3、子View的影響:實際測量

​ 測量過程以LinearLayout作為例子說明:

​ (1) LinearLayout根據父View的measureSpec以及自身的LayoutParams確定了自身的widthMeasureSpec、heightMeasureSpec後, 呼叫measure(widthMeasureSpec, heightMeasureSpec) -----> onMeasure(widthMeasureSpec, heightMeasureSpec)來進行實際的測量;

​ (2) 當該LinearLayout方向為vertical時,實際測量中應該計算所有子View的高度之和,作為LinearLayout的測量高度needHeight;

​ (3) heightMeasureSpec中size為父類給該LinearLayout的限制高度,根據heightMeasureSpec中mode判斷是取needHeight, 還是heightMeasureSpec中size, 然後呼叫setMeasuredDimension將測量的高度和寬度設定進去。

2.4、View的測量過程

​ Android中View測量是一種遞迴的過程(見下圖),首先View呼叫measure方法,內部呼叫了自身的onMeasure方法,這個方法內部呼叫子View的measure方法(子View同樣會呼叫自身的onMeasure方法),對子View進行測量,儲存子View的測量尺寸,測量完所有的子View後再對自身測量,儲存測量尺寸,之後便可以通過View.getMeasuredWidth()和View.getMeasuredHeight()來獲取View的測量寬高。
自定義流式佈局:ViewGroup的測量與佈局

3、自定義流式佈局FlowLayout

主要思路

​ 對FlowLayout的所有子View逐個進行測量,獲得measuredHeight和measuredWidth,在水平方向上根據這個尺寸依次對View進行放置,放不下則另起一行,每一行的高度取該行所有View的measuredHeight最大值。

3.1、單個子View測量

​ 對其指定子View----child的測量程式碼如下,其中paddingLeft、paddingRight、paddingTop、paddingBottom分別是FlowLayout四邊上的padding,widthMeasureSpec以及heightMeasureSpec是FlowLayout中onMeasure中的兩個引數。

int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, 
           paddingLeft + paddingRight, child.getLayoutParams().width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightMeasureSpec, 
           paddingTop + paddingBottom, child.getLayoutParams().height);
child.measure(childWidthSpec, childHeightSpec);

​ 於是子View的測量寬、高分別可以通過child.getMeasuredWidth() 和child.getMeasuredHeight()來進行獲得。

3.2、onMeasure:測量與模擬佈局View

   //子View的橫向間隔、縱向間隔
   private final int horizontalSpace = dp2px(20);
   private final int verticalSpace = dp2px(10);
   
   //儲存測量的子View, 每一個元素為一行的子View陣列
   private final List<List<View>> allLines = new ArrayList<>();
   //記錄每一行的最大高度,用於佈局
   private final List<Integer> heights = new ArrayList<>();

   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       allLines.clear();
       heights.clear();

       int paddingLeft = getPaddingLeft();
       int paddingRight = getPaddingRight();
       int paddingTop = getPaddingTop();
       int paddingBottom = getPaddingBottom();

       int usedWidth = 0;
       int height = 0;

       //父佈局對FlowLayout的約束寬高
       int seftWidth = MeasureSpec.getSize(widthMeasureSpec) - paddingLeft -
                  paddingRight;
       int seftHeight = MeasureSpec.getSize(heightMeasureSpec) - paddingTop - 
                  paddingBottom;

       //FlowLayout的測量寬高
       int needHeight = 0;
       int needWidth = 0;

       List<View> line = new ArrayList<>();


       int count = getChildCount();
       for (int i = 0; i < count; i++) {
           View child = getChildAt(i);

           int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
                  paddingLeft + paddingRight, child.getLayoutParams().width);
           int childHeightSpec = ViewGroup.getChildMeasureSpec(heightMeasureSpec, 
                  paddingTop + paddingBottom, child.getLayoutParams().height);
           child.measure(childWidthSpec, childHeightSpec);

           if (usedWidth + horizontalSpace + child.getMeasuredWidth() > seftWidth) {
               //當前行無法在放下下一個view,則儲存當前行的Views集合以及當前行的最大高度,
               heights.add(height + verticalSpace);
               allLines.add(line);
               //所有行的最大寬度
               needWidth = Math.max(needWidth, usedWidth);
               //所有行的高度之和
               needHeight += height + verticalSpace;

               //重置下一行的使用寬度、高度、View集合
               usedWidth = 0;
               height = 0;
               line = new ArrayList<>();
           }
           //獲取當前行的最大高度,作為當前行的高度
           height = Math.max(height, child.getMeasuredHeight());
           //記錄已經使用的寬度(第一個元素不需要加橫向間隔
           usedWidth += child.getMeasuredWidth() + (line.size() == 0 ? 0 : 
                 horizontalSpace);
           //儲存已經測量及模擬佈局的View
           line.add(child);

           //記錄最後一行的資料
           if (i == count - 1) {
               heights.add(height + verticalSpace);
               allLines.add(line);
               needWidth = Math.max(needWidth, usedWidth);
               needHeight += height + verticalSpace;
           }
       }

       int widthMode = MeasureSpec.getMode(widthMeasureSpec);
       int heightMode = MeasureSpec.getMode(heightMeasureSpec);
       //如果mode為MeasureSpec.EXACTLY, 則使用widthMeasureSpec中的size,
       //不然使用測量得到的size, 寬高同理
       int realWidth = widthMode == MeasureSpec.EXACTLY ? seftWidth : needWidth;
       int realHeight = heightMode == MeasureSpec.EXACTLY ? seftHeight : needHeight;

       //儲存測量的寬和高
       setMeasuredDimension(realWidth + paddingLeft + paddingRight,
               //如果只有一行,不需要縱向間隔
               realHeight + paddingTop + paddingBottom - (allLines.size() > 0 ?
               verticalSpace : 0));
   }

3.3、佈局:onLayout

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = getPaddingLeft();
        int top = getPaddingTop();
        for (int i = 0; i < allLines.size(); i++) {
            List<View> line = allLines.get(i);
            for (int j = 0; j < line.size(); j++) {
                View child = line.get(j);
                child.layout(left, top, left + child.getMeasuredWidth(), 
                        top + child.getMeasuredHeight());
                //一行中View佈局後每次向後移動child的測量寬 + 橫向間隔
                left += child.getMeasuredWidth() + horizontalSpace;
            }
            //每一行佈局從paddingLeft開始
            left = getPaddingLeft();
            //佈局完成一行,向下移動當前行的最大高度
            top += heights.get(i);
        }
    }

3.4、測試

測試程式碼如下:

    private final List<String> words = Arrays.asList("家用電器", "手機", "運營商", "數碼",
      "電腦", "辦公", "電子書", "惠普星系列高清一體機", "格力2匹移動空調");

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_flow);

        FlowLayout layout = findViewById(R.id.flow_layout);


        for (int i = 0; i < words.size(); i++) {
            TextView textView = new TextView(this);
            textView.setText(words.get(i));
            textView.setBackground(ContextCompat.getDrawable(this,
                    R.drawable.round_background));
            textView.setLayoutParams(new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.WRAP_CONTENT, FlowLayout.dp2px(60)));
            //textView.setLayoutParams(new ViewGroup.LayoutParams(
            //          ViewGroup.LayoutParams.WRAP_CONTENT, 
            //          ViewGroup.LayoutParams.WRAP_CONTENT));
            int padding = FlowLayout.dp2px(5);
            textView.setPadding(padding, padding, padding, padding);

            layout.addView(textView);
        }
    }

效果圖:

demo

連結

相關文章