在安卓开发中,我们经常会遇到给一个TextView中的部分文字设置不同的样式的情况,这之后我们不可能总是用好几个TextView来拼出这样一个效果,此时,我们就需要用到'''SpannableString'''以及和各种'''Span'''的用法了,下面就让我们看一下Android给我们提供了多少种Span,以及各自的用法:
SpannableString
首先来介绍SpannableString,官方解释是:这是内容不可变但可以附加和分离标记对象的文本的类。所谓的标记对象就是我们接下来要介绍的Span。对于他的用法:
SpannableString string = new SpannableString(str);
//设置TextView,可以被当做字符串设置给TextView
textView.setText(string);
各种Span
- BackgroundColorSpan:给部分文字设置背景颜色
- ForegroundColorSpan:给部分文字设置前景色
- ClickableSpan:设置点击事件
- URLSpan:设置链接,相当于Html的标签
- MaskFilterSpan:文字的装饰效果。分为两种:BlurMaskFilter(模糊效果) 和 EmbossMaskFilter (浮雕效果)
- AbsoluteSizeSpan:设置字体大小
- RelativeSizeSpan:设置字体的相对大小
- ImageSpan:设置图片
- ScaleXSpan:横向压缩
- SubscriptSpan:设置脚注
- SuperscriptSpan:上标,相当于数学中的平方样式
- TextAppearanceSpan:使用style来定义文本样式
- TypefaceSpan:设置字体
- RasterizerSpan:设置光栅字样
- StrikethroughSpan:删除线,相当于购物网站上的划掉的原价
- UnderlineSpan:下划线。
以上都是Android源码提供的效果,下面来大概写一下用法,其实用法基本上都是一样的:
String str = "大家好,下面来演示一下Span的用法";
SpannableString string = new SpannableString(str);
BackgroundColorSpan span = new BackgroundColorSpan(getResources().getColor(R.color.red));
//最后一句就是给String设置效果的
//第2个参数表示从哪个字符开始设置背景色
//第3个参数表示到哪个字符结束背景色的设置
//最后一个参数表示是否包含所设置的第二、三参数所代表的位置
//本例中设置的是前后都不包括
//相同的还有:
//Spanned.SPAN_INCLUSIVE_EXCLUSIVE(前面包括,后面不包括)、
//Spanned.SPAN_EXCLUSIVE_INCLUSIVE(前面不包括,后面包括)、
//Spanned.SPAN_INCLUSIVE_INCLUSIVE(前后都包括)
string.setSpan(span,0,3,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
其实看着android的文档,或者上网上搜这些东西会有很多,大家可以跟着敲一遍代码,运行一下就能清楚地知道每个Span所代表的含义了。下面进入主题:
公司最近要做一个练习的需求,就相当于学生在App中做试卷似得,于是就有了下面的这个设计图:
大概就是这种样式的图片,一段文字里面会嵌入很多的空格,并且还有背景色,还有文字,而且点击的时候还有各种效果,为了实现这个功能,确实看了很多Span的样式。
刚开始的时候准备用BackgroundColorSpan
+ClickableSpan
来实现这样的功能,后来发现,BackgroundColorSpan不能完美的实现这个功能,于是在网上找了BackgroundImageSpan来实现的。
public class BackgroundImageSpan extends ReplacementSpan implements ParcelableSpan {
private static final String TAG = "BackgroundImageSpan";
private Drawable mDrawable;
private int mImageId;
private int mWidth = -1;
/**
* new BackgroundImageSpan use resource id and Drawable
* @param id the drawable resource id
* @param drawable Drawable related to the id
* @internal
* @hide
*/
public BackgroundImageSpan(int id, Drawable drawable) {
mImageId = id;
mDrawable = drawable;
}
/**
* @hide
* @internal
*/
public BackgroundImageSpan(Parcel src) {
mImageId = src.readInt();
}
/**
* @hide
* @internal
*/
public void draw(Canvas canvas, int width,float x,int top, int y, int bottom, Paint paint) {
if (mDrawable == null) {//if no backgroundImage just don't do any draw
Log.e(TAG, "mDrawable is null draw()");
return;
}
Drawable drawable = mDrawable;
canvas.save();
canvas.translate(x, top); // translate to the left top point
mDrawable.setBounds(0, 0, width, (bottom - top));
drawable.draw(canvas);
canvas.restore();
}
@Override
public void updateDrawState(TextPaint tp) {
}
/**
* return a special type identifier for this span class
* @hide
* @internal
* @Override
*/
public int getSpanTypeId() {
return 0;
}
/**
* describe the kinds of special objects contained in this Parcelable's marshalled representation
* @hide
* @internal
* @Override
*/
public int describeContents() {
return 0;
}
/**
* flatten this object in to a Parcel
* @hide
* @internal
* @Override
*/
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mImageId);
}
/**
* @hide
* @internal
*/
public void convertToDrawable(Context context) {
if (mDrawable == null) {
mDrawable = context.getResources().getDrawable(mImageId);
}
}
/**
* convert a style text that contain BackgroundImageSpan, Parcek only pass resource id,
* after Parcel, we need to convert resource id to Drawable.
* @hide
* @internal
*/
public static void convert(CharSequence text , Context context) {
if (!(text instanceof SpannableStringBuilder)) {
return;
}
SpannableStringBuilder builder = (SpannableStringBuilder)text;
BackgroundImageSpan[] spans = builder.getSpans(0, text.length(), BackgroundImageSpan.class);
if (spans == null || spans.length == 0) {
return;
}
for (int i = 0; i < spans.length; i++) {
spans[i].convertToDrawable(context);
}
}
/**
* draw the span
* @hide
* @internal
* @Override
*/
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
// draw image
draw(canvas, mWidth,x,top, y, bottom, paint);
// draw text
// the paint is already updated
canvas.drawText(text,start,end, x,y, paint);
}
/**
* get size of the span
* @hide
* @internal
* @Override
*/
public int getSize(Paint paint, CharSequence text, int start, int end,
FontMetricsInt fm) {
float size = paint.measureText(text, start, end);
if (fm != null && paint != null) {
paint.getFontMetricsInt(fm);
}
mWidth = (int)size;
return mWidth;
}
}
以上这段代码是谷歌的Git上提供的,最后会放上链接,接着说一下我实现上图的过程,上代码比较快:
//前两行使用这则表达式来解析出一大段文本里面需要特殊设置的文本内容
Matcher textMatcher;
SpannableString str = new SpannableString(contentStr);
textMatcher = PATTERN_TEXT_SRC.matcher(content);
while (textMatcher.find()) {
//然后开始循环所有的文本来进行设置。
final String blank = textMatcher.group().trim();
int index = content.indexOf(blank);
ClickableSpan clickableSpan = new MyClickableSpan();
BackgroundImageSpan backgroundColorSpan;
//此处之所以有这个判断,完全是为了用来设置点击时候的样式,而currentStart和currentEnd也是点击的文字的第一字符的position和最后一个字符的position
if (currentStart == index && currentEnd == index + blank.length()) {
backgroundColorSpan = new BackgroundImageSpan(R.drawable.bg_answer_wrong, getResources().getDrawable(R.drawable.bg_answer_wrong));
} else {
backgroundColorSpan = new BackgroundImageSpan(R.drawable.bg_noanswer_unselected, getResources().getDrawable(R.drawable.bg_noanswer_unselected));
}
str.setSpan(backgroundColorSpan, index, index + blank.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
str.setSpan(clickableSpan, index, index + blank.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);}
//如果要设置点击事件,那么久一低昂要设置下面这一行,否则点击事件不起作用
mTextView.setMovementMethod(LinkMovementMethod.getInstance());
mTextView.setText(str);
//此处是定义的ClickableSpan
private class MyClickableSpan extends ClickableSpan {
@Override
public void onClick(View widget) {
//这里面的代码主要是用来获取所点击的那一部分的text,也可以根据下面的方法来获取具体点击的那部分文字内容
TextView tv = (TextView) widget;
Spanned s = (Spanned) tv.getText();
int start = s.getSpanStart(this);
int end = s.getSpanEnd(this);
currentStart = start;
currentEnd = end;
updateBlank();
Toast.makeText(MainActivity.this, tv.getText(), Toast.LENGTH_SHORT).show(); }}
基本上就是这些内容了,其实做起来不太难,主要是各种状态判断起来比较崩溃,下面一些就是收集个各种需要的效果:
给所设置的部分内容添加padding
BackgroundImageSpan
Clickable获取点击的文本内容
ClickableSpan的一些样式的设置
基本上就是以上这么多了,谢谢这些链接的文章作者所提供的优质内容,权当记录了吧。