Android自定義標(biāo)簽列表 詳細(xì)解析

最近項目的需求:需要一個View可以清晰的展現(xiàn)出員工的名字,并且可以進(jìn)行添加或刪除員工,本來打算Github找一個,但最近看了 Android開發(fā)藝術(shù)探索 看完了自定View的章節(jié)一直沒有動手寫一個,終于現(xiàn)在寫完了..

先看demo效果圖:

效果圖

Android自帶的LineLayout只支持水平或者垂直布局不支持自動換行這點相信都知道,只要解決LineLayout不會自動換行和確定每個子View應(yīng)該顯示的位置,基本就算解決問題了.

有了解過自定義View都知道主要有兩個方法:

  • onMeasure() --- 測量 (測量View的寬高)
  • onLayout() --- 布局 (把View顯示在想要的位置)

看一眼上面的效果圖,簡單的說onMeasure就是計算出整個ViewGrop()在屏幕中所占的大小.onLayout是控制每個藍(lán)色的標(biāo)簽在整個View的位置.

onMeasure寬高應(yīng)該是多少,該怎么樣測量?

邏輯是這樣的: 以上面的gif圖為例,標(biāo)簽的第一行從"陳奕迅"到"權(quán)志龍"一共能塞下5個標(biāo)簽,并且仔細(xì)看會發(fā)現(xiàn)"權(quán)志龍"的標(biāo)簽的右邊還有剩下了一小部分空間,但是多出來空間不足以塞下一個標(biāo)簽"周星馳"所以只能加多一行了,到了第二行的最后一個標(biāo)簽"趙又廷"又發(fā)現(xiàn)剩下的空間塞不下"吳孟達(dá)"了只能再加多一行...一直到最后一行的一共需要4行才能剛好夠塞下所有標(biāo)簽.

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

    measureChildren(widthMeasureSpec, heightMeasureSpec);

    //rows表示的是行數(shù),默認(rèn)為1
    int rows = 1;
    // lineWidth當(dāng)前行累計的總寬度
    int lineWidth = 0;
    //eachHeight表示的是當(dāng)前行的高度(注意是當(dāng)前行)
    int eachHeight ;

        //有N個子View就for循環(huán)執(zhí)行N次
     for (int i = 0; i < childCount; i++) {
        //獲取第N個子View
        final View childView = getChildAt(i);
        //獲取第N個子View的布局參數(shù)
        LinearLayout.LayoutParams childViewLayoutParams= (LayoutParams) childView.getLayoutParams();
        //獲取子View的布局參數(shù) 距離左右邊緣的大小
        int left=childViewLayoutParams.leftMargin;
        int right=childViewLayoutParams.rightMargin;
        /*這里比較重要,只要for循環(huán)不是執(zhí)行到最后一次,就用當(dāng)前子View的(高度或?qū)挾?和下一個子View
          的(高度或?qū)挾?進(jìn)行比較,把較大的值用(childViewMaxHeight或childViewmaxWidth)記錄下來.*/
        if(i!=childCount-1){
            childViewMaxHeight=getChildAt(i).getMeasuredHeight()<getChildAt(i+1).getMeasuredHeight()?getChildAt(i+1).getMeasuredHeight():childViewMaxHeight;
            childViewmaxWidth=getChildAt(i).getMeasuredWidth()<getChildAt(i+1).getMeasuredWidth()?getChildAt(i+1).getMeasuredWidth():childViewmaxWidth;           
        }
        //如果當(dāng)前行的總寬度+當(dāng)前子View的寬度+子View距離左右邊緣的大小>當(dāng)前屏幕的寬度
        if(lineWidth+getChildAt(i).getMeasuredWidth() + left+right>=widthSpecSize){ 
            //說明需要加一行,所以rows++
            rows++;
            lineWidth=left;
        }
        //當(dāng)前行的總寬度+=當(dāng)前子View的寬度+距離左右間距大小
        lineWidth += (getChildAt(i).getMeasuredWidth()  + left+right);
        //每行的高度 = 當(dāng)前行的最高高度
        eachHeight = childViewMaxHeight +top_bottom_margin ;
   
        if (childCount == 0) {
            //如果一個子View都沒有寬度和高度設(shè)置為0即可
            setMeasuredDimension(0, 0);
        } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecpMode == MeasureSpec.AT_MOST) {
            //如果寬高的屬性都是wrap_content,寬度是行的總寬度,高度是標(biāo)簽的 高度*行數(shù)
            setMeasuredDimension(lineWidth, rows * eachHeight + top_bottom_margin);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            //如果高度和寬度的屬性都是march_parent,
            setMeasuredDimension(lineWidth, heightSpecpSize);
        } else if (heightSpecpMode == MeasureSpec.AT_MOST) {
            //如果高度的屬性是wrap_content的用下面的測量方式
            //寬度是屏幕的大小,高度是標(biāo)簽的(高度*行數(shù))
            setMeasuredDimension(widthSpecSize,  rows * eachHeight + top_bottom_margin);
        }
    }

