TextView 局部文本样式设置之二:SpannableString

上次写了《TextView 局部文本样式设置》后,一直想整理一篇关于 SpannableString 的文章,但是一懒就忘记了,刚好最近项目中局部文字样式修改用得多,所以就趁空把文章整理出来,这是参考其他文章整理出来的,以便后续查看。
以下尺寸工具类采用《常用代码整理:尺寸工具类(SizeUtil)

1、常用样式
SpannableString spannableString1 = new SpannableString("这是自带骚气的文本,哈1哈2哈3");
// 设置字体大小,单位为像素
spannableString1.setSpan(new AbsoluteSizeSpan(20), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 设置字体大小,第二个参数表示前面的字体大小单位为是否为dip
spannableString1.setSpan(new AbsoluteSizeSpan(20, true), 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//设置字体大小,参数表示为默认字体大小的多少倍(如果开发者在xml中设置了android:textSize,就采用开发者设置的大小)
spannableString1.setSpan(new RelativeSizeSpan(2.5f), 5, 7, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 设置字体样式: NORMAL正常,BOLD粗体,ITALIC斜体,BOLD_ITALIC粗斜体
spannableString1.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 6, 7, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 设置字体前景色(字体颜色)
spannableString1.setSpan(new ForegroundColorSpan(Color.RED), 7, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 设置下划线
spannableString1.setSpan(new UnderlineSpan(), 9, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 设置删除线
spannableString1.setSpan(new StrikethroughSpan(), 10, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 设置上下标
spannableString1.setSpan(new SubscriptSpan(), 13, 14, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannableString1.setSpan(new SuperscriptSpan(), 15, 16, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mTvOne.setText(spannableString1);

2、图片替换
SpannableString spannableString2 = new SpannableString("再来看看骚气的图片替换");
Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
spannableString2.setSpan(new ImageSpan(drawable), 2, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mTvTwo.setText(spannableString2);

3、绘制背景
SpannableString spannableString3 = new SpannableString("这是骚气的指定文字背景绘制");
spannableString3.setSpan(new BackgroundSpan(this, 0xffff0000), 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mTvThree.setText(spannableString3);
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.text.style.ReplacementSpan;

public class BackgroundSpan extends ReplacementSpan {
    private Context mContext;
    private int mTextColor;
    private int mHeight, mTextSize, mRadius, mTextPadding, mMargin;
    private RectF mTempF = new RectF();

    public BackgroundSpan(Context context, int textColor) {
        super();
        this.mContext = context;
        this.mTextColor = textColor;
        this.mTextSize = SizeUtil.sp2px(mContext, 10);
        this.mRadius = SizeUtil.dip2px(mContext, 3);
        this.mHeight = SizeUtil.dip2px(mContext, 13);
        this.mTextPadding = SizeUtil.dip2px(mContext, 2.5f);
        this.mMargin = SizeUtil.dip2px(mContext, 5f);
    }

    /**
     * 设置宽度
     * 返回值就是Span替换文字后所占的宽度
     */
    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
        // 这里要设定文本的文字大小,不然系统会采用xml中开发者设置的android:textSize(开发者没设置就是textview的默认textSize)
        // 且要注意getSize方法快于draw方法被调用
        paint.setTextSize(mTextSize);
        return ((int) paint.measureText(text, start, end)) + mTextPadding * 2 + SizeUtil.dip2px(mContext, 3);
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        paint.setAntiAlias(true); // 抗锯齿
        paint.setDither(true); // 防抖动

        // 设置画笔,绘制背景
        // paint.setColor(mTextColor);
        // paint.setStyle(Paint.Style.STROKE);
        paint.setColor(0xff00ff00);
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        /*
        Paint.Style.FILL:填充内部
        Paint.Style.FILL_AND_STROKE  :填充内部和描边
        Paint.Style.STROKE  :描边
        */
        mTempF.set(x, top + mMargin, x + ((int) paint.measureText(text, start, end)) + mTextPadding * 2, top + mMargin + mHeight);
        canvas.drawRoundRect(mTempF, mRadius, mRadius, paint);
        /*
        public void drawRoundRect (RectF rect, float rx, float ry, Paint paint)
        rect:RectF对象,set(float left, float top, float right, float bottom)
        rx:x方向上的圆角半径
        ry:y方向上的圆角半径
        paint:绘制时所用画笔
        */

        // 设置画笔,绘制文字
        paint.setColor(mTextColor);
        paint.setTextSize(mTextSize);
        float textY = top + mMargin + mHeight / 2 - (paint.descent() + paint.ascent()) / 2;
        canvas.drawText(text, start, end, x + mTextPadding, textY, paint);
        /*
        drawText(CharSequence text, int start, int end, float x, float y, Paint paint)
        x:绘制文本的起始x坐标
        y:绘制文本的起始y坐标
        */
    }
}

4、可点击的文字:ClickableSpan
spannableString.setSpan(new XxxClick(), 0, 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    private class XxxClick extends ClickableSpan {

        @Override
        public void updateDrawState(@NonNull TextPaint ds) {
            super.updateDrawState(ds);
            // 不要下划线
            ds.setUnderlineText(false);
            ds.setColor(Color.parseColor("#ff0000"));
        }

        @Override
        public void onClick(@NonNull View widget) {
            // 点击操作
        }
    }
5、TextView 文字绘制的特殊说明

TextView 设置的 SpannableString 其实是通过 paint 在指定的位置画出想要达到的效果,其中指定位置的背景及文字都是通过 paint 画出来的,那么,绘制位置的确定就极为重要。

Android 系统默认的坐标起点是屏幕的左上角,x 轴向右为正,y 轴向下为正,但是,TextView 中对文字的绘制,是依据 TextView 的 baseline 来绘制的:

( 图片来源:https://www.jianshu.com/p/91b119f76c80 )

  • top:指的是最高字符到baseline的值,即ascent的最大值,为负数
  • ascent:是baseline之上至字符最高处的距离,为负数
  • baseline:基准点,字符在TextView中的基准点,字符的绘制就是通过这个基准点来绘制的,相当于字符的零点,top,bottom,ascent,descent的值就是以这个为零点来得到的,在baseline上面的top和ascent是负数,在baseline下面的bottom和descent是正数
  • descent:是baseline之下至字符最低处的距离,为正数
  • bottom:是指最低字符到baseline的值,即descent的最大值,为正数

    ( 图片来源:http://www.it610.com/article/5176148.htm )

通过 extends ReplacementSpan 方式来实现想要的结果,主要是 ReplacementSpan 中的两个方法:getSize 和 draw ,其中 getSize 确定的是 Span 替换文字后所占的宽度,而 draw 方法是绘制样式的主体,以下是 draw 方法的源码及其参数说明:

/**
* Draws the span into the canvas.
*
* @param canvas Canvas into which the span should be rendered.
* @param text Current text.
* @param start Start character index for span.
* @param end End character index for span.
* @param x Edge of the replacement closest to the leading margin.
* @param top Top of the line.
* @param y Baseline.
* @param bottom Bottom of the line.
* @param paint Paint instance.
*/
public abstract void draw(@NonNull Canvas canvas, 
		CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, 
		float x, int top, int y, int bottom, @NonNull Paint paint);
  • text Current text.(指定位置的文字)
  • start Start character index for span.(指定的起始位置)
  • end End character index for span.(指定的结束位置)
  • x Edge of the replacement closest to the leading margin.(不知道是啥)
  • top Top of the line.(文字的 top 线)
  • y Baseline.(文字的 baseline 线)
  • bottom Bottom of the line.(文字的 bottom 线)其中 bottom 线受 TextView 的 android:lineSpacingExtra 属性影响,即文字行间距

通过 debug 查看数值:

下面贴个例子(不要问我品如是谁):

<TextView
        android:id="@+id/tv_two"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:lineSpacingExtra="5dp"
        android:textSize="18sp" />
SpannableString spannableString = new SpannableString("2你好骚啊你好骚啊你好骚啊你好骚啊你好骚啊你好骚啊");
spannableString.setSpan(new BackgroundSpan(this, 0xffffffff, 0xffff0000), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mTvTwo.setText(spannableString);
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.style.ReplacementSpan;

public class BackgroundSpan extends ReplacementSpan {
    private Context mContext;
    private int mTextColor, mBgColor;
    private int mRadius; // 圆半径

    public BackgroundSpan(Context context, int textColor, int bgColor) {
        super();
        this.mContext = context;
        this.mTextColor = textColor;
        this.mBgColor = bgColor;
        this.mRadius = SizeUtil.dip2px(mContext, 8);
    }

    /**
     * 设置宽度
     * 返回值就是Span替换文字后所占的宽度
     */
    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
        return mRadius * 2 + SizeUtil.dip2px(mContext, 5);
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        paint.setAntiAlias(true); // 抗锯齿
        paint.setDither(true); // 防抖动

        // 设置画笔,绘制背景
        paint.setColor(mBgColor);
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        // 以 text 的 baseline 线为 y 轴起点计算圆点 y 轴
        canvas.drawCircle(mRadius, y - ((paint.descent() - paint.ascent()) / 2 - paint.descent()), mRadius, paint);

        // 设置画笔,绘制文字
        paint.setColor(mTextColor);
        // 画 text 是以 baseline 线为 paint 的 y 轴起点,从下往上画
        canvas.drawText(text, start, end, mRadius - paint.measureText(text, start, end) / 2, y, paint);
    }
}

其中最主要的是,对背景及文字的绘制位置的计算:

// 设置画笔,绘制背景
paint.setColor(mBgColor);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
// 以 text 的 baseline 线为 y 轴起点计算圆点 y 轴
canvas.drawCircle(mRadius, y - ((paint.descent() - paint.ascent()) / 2 - paint.descent()), mRadius, paint);

// 设置画笔,绘制文字
paint.setColor(mTextColor);
// 画 text 是以 baseline 线为 paint 的 y 轴起点,从下往上画
canvas.drawText(text, start, end, mRadius - paint.measureText(text, start, end) / 2, y, paint);

其中,圆形背景的圆点 x 轴为半径大小,圆点 y 轴等于文字基线减去文字高度的一半,绘制出来的圆形背景的圆点,就会在可见文字高度的中间线上。

drawCircle(float cx, float cy, float radius, @NonNull Paint paint)

因为本例子中文字大小不变,则文字绘制的 y 轴就是文字的基线,即 draw 方法中的参数 y;而文字想要绘制在圆形的中央,那么文字 x 轴起始点的位置,就在圆形背景圆点的位置减去待绘制文字总宽度的一半。

drawText(@NonNull CharSequence text, int start, int end, float x, float y,
            @NonNull Paint paint)

参考文章:
1、https://blog.csdn.net/freak_csh/article/details/79276945
2、https://blog.csdn.net/u012735483/article/details/52902047
3、https://blog.csdn.net/lixpjita39/article/details/78879336
4、https://www.jianshu.com/p/91b119f76c80
5、http://www.it610.com/article/5176148.htm

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值