Android自定義View精品(LineBreakLayout-自動換行的標籤容器)

版權聲明:本文爲openXu原創文章【openXu的博客】,未經博主允許不得以任何形式轉載

  最近一段時間比較忙,都沒有時間更新博客,今天公司的事情忙完得空,繼續爲我的自定義控件系列博客添磚加瓦。本篇博客講解的是標籤自動換行的佈局容器,正好前一陣子有個項目中需要,想了想沒什麼難度就自己弄了。而自定義控件系列文章中對於自定義ViewGroup上次只是講解了一些基礎和步驟 Android自定義ViewGroup(四、打造自己的佈局容器),這次就着這個例子我們來完成一個能在項目中使用的自定義佈局容器。

1. 初步分析

  首先我們看一看要完成的效果圖:
      這裏寫圖片描述

  上面紅色標示出的就是我們要實現的效果,Android自帶的佈局容器是沒辦法達到這樣的效果的。每個標籤長度不一定,當一行擺放滿需要自動換行,標籤之間左右上下有一定的距離,這就是這個容器的需求。其中每個標籤可以用TextView,標籤點擊之後有選中的效果(邊框和字體變爲藍色)。初步分析,我們自定義的容器需要兩個自定義屬性,維護兩個標籤集合(所有標籤、選中標籤)。接下來我們就動手一步步完成。

2. 定義屬性

  在values/attrs.xml中爲我們的容器定義兩個屬性,一個是標籤左右的間隔距離LEFT_RIGHT_SPACE ,另一個是標籤的行距ROW_SPACE,然後在構造方法中獲取屬性值:

values/attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="LineBreakLayout">
        <!--標籤之間左右距離-->
        <attr name="leftAndRightSpace" format="dimension" />
        <!--標籤行距-->
        <attr name="rowSpace" format="dimension" />
    </declare-styleable>
</resources>

佈局中使用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:openXu="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.openxu.lbl.LineBreakLayout
        android:id="@+id/lineBreakLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="20dip"
        openXu:leftAndRightSpace="20dip"
        openXu:rowSpace="10dip"/>
</LinearLayout>

LineBreakLayout.java

public class LineBreakLayout extends ViewGroup {
   private final static String TAG = "LineBreakLayout";
   /**
    * 所有標籤
    */
   private List<String> lables;
   /**
    * 選中標籤
    */
   private List<String> lableSelected = new ArrayList<>();

   //自定義屬性
   private int LEFT_RIGHT_SPACE; //dip
   private int ROW_SPACE;

   public LineBreakLayout(Context context) {
      this(context, null);
   }
   public LineBreakLayout(Context context, AttributeSet attrs) {
      this(context, attrs, 0);
   }
   public LineBreakLayout(Context context, AttributeSet attrs, int defStyleAttr) {
      super(context, attrs, defStyleAttr);
      TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LineBreakLayout);
      LEFT_RIGHT_SPACE = ta.getDimensionPixelSize(R.styleable.LineBreakLayout_leftAndRightSpace, 10);
      ROW_SPACE = ta.getDimensionPixelSize(R.styleable.LineBreakLayout_rowSpace, 10);
      ta.recycle(); //回收
      // ROW_SPACE=20   LEFT_RIGHT_SPACE=40
      Log.v(TAG, "ROW_SPACE="+ROW_SPACE+"   LEFT_RIGHT_SPACE="+LEFT_RIGHT_SPACE);
   }

   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   }

   @Override
   protected void onLayout(boolean changed, int l, int t, int r, int b) {
   }
}

3. 單個標籤

values/color.xml

<color name="tv_gray">#666666</color>
<color name="tv_blue">#308BE9</color>     //藍色
<color name="divider_gray">#d9d9d9</color>//細分割線顏色

標籤背景drawable/shape_item_lable_bg.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<!--選中效果-->
<item android:state_selected="true">
    <shape >
        <solid android:color="#ffffff" />
        <stroke android:color="@color/tv_blue"
            android:width="2px"/>
        <corners android:radius="10000dip"/>
    </shape>