需要注意的是下面這條判斷語句:

if(lineWidth+getChildAt(i).getMeasuredWidth() + left+right>=widthSpecSize){
rows++;
lineWidth=left;
}

因為lineWidth是當(dāng)前行的第一個標(biāo)簽到最后一個能完整顯示的標(biāo)簽所累加的寬度總和,而widthSpecSize是當(dāng)前手機屏幕寬度,這里的需求是每個標(biāo)簽都需要完整的顯示不能超出屏幕的寬度,否者會顯示不全,所以只要 [前者加上當(dāng)前標(biāo)簽的寬度總和>手機屏幕寬度] 說明當(dāng)前行已經(jīng)沒有足夠的位置塞下當(dāng)前標(biāo)簽了,所以需要加多一行 rows++.

onLayout里面要如何為每個子View顯示在合適的位置?

protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //當(dāng)前子View的left
        int childLeft = 0;
        // childCount子View的個數(shù)
        final int childCount = getChildCount();
        //獲取第一個子View
      View childView = getChildAt(0);
      //獲取第一個子View的布局屬性
      LinearLayout.LayoutParams childViewLayoutParams = (LayoutParams) childView.getLayoutParams();
      //獲取第一個子View左右距離邊緣的大小
      int left = childViewLayoutParams.leftMargin;
      int right = childViewLayoutParams.rightMargin;
      //把子View距離左邊緣的值賦值給childLeft(為什么這么做下面會說)
      childLeft = left;
      //初始化當(dāng)前當(dāng)前子View的高度
      int currentHeight = 0;
      //判斷當(dāng)前是否是第一行
      boolean firstLine = true;

       //略....
      for (int i = 0; i < childCount; i++) {
          childView = getChildAt(i);
          childViewLayoutParams = (LayoutParams) childView.getLayoutParams();
          left = childViewLayoutParams.leftMargin;
          right = childViewLayoutParams.rightMargin;
          //如果當(dāng)前子View可見
          if (childView.getVisibility() != View.GONE) {
              //獲取當(dāng)前子View的寬度
              final int childWidth = childView.getMeasuredWidth();
               //如果childLeft+子View的寬度 > 手機屏幕的寬度
              if (childLeft + childWidth + left + right > getMeasuredWidth()) {
                  //childLeft從新開始計算
                  childLeft = left;
                  //currentHeight += 當(dāng)前行高度最高的子View的高度(childViewMaxHeight在onMeasure申明)
                  currentHeight += childViewMaxHeight + top_bottom_margin;  
                  firstLine = false;
              }
             if (firstLine) {
                  childView.layout(childLeft, top_bottom_margin, childLeft + childWidth, childViewMaxHeight + top_bottom_margin);
              } else {
                  childView.layout(childLeft, currentHeight + top_bottom_margin, childLeft + childWidth, childViewMaxHeight + currentHeight + top_bottom_margin);
              }
              //childLeft進(jìn)行累加
              childLeft += childWidth + left + right;
          }

      }
  }

onLayout邏輯相比onMeasure比較復(fù)雜,但理解了原理之后其實很簡單.

先理清幾個比較重要的成員變量的含義:
  • childLeft
    有過自定義屬性經(jīng)驗的人都應(yīng)該知道確定一個View在屏幕上顯示的位置主要由4個參數(shù)決定:Left,Top,Right,Bottom這里的childLeft就是每個子View的Left,請注意這里的childLeft的值是動態(tài)改變的.
    可以看到onLayout函數(shù)中for循環(huán)語句的最后一段代碼是:

childLeft += childWidth + left + right;

意思是每遍歷一個子View就 讓 childLeft +=當(dāng)前子View 的寬度
以下面的圖為例:

屏幕快照 2017-08-06 下午9.43.31.png

假設(shè)陳奕迅這個子View的寬度是100,如果陳奕迅不想被周星馳擋住那么周星馳的Left至少要大于100

if (childLeft + childWidth + left + right > getMeasuredWidth()) {
                  childLeft = left;
                  currentHeight += childViewMaxHeight + top_bottom_margin;  
                  firstLine = false;
              }

