Android 字体描边设置,支持HTML

3,868 阅读3分钟

一、效果及源代码

参考了网上几种实现方式,采用了在原有TextView的基础上增加一个新的TextView的方式,同时将新的TextView的paint设置为描边模式,支持Html、Span等内容。 文本效果如下

image.png

使用setStroke设置描边的宽度和颜色,必须使用setText2设置TextView的颜色,完整代码如下。

public class StrokeTextView extends AppCompatTextView {

    private TextView borderText;///用于描边的TextView
    TextPaint tp1; //borderText的Paint

    private int mStrokeColor = Color.BLACK;
    private int strokeWidth = 2;

    public StrokeTextView(Context context){
        super(context);
        borderText = new TextView(context);
        borderText.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
        borderText.setTextColor(this.mStrokeColor);  //设置描边颜色
        borderText.setGravity(getGravity());
        tp1 = borderText.getPaint();
        tp1.setStrokeWidth(this.strokeWidth); //设置描边宽度
        tp1.setStyle(Paint.Style.STROKE); //设置画笔样式为描边
        tp1.setStrokeJoin(Paint.Join.ROUND); //连接方式为圆角

    }

    //设置描边的颜色和宽度
    public void setStroke(int color,int strokeWidth) {
        tp1.setStrokeWidth(strokeWidth);
        borderText.setTextColor(color);  //设置描边颜色
    }

    @Override
    public void setLineSpacing(float add, float mult) {
        super.setLineSpacing(add, mult);
        borderText.setLineSpacing(add, mult);
    }

    @Override
    public void setMaxWidth(int maxPixels) {
        super.setMaxWidth(maxPixels);
        borderText.setMaxWidth(maxPixels);
    }

    public void setLayoutParams(ViewGroup.LayoutParams params) {
        super.setLayoutParams(params);
        borderText.setLayoutParams(params);
    }

    @Override
    public void setGravity(int gravity) {
        super.setGravity(gravity);
        borderText.setGravity(gravity);
    }

    @Override
    public void setPadding(int left, int top, int right, int bottom) {
        super.setPadding(left, top, right, bottom);
        borderText.setPadding(left,top,right,bottom);
    }

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        borderText.measure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    public void setTextSize(int unit, float size) {
        super.setTextSize(unit, size);
        borderText.setTextSize(unit,size);
    }

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        borderText.layout(left, top, right, bottom);
    }

    //必须使用这个方法设置字体
    public void setText2(CharSequence text) {
        setText(text);
        borderText.setText(getText().toString());
    }

    protected void onDraw(Canvas canvas) {
        borderText.draw(canvas);
        super.onDraw(canvas);
    }
}

二、原理分析

这周接到一个新的需求--所有的文本都要有描边,我心想,这种功能应该很简单吧,网上肯定有很多现成的例子,没想到复制粘贴了一大堆,找了一堆开源库,最终竟然没找到一个好用的,不是位置不对,就是颜色不对,没办法,只能自己研究一下了。

首先网上主要有两种实现方法

  • 一种是使用在TextView的onDraw方法中,用反射的方式更改paint的颜色(在onDraw中直接调用setTextColor会引起多次invalidate,对性能影响非常大,绝对要避免这样使用),
  • 一种是采用新增一个TextView,将其的paint设置为描边,因为描边的字体会比原来的字体大,两个叠加在一起就有描边的效果了。 这里采用第二种方法,以下代码设置画笔属性为描边。
tp1 = borderText.getPaint();
tp1.setStyle(Paint.Style.STROKE); //设置画笔样式为描边
tp1.setStrokeJoin(Paint.Join.ROUND); //连接方式为圆角
tp1.setStrokeWidth(this.strokeWidth); //设置描边宽度

经过研究,网上的其他写法导致描边和文本出现的错位的原因,是因为描边的文本和原有本文的各种属性不一致导致的,我们要维持两个布局属性完全相同的TextView,所以要重写onMeasure、onLayout、setPadding等方法。 如果代码中动态设置了其他属性,务必重写该方法或者写一个新的方法,保证strokeTextView的属性一致。

FAQ

  • 网上有一种方法是在onMeasure中判断两者text是否相同,不同则重新设置,但是如果将TextView设置为固定宽高,则TextView并不会触发onMeasure流程。

  • 为什么要使用borderText.setText(getText().toString());,而不是borderText.setText(getText());

对于使用HTML或者SpannableStringBuilder设置了文本的颜色,则getText得到的内容会包含这些信息,导致描边颜色变为和文本一样的颜色,最终看上去的是加粗的效果,所以一定要使用getText().toString()去除这些颜色信息。

  • 进阶优化 为了维持两者文本的一致性,可以重写onTextChanged方法,当文本发生改变的时候,自动重新设置描边的文字
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
    super.onTextChanged(text, start, lengthBefore, lengthAfter);
    if (bgText != null) {
        CharSequence tt = bgText.getText();
        if (tt == null || !tt.equals(this.getText())) {
            bgText.setText(getText());
            this.postInvalidate();
        }
    }
}