</item>
<!--默認效果-->
<item>
    <shape >
        <solid android:color="#ffffff" />
        <stroke android:color="@color/divider_gray"
            android:width="2px"/>
        <corners android:radius="10000dip"/>
    </shape>
</item>
</selector>

標籤佈局layout/item_lable.xml

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/shape_item_lable_bg"
    android:paddingBottom="5dip"
    android:paddingLeft="12dip"
    android:paddingRight="12dip"
    android:paddingTop="5dip"
    android:text="lable"
    android:textSize="15sp"
    android:textColor="@color/tv_gray" />

4. 提供接口setlables(List lables)向容器中添加標籤

/**
 * 添加標籤
 * @param lables 標籤集合
 * @param add 是否追加
    */
public void setLables(List<String> lables, boolean add){
   if(this.lables == null){
      this.lables = new ArrayList<>();
   }
   if(add){
      this.lables.addAll(lables);
   }else{
      this.lables.clear();
      this.lables = lables;
   }
   if(lables!=null && lables.size()>0){
      LayoutInflater inflater = LayoutInflater.from(getContext());
      for (final String lable : lables) {
         //獲取標籤佈局
         final TextView tv = (TextView) inflater.inflate(R.layout.item_lable, null);
         tv.setText(lable);
         //設置選中效果
         if (lableSelected.contains(lable)) {
            //選中
            tv.setSelected(true);
            tv.setTextColor(getResources().getColor(R.color.tv_blue));
         } else {
            //未選中
            tv.setSelected(false);
            tv.setTextColor(getResources().getColor(R.color.tv_gray));
         }
         //點擊標籤後,重置選中效果
         tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
               tv.setSelected(tv.isSelected() ? false : true);
               if (tv.isSelected()) {
                  tv.setTextColor(getResources().getColor(R.color.tv_blue));
                  //將選中的標籤加入到lableSelected中
                  lableSelected.add(lable);
               } else {
                  tv.setTextColor(getResources().getColor(R.color.tv_gray));
                  lableSelected.remove(lable);
               }
            }
         });
         //將標籤添加到容器中
         addView(tv);
      }
   }
}

5. 重寫onMeasure()計算容器高度

  對於onMeasure()方法,之前已有一篇博客詳細講解,如果不明白可參考 Android自定義View(三、深入解析控件測量onMeasure)。這裏針對本佈局單獨說明一下,本佈局在寬度上是使用的建議的寬度(填充父窗體或者具體的size),如果需要wrap_content的效果,還需要重新計算,當然這種需求是非常少見的,所以直接用建議寬度即可;佈局的高度就得看其中的標籤需要佔據多少行(row ),那麼高度就爲row * 單個標籤的高度+(row -1) * 行距,具體實現代碼如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   //爲所有的標籤childView計算寬和高
   measureChildren(widthMeasureSpec, heightMeasureSpec);

   //獲取高的模式
   int heightMode = MeasureSpec.getMode(heightMeasureSpec);
   //建議的高度
   int heightSize = MeasureSpec.getSize(heightMeasureSpec);
   //佈局的寬度採用建議寬度(match_parent或者size),如果設置wrap_content也是match_parent的效果
   int width = MeasureSpec.getSize(widthMeasureSpec);

   int height ;
   if (heightMode == MeasureSpec.EXACTLY) {
      //如果高度模式爲EXACTLY(match_perent或者size),則使用建議高度
      height = heightSize;
   } else {
      //其他情況下(AT_MOST、UNSPECIFIED)需要計算計算高度
      int childCount = getChildCount();
      if(childCount<=0){
         height = 0;   //沒有標籤時,高度爲0
      }else{
         int row = 1;  // 標籤行數
         int widthSpace = width;// 當前行右側剩餘的寬度
         for(int i = 0;i<childCount; i++){
            View view = getChildAt(i);
            //獲取標籤寬度
            int childW = view.getMeasuredWidth();
            Log.v(TAG , "標籤寬度:"+childW +" 行數:"+row+"  剩餘寬度:"+widthSpace);
            if(widthSpace >= childW ){
               //如果剩餘的寬度大於此標籤的寬度,那就將此標籤放到本行
               widthSpace -= childW;
            }else{
               row ++;    //增加一行
               //如果剩餘的寬度不能擺放此標籤,那就將此標籤放入一行
               widthSpace = width-childW;
            }
            //減去標籤左右間距
            widthSpace -= LEFT_RIGHT_SPACE;
         }
         //由於每個標籤的高度是相同的,所以直接獲取第一個標籤的高度即可
         int childH = getChildAt(0).getMeasuredHeight();
         //最終佈局的高度=標籤高度*行數+行距*(行數-1)
         height = (childH * row) + ROW_SPACE * (row-1);

         Log.v(TAG , "總高度:"+height +" 行數:"+row+"  標籤高度:"+childH);
      }
   }

   //設置測量寬度和測量高度
   setMeasuredDimension(width, height);
}

