×

Android TextView 缩进指定距离

96
lovejjfg
2017.09.24 21:20* 字数 1369

最近产品汪和运营商讨下来决定要做商品促销活动,然后设计妹子给到最终的效果图。


最后效果图

第一感觉就是 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 每行的缩进值的,一个参数的就是给每一行都设置同样的值,最后当然就是调用两个参数的方法,两个参数的就是指定第一行和其他行的缩进值。

接着看下 SpannableStringsetSpan() 的方法,这里需要设置四个参数,第一个就是我们创建出来的 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 默认不是居中对齐,解决方案可以看看

强势刷一波存在,示例相关代码地址,欢迎点赞,对了,觉得我搞得复杂或者有更简单的实现请留言一起讨论下咯。😉😉

朝花夕拾
Web note ad 1