最近一段时间比较忙,都没有时间更新博客,今天公司的事情忙完得空,继续为我的自定义控件系列博客添砖加瓦。本篇博客讲解的是标签自动换行的布局容器,正好前一阵子有个项目中需要,想了想没什么难度就自己弄了。而自定义控件系列文章中对于自定义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

Android自定义View(LineBreakLayout-自动换行的标签容器)的更多相关文章

  1. Android自定义组件之自动换行及宽度自适应View:WordWrapView

    目的: 自定义一个ViewGroup,里面的子view都是TextView,每个子view  TextView的宽度随内容自适应且每行的子View的个数自适应,并可以自动换行 一:效果图 二:代码 整 ...

  2. android自定义view系列:认识activity结构

    标签: android 自定义view activity 开发中虽然我们调用Activity的setContentView(R.layout.activity_main)方法显示View视图,但是vi ...

  3. Android 自定义View合集

    自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...

  4. Android 自定义 view(四)—— onMeasure 方法理解

    前言: 前面我们已经学过<Android 自定义 view(三)-- onDraw 方法理解>,那么接下我们还需要继续去理解自定义view里面的onMeasure 方法 推荐文章: htt ...

  5. Android 自定义View及其在布局文件中的使用示例(三):结合Android 4.4.2_r1源码分析onMeasure过程

    转载请注明出处 http://www.cnblogs.com/crashmaker/p/3549365.html From crash_coder linguowu linguowu0622@gami ...

  6. Android 自定义View及其在布局文件中的使用示例(二)

    转载请注明出处 http://www.cnblogs.com/crashmaker/p/3530213.html From crash_coder linguowu linguowu0622@gami ...

  7. [原] Android 自定义View步骤

    例子如下:Android 自定义View 密码框 例子 1 良好的自定义View 易用,标准,开放. 一个设计良好的自定义view和其他设计良好的类很像.封装了某个具有易用性接口的功能组合,这些功能能 ...

  8. Android自定义View

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24252901 很多的Android入门程序猿来说对于Android自定义View ...

  9. Android自定义View和控件之一-定制属于自己的UI

    照例,拿来主义.我的学习是基于下面的三篇blog.前两是基本的流程,第三篇里有比较细致的绘制相关的属性.第4篇介绍了如何减少布局层次来提高效率. 1. 教你搞定Android自定义View 2. 教你 ...

随机推荐

  1. Linux下文件权限(一)用户ID和用户组ID

    最近在读<unix环境高级编程>,看到文件权限这里比较糊涂,主要设计多个用户ID和用户组ID,包括下面两个: (1)实际用户ID和实际用户组ID:这一部分表示我们究竟是谁.这两个字段在登录 ...

  2. 知物由学 | 基于DNN的人脸识别中的反欺骗机制

    "知物由学"是网易云易盾打造的一个品牌栏目,词语出自汉·王充<论衡·实知>.人,能力有高下之分,学习才知道事物的道理,而后才有智慧,不去求问就不会知道."知物 ...

  3. 一篇文章说透Nginx的rewrite模块

    rewrite模块即ngx_http_rewrite_module模块,主要功能是改写请求URI,是Nginx默认安装的模块.rewrite模块会根据PCRE正则匹配重写URI,然后发起内部跳转再匹配 ...

  4. Jenkins持续集成演示

    1.去我们的仓库修改一下代码 为了节约时间,我直接在网页上改了. 我们把布局页的footer信息改一下: 然后提交. 2.切换到Jenkins可以看到已经在构建了 等待构建完成. 3.访问我们部署的地 ...

  5. 解决有关flask-socketio中服务端和客户端回调函数callback参数的问题(全网最全)

    由于工作当中需要用的flask_socketio,所以自己学习了一下如何使用,查阅了有关文档,当看到回调函数callback的时候,发现文档里都描述的不太清楚,最后终于琢磨出来了,分享给有需要的朋友 ...

  6. [COGS 2877]老m凯的疑惑

    Description Margatroid退役之后沉迷文化课 这天,写完数学作业之后的他脑洞大开,决定出一道比NOIP2017 D1T1<小凯的疑惑math>还要好的题 题面是这样的 $ ...

  7. [HNOI2010]PLANAR

    题目描述 若能将无向图G=(V,E)画在平面上使得任意两条无重合顶点的边不相交,则称G是平面图.判定一个图是否为平面图的问题是图论中的一个重要问题.现在假设你要判定的是一类特殊的图,图中存在一个包含所 ...

  8. [SDOI2006]仓库管理员的烦恼

    题目描述 仓库管理员M最近一直很烦恼,因为他的上司给了他一个艰难的任务:让他尽快想出一种合理的方案,把公司的仓库整理好. 已知公司共有n个仓库和n种货物,由于公司进货时没能很好的归好类,使得大部分的仓 ...

  9. ●Joyoi Easy

    题链: http://www.joyoi.cn/problem/tyvj-1952题解: 概率dp (先做的BZOJ 4318: OSU![本人题解],然后就感觉这个题很简单了) 令p[i]表示第i个 ...

  10. ●POJ 1329 Circle Through Three Points

    题链: http://poj.org/problem?id=1329 题解: 计算几何,求过不共线的三点的圆 就是用向量暴力算出来的东西... (设出外心M的坐标,由于$|\vec{MA}|=|\ve ...