SpannableStringBuilder 其实可以这么用

如下图所示,在App开发过程中很多时候需要对文字做类似如下处理:


WX20170729-221047@2x.png

给文字加颜色,加背景,加下划线,可点击,加中划线,字体大小调整,甚至还有中间插入一个小icon之类的。
好在Android SDK对这此功能是支持的,通过SpannableStringBuilderParcelableSpan的众多扩展子类完成,下面是Android默认实现方式:

String str = "红字, 绿背景,下划线|灰色字,可点击,中划线,大号字";
SpannableStringBuilder builder = new SpannableStringBuilder(str);
//设置颜色
builder.setSpan(new ForegroundColorSpan(Color.RED), 0, 2,
        // setSpan时需要指定的 flag,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE(前后都不包括).
        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//超链接
builder.setSpan(new BackgroundColorSpan(Color.GREEN), 2, 8, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

// 下划线|灰色字
builder.setSpan(new UnderlineSpan(), 8, 16, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new BackgroundColorSpan(Color.GRAY), 8, 16, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

//删除线
builder.setSpan(new ClickableSpan() {
    @Override
    public void onClick(View widget) {
        Toast.makeText(MainActivity.this, "hello", Toast.LENGTH_SHORT).show();
    }
}, 16, 20, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setMovementMethod(LinkMovementMethod.getInstance());

// 中划线
builder.setSpan(new StrikethroughSpan(), 20, 24, Spannable.SPAN_INCLUSIVE_INCLUSIVE);

// 大号字
builder.setSpan(new AbsoluteSizeSpan(80), 24, 27, Spannable.SPAN_INCLUSIVE_INCLUSIVE);

//将文字赋予TextView
tv.setText(builder);

我想第一次使用此API可能会有些犯晕,毕竟其中太多需要计算的span起始和结束的index,虽然算不上非常复杂,但是也挺烦人。其实,完全可以借鉴JDK中对StringBuilder API的设计进行对其改造。
首先,先看看改造后的使用,看如何实现上图里的效果:

TextView textView = (TextView) findViewById(R.id.textView);
textView.setMovementMethod(LinkMovementMethod.getInstance());

StyleStringBuilder builder = new StyleStringBuilder();
builder.append(new StyleString("红字, ").setTextColor(ContextCompat.getColor(this, R.color.red)));
builder.append(new StyleString("绿背景, ").setBackgroundColor(ContextCompat.getColor(this, R.color.green)));
builder.append(new StyleString("下划线|灰色字, ").setUnderline().setTextColor(ContextCompat.getColor(this, R.color.grey)));
builder.append(new StyleString("可点击").setClickable(new ClickableSpan() {
    @Override
    public void onClick(View widget) {
        Toast.makeText(MainActivity.this, "clicked", Toast.LENGTH_SHORT).show();
    }
}));
builder.append(new StyleString("中划线").setStrikeThrough());
builder.append(new StyleString("大号字").setTextSize(getResources().getDimensionPixelOffset(R.dimen.text_size_large)));

textView.setText(builder.build());

我相信大家对这种风格的api是相对很熟悉的,可读性也尚可,来看看如何屏蔽那些琐碎的index的:

  1. StyleString:
public class StyleString {
        private final String mText;
        private final SpannableStringBuilder mBuilder;

        public StyleString(String text) {
                if (text == null) {
                        text = "";
                }
                mBuilder = new SpannableStringBuilder();
                mBuilder.append(mText = text);
        }

        public StyleString setTextColor(@ColorInt int color) {
                mBuilder.setSpan(new ForegroundColorSpan(color),
                                0, mText.length(),
                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                return this;
        }

        /**
         * @deprecated please use {@link #setTextColor(int)} instead
         */
        public StyleString setForegroundColor(int color) {
                return setTextColor(color);
        }

        public StyleString setBackgroundColor(@ColorInt int color) {
                mBuilder.setSpan(new BackgroundColorSpan(color), 0, mText.length(),
                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                return this;
        }

        /**
         * @deprecated please use {@link #setTextSize(int)} instead
         */
        public StyleString setFontSizePX(int sizeInPx) {
                return setTextSize(sizeInPx);
        }

        public StyleString setTextSize(int sizeInPx) {
                mBuilder.setSpan(new AbsoluteSizeSpan(sizeInPx), 0, mText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                return this;
        }

        /**
         * About typeFace see {@link Typeface}
         */
        public StyleString setFontStyle(int typeFace) {
                mBuilder.setSpan(new StyleSpan(typeFace), 0, mText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                return this;
        }

        public StyleString setUnderline() {
                mBuilder.setSpan(new UnderlineSpan(), 0, mText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                return this;
        }

        /**
         * If StyleString was used as content of TextView or EditText,
         * setMovementMethod should be called like below:
         * 
         * <pre>
         * TextView textView = new TextView(context);
         * textView.setMovementMethod(LinkMovementMethod.getInstance());
         * </pre>
         */
        public StyleString setClickable(ClickableSpan clickable) {
                mBuilder.setSpan(clickable, 0, mText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                return this;
        }

        public StyleString setUri(String uri) {
                mBuilder.setSpan(new URLSpan(uri), 0, mText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                return this;
        }

        public StyleString setStrikeThrough() {
                mBuilder.setSpan(new StrikethroughSpan(), 0, mText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                return this;
        }

        public SpannableStringBuilder toStyleString() {
                return mBuilder;
        }
}
  1. StyleStringBuilder:
public class StyleStringBuilder {
        private final SpannableStringBuilder builder = new SpannableStringBuilder();

        public StyleStringBuilder() {
        }

        public StyleStringBuilder(CharSequence text) {
                builder.append(text);
        }

        public StyleStringBuilder(StyleString styleString) {
                builder.append(styleString.toStyleString());
        }

        public StyleStringBuilder append(CharSequence text) {
                builder.append(text);
                return this;
        }

        public StyleStringBuilder append(StyleString styleString) {
                builder.append(styleString.toStyleString());
                return this;
        }

        public Spanned build() {
                return builder;
        }

        public static CharSequence formatString(Context context, String text, String regular, int colorResId) {

                try {
                        SpannableStringBuilder builder = new SpannableStringBuilder(text);

                        Pattern p = Pattern.compile(regular);
                        Matcher matcher = p.matcher(text);
                        int start = 0;
                        int end = 0;
                        while (matcher.find()) {
                                if (matcher.start() == end) {
                                        end = matcher.end();
                                } else {
                                        if (start != end) {
                                                ForegroundColorSpan span = new ForegroundColorSpan(ContextCompat.getColor(context, colorResId));
                                                builder.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                                        }
                                        start = matcher.start();
                                        end = matcher.end();
                                }
                        }
                        if (start != end) {
                                ForegroundColorSpan span = new ForegroundColorSpan(ContextCompat.getColor(context, colorResId));
                                builder.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                        }
                        return builder;
                } catch (Exception e) {
                        return text;
                }
        }
}

至于为什么没有对图片等其他特性支持是因为一时半会没有加那些span的支持,自己参考着扩展就好,因为这里已经没有什么秘密了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容