SpannableString 和 SpannableStringBuilder的使用

介绍

谷歌API这样描述 SpannableString 与 SpannableStringBuilder:

This is the class for text whose content and markup can both be changed.
(这是一个内容和标记都可以更改的文本类)

1.SpannableString与SpannableStringBuilder介绍

SpannableStringBuilder,SpannableString其实和String一样,都是用来存储字符串,并且多继承自 CharSequence,所以最终多可以通过 setText(CharSequence text)方法,将字符设置给TextView。

作用:

将原有的 String 字符串,通过使用其方法setSpan( )方法实现字符串各种形式风格的显示。比如在 加下划线、加背景色、改变字体颜色、用图片把指定的文字给替换掉…,这也是与 String 不同的地方。

区别:

SpannableString 与 SpannableStringBuilder区别,相信大家首先会想到String 与 StringBuilder的区别。其实类似的,如果传入一个字符串,前者必须一次传入,但后者可以通过append()方法动态拼接多个SpannableString 。(后面例子会展示)

本文主要通过一个TextView,实现如下功能,并实现局部点击。

图片描述

使用

官方API 点这里 SpannableStringBuilder
更详细的API使用,可以自行到官方API查看,使用比较简单。这里,主要介绍setSpan( ) 的使用,也是最重要的。

setSpan(Object what, int start, int end, int flags)

在指定的文本范围内,使用指定的 What 类型

参数:
start,end:字符的起始位置,和结束位置,形式是 : [ start , end )
what:是一个CharacterStyle对象,一般常用的有以下几种:

参数 what 说明
ForegroundColorSpan 文本颜色
BackgroundColorSpan 文本背景色
ClickableSpan 点击事件
MaskFilterSpan 修饰效果,如模糊(BlurMaskFilter)浮雕
RasterizerSpan 光栅效果
StrikethroughSpan 删除线
SuggestionSpan 相当于占位符
UnderlineSpan 下划线
AbsoluteSizeSpan 文本字体(绝对大小)
DynamicDrawableSpan 设置图片,基于文本基线或底部对齐。
ImageSpan 图片
RelativeSizeSpan 相对大小(文本字体)
ScaleXSpan 基于x轴缩放
StyleSpan 字体样式:粗体、斜体等
SubscriptSpan 下标(数学公式会用到)
SuperscriptSpan 上标(数学公式会用到)
TextAppearanceSpan 文本外貌(包括字体、大小、样式和颜色)
TypefaceSpan 文本字体
URLSpan 文本超链接

flags:作用域标识

参数 flags 说明
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 前后都不包括(在标志位 [start,end)前后添加文字,新添加的文字不会有任何设置的属性)
Spannable.SPAN_EXCLUSIVE_INCLUSIVE 前面不包括,后面包括。(在标志位 [start,end)前添加文字,新添加的文字不会有任何设置的属性,后边的添加的文字会带有设置的what属性)
Spannable.SPAN_INCLUSIVE_EXCLUSIVE 前面包括,后面不包括。(在标志位 [start,end)后添加文字,新添加的文字不会有任何设置的属性,前边边的添加的文字会带有设置的what属性)
Spannable.SPAN_INCLUSIVE_INCLUSIVE 前后都包括。前后都不包括(在标志位 [start,end)前后添加文字,新添加的文字会有设置的属性)

知道这些参数之后,接下来就简单了,大家可以逐一测试下。
下面,围绕效果图,开始实现。

1.添加字体颜色

一般在评论中,需要将用户名的文字使用不同颜色,如下:


		SpannableString spannableString;
		mSpannableStringBuilder = new SpannableStringBuilder();
        spannableString = new SpannableString("小哥哥");
        spannableString.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorPrimary)),0, spannableString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        mSpannableStringBuilder.append(spannableString);

        mSpannableStringBuilder.append("回复");

        spannableString = new SpannableString("小姐姐 : ");
        spannableString.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorPrimary)),0, spannableString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        mSpannableStringBuilder.append(spannableString);

        mSpannableStringBuilder.append("在我心中,你最美~");
        mContentTv.setText(mSpannableStringBuilder);

图片描述

2. 添加点击事件

首先,添加点击事件,关联东西会稍微复杂一点,这里一步一步介绍。

