Android自定義View實現流式布局(熱門標簽效果)

BetFarnell 8年前發布 | 14K 次閱讀 安卓開發 Android開發 移動開發

效果圖

實現效果圖

思維導圖

思維導圖

一、流式布局的實現

實現原理:采用面向對象思想將整個布局分為很多行的對象,每個行對象管理自己行內的孩子,這里通過集合來管理。

1. 內部類Line的實現

1.1 定義行的基本屬性

  • List<View>:管理行中的孩子
  • maxWidth:行的最大寬度
  • usedWidth:使用的寬度
  • height:行的高度
  • space:孩子之間的間距
  • 構造初始化maxWidth和space

    public Line(int maxWidth, int horizontalSpace) {
          this.maxWidth = maxWidth;
          this.space = horizontalSpace;
      }

1.2 addView(View view)方法實現

  • 往行的集合里添加View,更新行的使用寬度和高度

    /**
       * 往集合里添加孩子
       */
      public void addView(View view) {
          int childWidth = view.getMeasuredWidth();
          int childHeight = view.getMeasuredHeight();
    
          // 更新行的使用寬度和高度
          if (views.size() == 0) {
              // 集合里沒有孩子的時候
              if (childWidth > maxWidth) {
                  usedWidth = maxWidth;
                  height = childHeight;
              } else {
                  usedWidth = childWidth;
                  height = childHeight;
              }
          } else {
              usedWidth += childWidth + space;
              height = childHeight > height ? childHeight : height;
          }
    
          // 添加孩子到集合
          views.add(view);
      }

1.3 canAddView(View view)方法實現

  • 判斷是否能往行里添加孩子,如果孩子的寬度大于剩余寬度就不能

    /**
       * 判斷當前的行是否能添加孩子
       *
       * @return
       */
      public boolean canAddView(View view) {
          // 集合里沒有數據可以添加
          if (views.size() == 0) {
              return true;
          }
    
          // 最后一個孩子的寬度大于剩余寬度就不添加
          if (view.getMeasuredWidth() > (maxWidth - usedWidth - space)) {
              return false;
          }
    
          // 默認可以添加
          return true;
      }

2. 對容器進行測量(onMeasure方法的實現)

2.1 獲取寬度,計算maxWidth,構造傳入Line

  • 總寬度減去左右邊距就是行的最大寬度

    // 獲取總寬度
      int width = MeasureSpec.getSize(widthMeasureSpec);
      // 計算最大的寬度
      mMaxWidth = width - getPaddingLeft() - getPaddingRight();

2.2 循環獲取孩子進行測量

  • 獲取孩子總數,遍歷獲取每一個孩子,然后進行測量,測量完之后還需要將孩子添加到行集合里,然后將行添加到管理行的集合里

    // ******************** 測量孩子 ********************
      // 遍歷獲取孩子
      int childCount = this.getChildCount();
      for (int i = 0; i < childCount; i++) {
          View childView = getChildAt(i);
          // 測量孩子
          measureChild(childView, widthMeasureSpec, heightMeasureSpec);
    
          // 測量完需要將孩子添加到管理行的孩子的集合中,將行添加到管理行的集合中
    
          if (mCurrentLine == null) {
              // 初次添加第一個孩子的時候
              mCurrentLine = new Line(mMaxWidth, HORIZONTAL_SPACE);
    
              // 添加孩子
              mCurrentLine.addView(childView);
              // 添加行
              mLines.add(mCurrentLine);
    
          } else {
              // 行中有孩子的時候,判斷時候能添加
              if (mCurrentLine.canAddView(childView)) {
                  // 繼續往該行里添加
                  mCurrentLine.addView(childView);
              } else {
                  //  添加到下一行
                  mCurrentLine = new Line(mMaxWidth, HORIZONTAL_SPACE);
                  mCurrentLine.addView(childView);
                  mLines.add(mCurrentLine);
              }
          }
      }

