最近产品汪和运营商讨下来决定要做商品促销活动,然后设计妹子给到最终的效果图。
最后效果图
第一感觉就是 so so easy 嘛,加个标签,费不了什么事儿。第一印象记得 Spanable
可以更改对应文字的颜色和背景,设置设置点击事件。
接着,发现了一个问题,上面说到的 Spanable
只能实现全色的背景,不能实现这种边框的背景。看来这种方案是行不通的。
第一感觉不奏效,那么就要分析下这种效果,我想到以下两种方案。
第一种方案就是是否可以直接给 TextView
设置指定的留白呢?就是前面的标签是一个控件,TextView
留白便签控件宽度+margin值。这个方案需要解决的问题是,这里是否有相关的 Api 可以直接设置每行留白的距离,另外首行标签和文字居中对齐问题,毕竟设计师都是像素眼,没有按要求对齐,行距不对都可能无法验收。
第二种方案就是取巧,将 title 的 TextView
拆分为两个 TextView
,第一行直接就是线性水平布局,第二行再是一个独立的TextView
。这里需要解决的问题是,我怎么获取 TextView
第一行显示的文字,然后截取剩余的文字单独显示在第二行。这种方法实现似乎没有第一种优雅,但是可以轻松避开第一行标签和 title 文字居中对齐的问题。
在否定一种方案和提出新的两种方案后,可以看看后两种方案到底可以怎么实现。
第一种方案:
这里需要使用到 SpannableString
这个类,接着就是主角 LeadingMarginSpan
这个类。
A paragraph style affecting the leading margin. There can be multiple leading margin spans on a single paragraph; they will be rendered in order, each adding its margin to the ones before it. The leading margin is on the right for lines in a right-to-left paragraph. LeadingMarginSpans should be attached from the first character to the last character of a single paragraph.
一句话,它可以给 TextView
每行设置指定的头间距,找到相关 API 之后,接着计算出标签的整体宽度。
LeadingMarginSpan.Standard what = new LeadingMarginSpan.Standard(width, 0);
spannableString.setSpan(what, 0, spannableString.length(), SpannableString.SPAN_INCLUSIVE_INCLUSIVE);
LeadingMarginSpan
是接口,内部的 Standard
看名字就知道是它的标准实现,它有两个构造方法,Standard(int every)
和 Standard(int first, int rest)
,这个就是指定 TextView
每行的缩进值的,一个参数的就是给每一行都设置同样的值,最后当然就是调用两个参数的方法,两个参数的就是指定第一行和其他行的缩进值。
接着看下 SpannableString
的 setSpan()
的方法,这里需要设置四个参数,第一个就是我们创建出来的 LeadingMarginSpan
,第二个和第三个其实就是第一个对象的作用范围,第四个参数控制范围的边界包含情况。我们这里不是给具体第几个到第几个的字设置属性,所以后面的 start、end 以及边界限制随便写都会生效的。
对于第四个参数,就是对上下边界是否包含自己的限定,这里你只需要认识这两个单词就好,「EXCLUSIVE」 就是不包含,「INCLUSIVE 」就是包含。所以这里就有四种情况,当然这个不是这次的重点。
第二种方案:
这里需要使用到 Layout
个类, TextView
使用它管理文字显示。
A base class that manages text layout in visual elements on the screen. For text that will be edited, use a {@link DynamicLayout}, which will be updated as the text changes. For text that will not change, use a {@link StaticLayout}.
通过这个 Layout
,我们就可以获取到 TextView
每行的内容,然后就可以决定第二行是否显示及其内容。
Layout layout = first.getLayout();
int lineEnd = layout.getLineEnd(0);
上面的 lineEnd 就是第一行文字显示的数量,拿到之后,就可以判断下,如果和总长度相等,那就说明第一行就可以显示完全,第二行根据具体情况,控制显示与否。如果小于总长度,那么久截取出剩余文字,用于第二行 TextView
显示。
到这里,两种方案实现完毕,接着再聊一个问题,那就是测量时机,这里的需求总是出现在列表页面,这就涉及到一个计算时机问题,这里我的解决方案是添加一个 addOnPreDrawListener
的方式,这个方法是每次绘制之前都会调用,比较符合列表的刷新。
最终效果:
方案一(左边)方案二(右边)
方案一(左边)方案二(右边)
贴下详细的代码:
//方案一:将文字查分为两个两个TextView 显示
public static void calculateTag1(TextView first, TextView second, final String text) {
ViewTreeObserver observer = first.getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
Layout layout = first.getLayout();
int lineEnd = layout.getLineEnd(0);
String substring = text.substring(0, lineEnd);
String substring1 = text.substring(lineEnd, text.length());
Log.i("TAG", "onGlobalLayout:"+ "+end:" + lineEnd);
Log.i("TAG", "onGlobalLayout: 第一行的内容::" + substring);
Log.i("TAG", "onGlobalLayout: 第二行的内容::" + substring1);
if (TextUtils.isEmpty(substring1)) {
second.setVisibility(View.GONE);
second.setText(null);
} else {
second.setVisibility(View.VISIBLE);
second.setText(substring1);
}
first.getViewTreeObserver().removeOnPreDrawListener(
this);
return false;
}
});
}
//方案二:动态设置缩进距离的方式
public static void calculateTag2(TextView tag, TextView title, final String text) {
ViewTreeObserver observer = tag.getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
SpannableString spannableString = new SpannableString(text);
//这里没有获取margin的值,而是直接写死的
LeadingMarginSpan.Standard what = new LeadingMarginSpan.Standard(tag.getWidth() + dip2px(tag.getContext(), 10), 0);
spannableString.setSpan(what, 0, spannableString.length(), SpannableString.SPAN_INCLUSIVE_INCLUSIVE);
title.setText(spannableString);
tag.getViewTreeObserver().removeOnPreDrawListener(
this);
return false;
}
});
}
public static int dip2px(Context context, double dpValue) {
float density = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * density + 0.5);
}
PS:SpannableStringBuilder
阔以用于快速给 TextView
设置Span,最后看了下某东的效果,它的标签不是一个独立的控件,看样子或许是使用的 ImageSpan
来实现。但是 ImageSpan
默认不是居中对齐,解决方案可以看看。