自定义标签流水型布局
对于自定义view我们平时在项目中使用的很多,但是我们自己写的却很少,因为现在开源的代码越来越多,使得我们的惰性也变大了。
前段时间朋友找我解决个需求,他们产品需要做一个类似于标签的流水型布局如下图所示:
数据:/#Python#/是一种/#入门快#/、/#功能强大#/、/#高效灵活#/的编程语言,学会之后无论是想进入/#数据分析#/、/#人工智能#/、/#网站开发#/、/#网络安全#/、/#集群运维#/这些领域,还是希望掌握第一门编程语言,都可以用 Python 来开启美好未来的无限可能!
备注:在 "/#" 和 "#/" 中为特殊字符需要进行变小字体、加圆角背景
正常情况下我们使用Textview,组件内部会给我们做到自动换行,现在对于这个需求 我采用的是自定义viewgroup,每一行使用多个TextView拼接的方式来排版,通过计算字符的方式来进行换行。(方式有很多种,希望大家多给提提宝贵意见)
一、初始化
初始化普通字体TextView、特殊字体TextView,我们需要根据他们所对应的的画笔Paint来测量绘画每个字符所需要的长度,我们需要计算每个分隔号好的字符串来做到换行处理。
/**
* 初始化
*/
private void init(Context context) {
this.context = context;
//比较普通字体和特殊字体的大小来决定行高(layout排布的时候把小字体的居中显示)
maxLineHeight =orTextSize >spTextSize ?orTextSize *4 :spTextSize *4;
//特殊字体
specialView =new TextView(this.context);
specialView.setTextSize(spTextSize);
specialPaint =specialView.getPaint();
//普通字体
ordinaryView =new TextView(this.context);
ordinaryView.setTextSize(orTextSize);
ordinaryPaint =ordinaryView.getPaint();
}
二、拆分字体
我是采用多次替换的方式来进行分隔普通字体和特殊字体
/**
* 拆分字体
*/
private String[] split(String data) {
data = data.replaceAll("/#", "&");
data = data.replaceAll("#/", "~&");
String[] split = data.split("&");
return split;
}
/**
* creat view
*/
private void startCalc() {
if (null == data || data.length() == 0) return;
if (isStart) return;
String[] split = split(data);
for (int i = 0; i < split.length; i++) {
String str = split[i];
if (null != str && str.length() > 0) {
calc(str.contains("~"), str);
}
}
invalidate();
isStart = true;
}
/**
* 计算数据
*/
private void calc(boolean flag, String data) {
//特殊字体
if (flag) {
data = data.replace("~", "");
addSpecialText(data);
// Log.d("TAG-main","特殊字体:"+data);
// 普通字体
} else {
addOrdinaryText(data);
// Log.d("TAG-main","普通字体:"+data);
}
}
三、计算每行可放置几个TextView
这里我通过viewgroup的宽度来计算每行可放置几个view,有一点要考虑的如果剩下的宽度不足以放置下一个view 我们就要根据剩余宽度来计算可放置几个字符来拆分下一个字符串,来拆分成两个或者多个(我使用的是递归的方式)
/**
* 添加普通字体
*
* @param data
*/
private void addOrdinaryText(String data) {
float v = ordinaryPaint.measureText(data);
//如果字符串宽度小于剩余宽度 直接创建一个textview
if (v <= lastWidth) {
lastWidth = lastWidth - v;
creatView(data);
//如果字符串宽度大于剩余宽度 需要拆分字符串
} else {
String str = getOrdinaryStr(data);
lastWidth = width;
creatView(str);
addOrdinaryText(data.substring(str.length(), data.length()));
}
}
/**
* 获取剩余控件特殊字体可填入的内容
*
* @param data
* @return
*/
private String getOrdinaryStr(String data) {
String str = "";
char[] chars = data.toCharArray();
for (int i = 0; i < chars.length; i++) {
str += chars[i];
if (ordinaryPaint.measureText(str) >= lastWidth) {
if (i <= chars.length) {
str = data.substring(0, i);
}
break;
}
}
return str;
}
四、测量、排布
我们这里测量的目的是要获取当前viewgroup的宽度,以及做一个自适应的高度,排布的话就是layout函数在摆放子view的位置来达到一个流水型布局的目的。(使用的指定viewgroup具体的宽度数值)
/**
* 测量
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int mode = MeasureSpec.getMode(widthMeasureSpec);
if (width >0 && mode == MeasureSpec.EXACTLY) {
this.width = width;
lastWidth = width;
startCalc();
}
int count = getChildCount();
int l =0;
float totalHeight =lineHeight;
int viweHeight =0;
int maxLineHeight =0;
for (int i =0; i < count; i++) {
View v = getChildAt(i);
if (v.getVisibility() != View.GONE) {
measureChild(v, widthMeasureSpec, heightMeasureSpec);
View childAt = getChildAt(i);
int viewWidth = childAt.getMeasuredWidth();
viweHeight = childAt.getMeasuredHeight();
if (viewWidth > width - l) {
totalHeight += maxLineHeight +lineHeight;
l =0;
maxLineHeight =0;
}
if (maxLineHeight ==0) {
maxLineHeight = viweHeight;
}
if (viweHeight > maxLineHeight) {
maxLineHeight = viweHeight;
}
if (width - l >0) {
l += viewWidth;
}
}
}
setMeasuredDimension(width, (int) totalHeight + viweHeight);
}
/**
* 排布
*
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed,int l,int t,int r,int b) {
int childCount = getChildCount();
float totalHeight =lineHeight;
if (childCount ==0) {
return;
}
l =0;
for (int i =0; i < childCount; i++) {
View childAt = getChildAt(i);
int viewWidth = childAt.getMeasuredWidth();
int viewHeight = childAt.getMeasuredHeight();
if (viewWidth >width - l) {
totalHeight +=maxLineHeight +lineHeight;
l =0;
maxLineHeight =orTextSize >spTextSize ?orTextSize *4 :spTextSize *4;
}
if (viewHeight >maxLineHeight) {
maxLineHeight = viewHeight;
t = (int) totalHeight;
b = (int) totalHeight + viewHeight;
}else {
t = (int) totalHeight + ((maxLineHeight - viewHeight) /2);
b = (int) totalHeight + viewHeight + ((maxLineHeight - viewHeight) /2);
}
childAt.layout(l, t, l + viewWidth, b);
if (width - l >0) {
l += (viewWidth) ;
}
}
}
我上面大概的讲解了一下我实现这个自定义viewgroup的思路,完整的代码我放到下面了,各位看官自行理解!!!
Java代码:
/**
* 自定义标签流水型布局
* create by wxy on 2020/1/13
*/
public class LabelLayout extends ViewGroup {
//特殊字体属性
//字体大小
private int spTextSize = 10;
//字体颜色
private int spTextColor = R.color.white_a_color;
//普通字体属性
//字体大小
private int orTextSize = 14;
//字体颜色
private int orTextColor = R.color.gray_a_color;
//行高
private int lineHeight = 10;
private Context context;
private float width;
private float lastWidth;
//特殊字体textview
private TextView specialView;
//特殊字体textview paint
private Paint specialPaint;
//普通字体textview
private TextView ordinaryView;
//特殊字体textview paint
private Paint ordinaryPaint;
private int maxLineHeight;
public LabelLayout(Context context) {
super(context);
init(context);
}
public LabelLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public LabelLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/**
* 初始化
*/
private void init(Context context) {
this.context = context;
maxLineHeight = orTextSize > spTextSize ? orTextSize * 4 : spTextSize * 4;
//特殊字体
specialView = new TextView(this.context);
specialView.setTextSize(spTextSize);
specialPaint = specialView.getPaint();
//普通字体
ordinaryView = new TextView(this.context);
ordinaryView.setTextSize(orTextSize);
ordinaryPaint = ordinaryView.getPaint();
}
/**
* 测量
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int mode = MeasureSpec.getMode(widthMeasureSpec);
if (width > 0 && mode == MeasureSpec.EXACTLY) {
this.width = width;
lastWidth = width;
startCalc();
}
int count = getChildCount();
int l = 0;
float totalHeight = lineHeight;
int viweHeight = 0;
int maxLineHeight = 0;
for (int i = 0; i < count; i++) {
View v = getChildAt(i);
if (v.getVisibility() != View.GONE) {
measureChild(v, widthMeasureSpec, heightMeasureSpec);
View childAt = getChildAt(i);
int viewWidth = childAt.getMeasuredWidth();
viweHeight = childAt.getMeasuredHeight();
if (viewWidth > width - l) {
totalHeight += maxLineHeight + lineHeight;
l = 0;
maxLineHeight = 0;
}
if (maxLineHeight == 0) {
maxLineHeight = viweHeight;
}
if (viweHeight > maxLineHeight) {
maxLineHeight = viweHeight;
}
if (width - l > 0) {
l += viewWidth;
}
}
}
setMeasuredDimension(width, (int) totalHeight + viweHeight);
}
/**
* 排布
*
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
float totalHeight = lineHeight;
if (childCount == 0) {
return;
}
l = 0;
for (int i = 0; i < childCount; i++) {
View childAt = getChildAt(i);
int viewWidth = childAt.getMeasuredWidth();
int viewHeight = childAt.getMeasuredHeight();
if (viewWidth > width - l) {
totalHeight += maxLineHeight + lineHeight;
l = 0;
maxLineHeight = orTextSize > spTextSize ? orTextSize * 4 : spTextSize * 4;
}
if (viewHeight > maxLineHeight) {
maxLineHeight = viewHeight;
t = (int) totalHeight;
b = (int) totalHeight + viewHeight;
} else {
t = (int) totalHeight + ((maxLineHeight - viewHeight) / 2);
b = (int) totalHeight + viewHeight + ((maxLineHeight - viewHeight) / 2);
}
childAt.layout(l, t, l + viewWidth, b);
if (width - l > 0) {
l += (viewWidth);
}
}
}
private String data;
private boolean isStart;
public void upData(String data) {
this.data = data;
}
/**
* creat view
*/
private void startCalc() {
if (null == data || data.length() == 0) return;
if (isStart) return;
String[] split = split(data);
for (int i = 0; i < split.length; i++) {
String str = split[i];
if (null != str && str.length() > 0) {
calc(str.contains("~"), str);
}
}
invalidate();
// requestLayout();
isStart = true;
}
/**
* 计算数据
*/
private void calc(boolean flag, String data) {
//特殊字体
if (flag) {
data = data.replace("~", "");
addSpecialText(data);
//普通字体
} else {
addOrdinaryText(data);
}
}
/**
* 拆分字体
*/
private String[] split(String data) {
data = data.replaceAll("/#", "&");
data = data.replaceAll("#/", "~&");
String[] split = data.split("&");
return split;
}
/**
* 添加普通字体
*
* @param data
*/
private void addOrdinaryText(String data) {
float v = ordinaryPaint.measureText(data);
//如果字符串宽度小于剩余宽度 直接创建一个textview
if (v <= lastWidth) {
lastWidth = lastWidth - v;
creatView(data);
//如果字符串宽度大于剩余宽度 需要拆分字符串
} else {
String str = getOrdinaryStr(data);
lastWidth = width;
creatView(str);
addOrdinaryText(data.substring(str.length(), data.length()));
}
}
/**
* 添加特殊字体
*
* @param data
*/
private void addSpecialText(String data) {
float v = specialPaint.measureText(data) + 20;
//如果字符串宽度小于剩余宽度 直接创建一个textview
if (v <= lastWidth) {
lastWidth = lastWidth - v;
creatSpView(data);
//如果字符串宽度大于剩余宽度 需要拆分字符串
} else {
String str = getSpecialStr(data);
lastWidth = width;
creatSpView(str);
addSpecialText(data.substring(str.length(), data.length()));
}
}
/**
* 获取剩余控件特殊字体可填入的内容
*
* @param data
* @return
*/
private String getSpecialStr(String data) {
String str = "";
char[] chars = data.toCharArray();
for (int i = 0; i < chars.length; i++) {
str += chars[i];
if (specialPaint.measureText(str) >= lastWidth) {
if (i <= chars.length) {
str = data.substring(0, i);
}
break;
}
}
return str;
}
/**
* 获取剩余控件特殊字体可填入的内容
*
* @param data
* @return
*/
private String getOrdinaryStr(String data) {
String str = "";
char[] chars = data.toCharArray();
for (int i = 0; i < chars.length; i++) {
str += chars[i];
if (ordinaryPaint.measureText(str) >= lastWidth) {
if (i <= chars.length) {
str = data.substring(0, i);
}
break;
}
}
return str;
}
/**
* 创建普通字体textview
*
* @param data
*/
private void creatView(String data) {
if (null == data || data.length() == 0) return;
int width = (int) ordinaryPaint.measureText(data);
ordinaryView = new TextView(this.context);
ordinaryView.setTextSize(orTextSize);
ordinaryView.setGravity(Gravity.CENTER);
ordinaryView.setTextColor(getResources().getColor(orTextColor));
ordinaryView.setWidth(width);
ordinaryView.setText(data);
addView(ordinaryView);
}
/**
* 创建特殊字体textview
*
* @param data
*/
private void creatSpView(String data) {
if (null == data || data.length() == 0) return;
int width = (int) specialPaint.measureText(data) + 18;
specialView = new TextView(this.context);
specialView.setTextSize(spTextSize);
specialView.setGravity(Gravity.CENTER);
specialView.setTextColor(getResources().getColor(spTextColor));
specialView.setBackgroundColor(getResources().getColor(R.color.black_d_color));
specialView.setWidth(width);
specialView.setHeight(maxLineHeight);
specialView.setText(data);
addView(specialView);
}
}
XML代码:
<com.hyperx.wlworktools.test.LabelLayout
android:id="@+id/group_view"
android:layout_width="240dp"
android:layout_height="match_parent"
</com.hyperx.wlworktools.test.LabelLayout>
Activity代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_cus_group);
final LabelLayout group_view = findViewById(R.id.group_view);
String data ="/#Python#/是一种/#入门快#/、/#功能强大#/、/#高效灵活#/的编程语言,学会之后无论是想进入/#数据分析#/、/#人工智能#/、/#网站开发#/、/#网络安全#/、/#集群运维#/这些领域,还是希望掌握第一门编程语言,都可以用 Python 来开启美好未来的无限可能!";
group_view.upData(data);
}
感觉有用的同学,动动小手指给个赞,码字不易。