2.3 測量自己

  • 由于寬度肯定是填充整個屏幕,這里只需要處理行的高度,累加所有的行高和豎直邊距算出高度

    // ******************** 測量自己 *********************
      // 測量自己只需要計算高度,寬度肯定會被填充滿的
      int height = getPaddingTop() + getPaddingBottom();
      for (int i = 0; i < mLines.size(); i++) {
          // 所有行的高度
          height += mLines.get(i).height;
      }
      // 所有豎直的間距
      height += (mLines.size() - 1) * VERTICAL_SPACE;
    
      // 測量
      setMeasuredDimension(width, height);

3. 指定孩子的顯示位置(onLayout方法的實現)

實現思路:指定孩子的位置,孩子給了行管理,所以這里具體孩子的位置應該交給行去指定。容器只需要指定行的位置就可以。

  • 遍歷獲取所有的行,讓行去指定孩子的位置,指定行的高度

    @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(t, l);
    
              // 更新高度
              t += line.height;
              if (i != mLines.size() - 1) {
                  // 不是最后一條就添加間距
                  t += VERTICAL_SPACE;
              }
          }
      }

4. Line中layout方法的實現(指定孩子的位置)

  • 遍歷獲取每一個孩子,獲取孩子的寬度和高度,計算上下左右的大小,指定孩子的位置,之后還需要更新孩子左邊的大小

    // 循環指定孩子位置
      for (View view : views) {
          // 獲取寬高
          int measuredWidth = view.getMeasuredWidth();
          int measuredHeight = view.getMeasuredHeight();
          // 重新測量
          view.measure(MeasureSpec.makeMeasureSpec(measuredWidth + avg, MeasureSpec.EXACTLY),
                  MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY));
          // 重新獲取寬度值
          measuredWidth = view.getMeasuredWidth();
    
          int top = t;
          int left = l;
          int right = measuredWidth + left;
          int bottom = measuredHeight + top;
          // 指定位置
          view.layout(left, top, right, bottom);
    
          // 更新數據
          l += measuredWidth + space;
      }

5. 細節處理

  • 第一次測量之后,行管理器中就有了行的對象,之后每次測量都會去創建下一行,這樣就會出現很多空行出來,所以需要在測量之前將集合清空。

    mLines.clear();
      mCurrentLine = null;
  • 每一行的最后一個孩子放不下就放到下一行,這樣每一行就都會有空格,這里將這些空格平分給行里的每一個孩子,重新指定其寬度。

    // 平分剩下的空間
      int avg = (maxWidth - usedWidth) / views.size();
    
      // 重新測量
      view.measure(MeasureSpec.makeMeasureSpec(measuredWidth + avg, MeasureSpec.EXACTLY),
              MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY));
      // 重新獲取寬度值
      measuredWidth = view.getMeasuredWidth();

6. 使用自定義屬性,將水平間距和豎直間距做成屬性,在布局中指定,增強擴展性

  • attrs文件指定屬性名

    <declare-styleable name="FlowLayout">
          <attr name="width_space" format="dimension"/>
          <attr name="height_space" format="dimension"/>
     </declare-styleable>
  • 構造中獲取屬性

    // 獲取自定義屬性
      TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
      horizontal_space = array.getDimension(R.styleable.FlowLayout_width_space,0);
      vertical_space =  array.getDimension(R.styleable.FlowLayout_height_space,0);
      array.recycle();
  • 布局中使用屬性

    app:width_space="10dp"
      app:height_space="10dp"

經過以上步驟之后,FlowLayout基本就已經實現了,接下來就是使用了。

二、流式布局的使用

  • 布局中申明

    <ScrollView
          xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          xmlns:app="http://schemas.android.com/apk/res-auto"
          android:fillViewport="true"
          tools:context="com.pinger.sample.MainActivity">
    
          <com.pinger.library.FlowLayout
              app:width_space="10dp"
              app:height_space="10dp"
              android:id="@+id/flow_layout"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:padding="5dp"/>
      </ScrollView>
  • 代碼中使用

  • 其實就是循環遍歷數據的長度,不斷的創建TextView,然后設置TextView的屬性和背景,包括五彩背景等,最后將TextView添加到FlowLayout中就可以。

 

 

 

 

 本文由用戶 BetFarnell 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!