Android自定义View(LineBreakLayout-自动换行的标签容器)
最近一段时间比较忙,都没有时间更新博客,今天公司的事情忙完得空,继续为我的自定义控件系列博客添砖加瓦。本篇博客讲解的是标签自动换行的布局容器,正好前一阵子有个项目中需要,想了想没什么难度就自己弄了。而自定义控件系列文章中对于自定义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的可用空间(除去margin和padding后的剩余空间)的左、上、右、下的坐标(相对于自身),相当于一个约束,如果子控件摆放的位置超过这个范围,超出的部分将不可见。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谢谢
源码下载:
注:没有积分的童鞋 请留言索要代码喔
Android自定义View(LineBreakLayout-自动换行的标签容器)的更多相关文章
- Android自定义组件之自动换行及宽度自适应View:WordWrapView
目的: 自定义一个ViewGroup,里面的子view都是TextView,每个子view TextView的宽度随内容自适应且每行的子View的个数自适应,并可以自动换行 一:效果图 二:代码 整 ...
- android自定义view系列:认识activity结构
标签: android 自定义view activity 开发中虽然我们调用Activity的setContentView(R.layout.activity_main)方法显示View视图,但是vi ...
- Android 自定义View合集
自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...
- Android 自定义 view(四)—— onMeasure 方法理解
前言: 前面我们已经学过<Android 自定义 view(三)-- onDraw 方法理解>,那么接下我们还需要继续去理解自定义view里面的onMeasure 方法 推荐文章: htt ...
- Android 自定义View及其在布局文件中的使用示例(三):结合Android 4.4.2_r1源码分析onMeasure过程
转载请注明出处 http://www.cnblogs.com/crashmaker/p/3549365.html From crash_coder linguowu linguowu0622@gami ...
- Android 自定义View及其在布局文件中的使用示例(二)
转载请注明出处 http://www.cnblogs.com/crashmaker/p/3530213.html From crash_coder linguowu linguowu0622@gami ...
- [原] Android 自定义View步骤
例子如下:Android 自定义View 密码框 例子 1 良好的自定义View 易用,标准,开放. 一个设计良好的自定义view和其他设计良好的类很像.封装了某个具有易用性接口的功能组合,这些功能能 ...
- Android自定义View
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24252901 很多的Android入门程序猿来说对于Android自定义View ...
- Android自定义View和控件之一-定制属于自己的UI
照例,拿来主义.我的学习是基于下面的三篇blog.前两是基本的流程,第三篇里有比较细致的绘制相关的属性.第4篇介绍了如何减少布局层次来提高效率. 1. 教你搞定Android自定义View 2. 教你 ...
随机推荐
- string [] 去除重复字符两个方法
不废话直接看图 结果 代码: this.txtListHTML.Text = String.Join(",", list.Replace("\r\n", &qu ...
- 【推荐】CentOS修复OpenSSH用户枚举漏洞
注:以下所有操作均在CentOS 6.8 x86_64位系统下完成. #漏洞说明# OpenSSH(OpenBSD Secure Shell)是OpenBSD计划组所维护的一套用于安全访问远程计算机的 ...
- 列表(list)之二 -运用篇 -快速生成规律性列表
生成列表[1*2,3*4,5*6,7*8,9*10,11*12] 方法一:list1=[]for i in range(1,13,2): list1.append(i*(i+1))print(list ...
- C#之winform实现文件拖拽功能
将一个文件拖拽到窗体的某个控件时,将该控件的路径显示在该控件上,只要拿到了路径自然可以读取文件中的内容了 将一个控件的属性AllowDrop设置为true,然后添加DragDrop.DragEnter ...
- laravel 5.5 接入蚂蚁金服官方SDK(支付宝APP支付为例)开发步骤
一.创建应用及配置 首先需要到蚂蚁金服开放平台(open.alipay.com)注册应用,获取应用id(APP_ID),并且配置应用,主要是签约应用,这个需要审核,一般2-5个工作日,审核通过后,去生 ...
- PHPCMS v9.5.6 通杀getshell(前台)
漏洞url:http://wooyun.jozxing.cc/static/bugs/wooyun-2014-062881.html 很好的fuzz思路. 文章提到:文件名前面的数字是被"干 ...
- [BZOJ 5093]图的价值
Description 题库链接 一个带标号的图的价值定义为每个点度数的 \(k\) 次方的和.给定 \(n\) 和 \(k\) ,请计算所有 \(n\) 个点的带标号的简单无向图的价值之和.对 \( ...
- [BZOJ 5071]小A的数字
Description 小A成为了一个数学家,他有一串数字A1,A2...An 每次可以进行如下操作,选择一个数字i(1<i<=n),将(Ai-1,Ai,Ai+1) 变为(Ai-1 + A ...
- UVA11404:Palindromic Subsequence
回文子串dp,最小字典序的话需要记录一下,注意是string型的,不能只记录一个字符,因为可能出现相等的情况 #include<cstdio> #include<cstdlib> ...
- UVA12186
给出一个树状关系图,公司里只有一个老板编号为0,其他人员从1开始编号.除了老板,每个人都有一个直接上司,没有下属的员工成为工人. 工人们想写一份加工资的请愿书,只有当不少于员工的所有下属的T%人递交请 ...