在评论的中,点击用户名或者回复的信息,应该做不同的处理。首先,给用户名添加点击事件。


		final SpannableString spannableString;
        mSpannableStringBuilder = new SpannableStringBuilder();
        spannableString = new SpannableString("小哥哥");
        /**
         * 添加监听事件
         */
        spannableString.setSpan(new ClickableSpan() {
            @Override
            public void onClick(View widget) {
                Toast.makeText(getApplicationContext(), spannableString.toString(),Toast.LENGTH_SHORT).show();
            }
        },0, spannableString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );
        mSpannableStringBuilder.append(spannableString);

        mSpannableStringBuilder.append("回复");


        final SpannableString spannableString1;
        spannableString1 = new SpannableString("小姐姐: ");
        /**
         *  添加监听事件
         */
        spannableString1.setSpan(new ClickableSpan() {
            @Override
            public void onClick(View widget) {
                Toast.makeText(getApplicationContext(), spannableString1.toString(),Toast.LENGTH_SHORT).show();
            }
        },0, spannableString1.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );
        mSpannableStringBuilder.append(spannableString1);

        mSpannableStringBuilder.append("在我心中,你最美~");
        mContentTv.setText(mSpannableStringBuilder);

然后,运行代码之后,发现不能点击。因为,还需要添加这一行,其作用就是链接响应,简单来说在xml设置:android:autoLink 是一样的。

mContentTv.setMovementMethod(LinkMovementMethod.getInstance());

运行之后就可以了,点击:

SpannableString 和 SpannableStringBuilder的使用_第1张图片

这里有 4个问题 需要解决:

  1. 字体默认有颜色;
  2. 字体默认有下划线
  3. 点击之后,字体背影一直不消失
  4. 点击span部分响应时,同时,整个textView的onClick也响应

1、2 问题 解决:

方法可能会有多种,比如添加点击事件之后,再改变字体颜色。这里说下正常简单的操作是:自定义 ClickableSpan


	public abstract class SpannableClickable extends ClickableSpan implements View.OnClickListener {
		/**
		 * text颜色
		 */
		private int textColor;
		public SpannableClickable(Context context) {
			this.textColor = context.getResources().getColor(R.color.colorPrimary);
		}
		public SpannableClickable(int textColor) {
			this.textColor = textColor;
		}
		
		/**
		 * 重写该方法
		 * 1.设置字体颜色,在构造函数可以自定义 传入
		 * 2.取消下划线
		 * 3.清除背景图层
		 */
		@Override
		public void updateDrawState(TextPaint ds) {
			super.updateDrawState(ds);
			ds.setColor(textColor);
			ds.setUnderlineText(false);
	//		ds.bgColor = bgColor;
			ds.clearShadowLayer();
		}
	}