上面這段代碼的意思可以這樣理解:
假設(shè)for循環(huán)執(zhí)行到了第二行的周星馳的時候進(jìn)行了判斷語句的時候, childLeft的值就是權(quán)志龍這個標(biāo)簽的 Left,如果childLeft + 當(dāng)前標(biāo)簽的寬度(這里指周星馳這個標(biāo)簽) > 手機屏幕的寬度,說明周星馳顯示不下了,只能換行了,所以currentHeight+= childViewMaxHeight(childViewMaxHeight在onMeasure申明過)這樣就不會出現(xiàn)第二行的周星馳把第一行的陳奕迅擋住的情況

if (firstLine) {
                  childView.layout(childLeft, top_bottom_margin, childLeft + childWidth, childViewMaxHeight + top_bottom_margin);
              } else {
                  childView.layout(childLeft, currentHeight + top_bottom_margin, childLeft + childWidth, childViewMaxHeight + currentHeight + top_bottom_margin);
              }

最后要注意的就是上面的代碼:
如果是第一行Top就是top_bottom_margin(這里是讓每個標(biāo)簽的上下之間有一個間距)
否者如果第二行Top就是取當(dāng)前標(biāo)簽所在的那一行中子View高度最高的值+top_bottom_margin

基本onLayout需要注意的代碼就是這些.

剩下的是添加標(biāo)簽,刪除標(biāo)簽,切換刪除標(biāo)簽的模式代碼,不寫解析了.沒看懂的可以在下面留言.

private boolean isMutil = false;
private List<View> AllViews;


//添加標(biāo)簽的主要的代碼:
public MyLinearLayout addTag(String... tag) {
    for (int i = 0; i < tag.length; i++) {
        if (AllViews == null)
            AllViews = new ArrayList<>();
        TextView tagView=NewTagView(tag[i]);
        addView(tagView);
        if (!AllViews.contains(tagView))
            AllViews.add(tagView);
        MultiRemove(isMutil);
    }
    return this;
}

 private TextView NewTagView(String text) {
    TextView tagView = new TextView(getContext());
    tagView.setTextSize(20);
    tagView.setPadding(30, 20, 30, 20);
    tagView.setTextColor(Color.WHITE);
    tagView.setBackgroundResource(normal_color);
    tagView.setText(text);
    LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    lp.setMargins(20, 10, 0, 0);
    tagView.setLayoutParams(lp);
    return tagView;
}


private List<View> selectView;

 //執(zhí)行移除多個標(biāo)簽的代碼:
public void removeMultiTagViews() {
    for (View view : selectView) {
        removeView(view);
        AllViews.remove(view);
    }
}

//切換移除標(biāo)簽的模式(多選后點擊刪除按鈕刪除 或 長按某個標(biāo)簽直接刪除):
public void ChangeRemoveModel() {
    isMutil = isMutil == false ? true : false;
    MultiRemove(isMutil);
}




private void MultiRemove(boolean isMulti) {

    for (View view : AllViews) {
        final TextView tagView = (TextView) view;
        if (isMulti) {
            tagView.setOnClickListener(null);
            tagView.setOnLongClickListener(new OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    removeView(v);
                    AllViews.remove(v);
                    return false;
                }
            });
        } else {
            if (selectView == null)
                selectView = new ArrayList<>();

            tagView.setOnLongClickListener(null);
            tagView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (selectView.contains(v)) {
                        tagView.setBackgroundResource(normal_color);
                        selectView.remove(v);
                    } else {
                        TextView tagView1 = (TextView) v;
                        tagView1.setBackgroundResource(select_color);
                        selectView.add(v);
                    }
                }
            });
        }
    }
}

調(diào)用代碼:


    Button add;
    Button remove;
    Button switch_mode;
    EditText tag_content;
    MyLinearLayout tag_layout; //本文的自定義View
    ......
  public void onClick(View v) {
        switch (v.getId()){
            case R.id.add:
                if(tag_content.getText().toString().isEmpty()) {
                    Toast.makeText(MainActivity.this, "請輸入tag內(nèi)容再點擊新增TAG", Toast.LENGTH_SHORT).show();
                    return;
                }
                tag_layout.addTag(tag_content.getText().toString());
                tag_content.setText("");               
                break;
            case R.id.remove:
                tag_layout.removeMultiTagViews();
                break;
            case R.id.switch_mode:
                tag_layout.ChangeRemoveModel();

                break;
        }
    }

如果文章有錯誤的地方希望能指出,避免誤導(dǎo)他人.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容