6. 重寫onLayout()擺放標籤

  onLayout(boolean changed, int l, int t, int r, int b)方法是一個抽象方法,自定義ViewGroup時必須實現它,用於給佈局中的子控件分配位置,其中的參數l,t,r,b分別代表本ViewGroup的可用空間(除去marginpadding後的剩餘空間)的左、上、右、下的座標(相對於自身),相當於一個約束,如果子控件擺放的位置超過這個範圍,超出的部分將不可見。onLayout()的實現代碼如下,註釋已經很清楚,就不再贅述:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
   int row = 0;
   int right = 0;   // 標籤相對於佈局的右側位置
   int botom;       // 標籤相對於佈局的底部位置
   for (int i = 0; i < getChildCount(); i++) {
      View childView = getChildAt(i);
      int childW = childView.getMeasuredWidth();
      int childH = childView.getMeasuredHeight();
      //右側位置=本行已經佔有的位置+當前標籤的寬度
      right += childW;
      //底部位置=已經擺放的行數*(標籤高度+行距)+當前標籤高度
      botom = row * (childH + ROW_SPACE) + childH;
      // 如果右側位置已經超出佈局右邊緣,跳到下一行
      // if it can't drawing on a same line , skip to next line
      if (right > (r - LEFT_RIGHT_SPACE)){
         row++;
         right = childW;
         botom = row * (childH + ROW_SPACE) + childH;
      }
      Log.d(TAG, "left = " + (right - childW) +" top = " + (botom - childH)+
            " right = " + right + " botom = " + botom);
      childView.layout(right - childW, botom - childH,right,botom);
     
       right += LEFT_RIGHT_SPACE;
   }
}

7. 使用

  到此爲止,這個自動換行的標籤佈局已經定義完成,現在就讓我們使用看看運行效果怎麼樣,這裏爲佈局設置了紅色背景,用於直觀的查看我們的計算有沒有出錯,可以看到,標籤沒有超出佈局,佈局的寬高也正好包裹所有標籤:

List<String> lable = new ArrayList<>();
lable.add("經濟");
lable.add( "娛樂");
lable.add("八卦");
lable.add("小道消息");
lable.add("政治中心");
lable.add("彩票");
lable.add("情感");
//設置標籤
lineBreakLayout.setLables(lable, true);
//獲取選中的標籤
List<String> selectedLables = lineBreakLayout.getSelectedLables();

運行效果:
      這裏寫圖片描述

8.總結

  這個佈局的實現在技術上來說是比較簡單的,但是它非常具有代表性,非常典型的自定義ViewGroup,相信如果能完全寫下這個示例,下次需要自定義ViewGroup的時候也不會有太大難度了。當然這個佈局不是完美的,就算Android自帶的佈局也不能說完美,只要它能滿足我們項目中的開發需求就ok。對於自定義ViewGroup還有一些重要的知識點(事件處理等)在後面的博客中會陸續講解。

歡迎關注,希望在這裏有你想要的,博主會持續更新高(di)質(ji)量(shu)的文章和大家交流學習

喜歡請點贊,no愛請勿噴~O(∩_∩)O謝謝

##源碼下載:

注:沒有積分的童鞋 請留言索要代碼喔

http://download.csdn.net/detail/u010163442/9698873

https://github.com/openXu/LineBreakLayout

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