然后设置这个自定义的监听即可:

	spannableString.setSpan(new SpannableClickable(getApplicationContext()) {
	            @Override
	            public void onClick(View widget) {
	                Toast.makeText(getApplicationContext(), spannableString.toString(),Toast.LENGTH_SHORT).show();
	            }
	        },0, spannableString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

3 ,4 问题解决: :自定义 MovementMethod

首先说下2点需求:

1.改变监听, 一般需求是:点击span部分的时候,整体textView不再响应。
2.改变背景 , 一般需求是,点击的时候,显示背景色,按键起来的时候,恢复原来的背景(透明)。

改变监听,这个会比较好处理,重新修改textView 的onTouch方法就可以。
但是,再修改的过程,发现 在该方法中对span文本的背景色设置没有效果,所以放弃这种。

最后,使用自定义的 MovementMethod,如下:

	public class MovementMethod extends BaseMovementMethod {
		public final String			TAG			= MovementMethod.class.getSimpleName();
		/**
		 * 整个textView的背景色
		 */
		private int					textViewBgColor;
		/**
		 * 点击部分文字时部分文字的背景色
		 */
		private int					clickableSpanBgClor;
		private BackgroundColorSpan	mBgSpan;
		private ClickableSpan[]		mClickLinks;
		/**
		 * true:响应textview的点击事件, false:响应设置的clickableSpan事件
		 */
		private boolean				isPassToTv	= true;
		public boolean isPassToTv() {
			return isPassToTv;
		}
		private void setPassToTv(boolean isPassToTv) {
			this.isPassToTv = isPassToTv;
		}
		
		/**
		 * @param clickableSpanBgClor   点击选中部分时的背景色
		 * @param textViewBgColor       整个textView点击时的背景色
		 */
		public MovementMethod(int clickableSpanBgClor, int textViewBgColor) {
			this.textViewBgColor = textViewBgColor;
			this.clickableSpanBgClor = clickableSpanBgClor;
		}
		
		@Override
		public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
	
			/**
			 * 这些代码 直接从LinkMovementMethod 拿过来就可以了
			 */
			int action = event.getAction();
			if (action == MotionEvent.ACTION_DOWN) {
				int x = (int) event.getX();
				int y = (int) event.getY();
				x -= widget.getTotalPaddingLeft();
				y -= widget.getTotalPaddingTop();
				x += widget.getScrollX();
				y += widget.getScrollY();
				
				Layout layout = widget.getLayout();
				int line = layout.getLineForVertical(y);
				int off = layout.getOffsetForHorizontal(line, x);
				
				mClickLinks = buffer.getSpans(off, off, ClickableSpan.class);
				if (mClickLinks.length > 0) {
					// 点击的是Span区域,不要把点击事件传递
					setPassToTv(false);
					Selection.setSelection(buffer, buffer.getSpanStart(mClickLinks[0]), buffer.getSpanEnd(mClickLinks[0]));
					// 设置点击区域的背景色
					mBgSpan = new BackgroundColorSpan(clickableSpanBgClor);
					buffer.setSpan(mBgSpan, buffer.getSpanStart(mClickLinks[0]), buffer.getSpanEnd(mClickLinks[0]), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
				} else {
					setPassToTv(true);
					// textview选中效果
					widget.setBackgroundColor(textViewBgColor);
				}
				
			} else if (action == MotionEvent.ACTION_UP) {
				if (mClickLinks.length > 0) {
					mClickLinks[0].onClick(widget);
					if (mBgSpan != null) {// 移除点击时设置的背景span
						buffer.removeSpan(mBgSpan);
					}
				} else {
					
				}
				Selection.removeSelection(buffer);
				widget.setBackgroundResource(R.color.transparent);
			} else if (action == MotionEvent.ACTION_MOVE) {
				// 这种情况不用做处理
			} else {
				if (mBgSpan != null) {// 移除点击时设置的背景span
					buffer.removeSpan(mBgSpan);
				}
				widget.setBackgroundResource(R.color.transparent);
			}
			return super.onTouchEvent(widget, buffer, event);
		}
	}

最后:

	mContentTv.setMovementMethod(new MovementMethod(getResources().getColor(R.color.gray), getResources().getColor(R.color.gray)));
SpannableString 和 SpannableStringBuilder的使用_第2张图片

最后,问题多解决了,哈哈哈。。。。

问题1.每个CharacterStyle对象只能应用于一个Spanned

这说的CharacterStyle对象,就是前面说的What对象。
在使用的时候,可能会有这种情况:一个CharacterStyle对象被使用多次,如下:

        String content = "小姐姐回复小哥哥:在我心中,你最帅~";
        mSpannableStringBuilder = new SpannableStringBuilder(content);
        ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.colorPrimary));
        mSpannableStringBuilder.setSpan(foregroundColorSpan,0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        mSpannableStringBuilder.setSpan(foregroundColorSpan,5, 8, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

本来是想2个用户名,多想改变成蓝色,但最终结果是:

图片描述

只有最后一次使用的时候才有效果。如何解决,这里提供2种:
1.那就是再重新 new CharacterStyle对象;(废话!!!)
2.使用CharacterStyle类的静态方法wrap方法:

官方这样说:

public static CharacterStyle wrap (CharacterStyle cs)

A given CharacterStyle can only applied to a single region of a given Spanned. If you need to attach the same CharacterStyle to multiple regions, you can use this method to wrap it with a new object that will have the same effect but be a distinct object so that it can also be attached without conflict.
给定的字符样式只能应用于给定跨距的单个区域。如果需要将相同的CharacterStyle附加到多个区域,可以使用此方法用一个新对象对其进行包装,该对象具有相同的效果,但是是不同的对象,因此也可以不冲突地附加它。

        String content = "小姐姐回复小哥哥:在我心中,你最帅~";
        mSpannableStringBuilder = new SpannableStringBuilder(content);
        ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.colorPrimary));
        mSpannableStringBuilder.setSpan(CharacterStyle.wrap(foregroundColorSpan),0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        mSpannableStringBuilder.setSpan(CharacterStyle.wrap(foregroundColorSpan),5, 8, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        mContent1Tv.setText(mSpannableStringBuilder);
图片描述

总结

SpannableStringBuilder 可以说是对TextView高级的用法,平时没怎么用,也不知道有这功能。目前项目有评论,于是就了解一下。
后面会写一个项目的实际用法,希望可以尽量完成。

如果议论,欢迎指出,谢谢!看完,麻烦点个赞呀!


欢迎点赞

参考:

https://developer.android.google.cn/reference/android/text/SpannableString

https://blog.csdn.net/baidu_31956557/article/details/78339071

你可能感兴趣的:(android)