TextView里画世界——ReplacementSpan实践

需求&效果

  相信很多同学都多多少少接触过一些常用的Span,例如,用于设置TextView里某段文字字体大小的AbsoluteSizeSpan,可以改变背景颜色的BackgroundColorSpan,还有可以直接画出一个图片ImageSpan等等,常用的Span用法百度谷歌一下一大把,这里就不再赘述。今天,我想和大家分享稍微高♂级一点的内容:如何通过extends ReplacementSpan在TextView控件区域内画自己想画的东西。单是用文字说,你可能会感觉有点懵,下面我们先来看来自某个知名App里的ListView item的效果图:

在这里插入图片描述

  上图里自带背景的“精”、“热”这两个小icon,就是我想要实现的最终效果。可能有人要说,这还不简单,我做一个水平LinearLayout,然后往里面放三个TextView也能实现,根本不需要用到Span。用常规的布局实现缺点太多了,容我一个个给你数:
  1. 只用一个水平的LinearLayout实现不了,“阿狸”的“狸”字会出现在“热”icon的右下方,而不是在行首。
  2. 这个是ListView的Item,如果icon显示的个数由服务端控制,动态addView会导致ListView滑动不够流畅,而提前在xml写好若干个View(如最大个数9个),会导致单个Item里View的个数增加,不够优雅,性能也不好。
  3. 效果要求:垂直方向,icon的中轴线与正常文字的中轴线要对齐,一旦正常文字的字号改变,icon的位置也要手动调整。
  4. …

  如果icon是单纯的图片,其实用文章开始提到的ImageSpan就可以搞定,但是如果ui组的同事比较懒,不愿意因为改变色值或内容而重新切图呢,比如我们公司的(逃- -!!,那背景和字都得我们自己画,开始动手吧!

ReplacementSpan实践

  关于ReplacementSpan这个类,开发者文档和源码里几乎没明确说明这玩意儿到底是啥。不过通过名称,我们可以猜测,这个是可以用作替换功能的Span,也就是用这个Span替代文字。
  ReplacementSpan只有两个抽象方法需要我们@Override:

/**
 * Returns the width of the span
 */
public abstract int getSize(Paint paint, CharSequence 	text, int start, int end, Paint.FontMetricsInt fm);

/**
 * Draws the span into the canvas.
 */
public abstract void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint);

  第一个方法getSize(),返回值就是Span替换文字后所占的宽度
  第二个方法draw(),在TextView绘制时被调用,与此同时,会把canvas,text,paint以及一堆坐标传给我们,我们覆盖这个方法,就可以在特定位置画一些我们想画的东西了。

  下面我盗了一张FontMetrics图以助于你理解,其实TextView里文字所占的高度就是FontMetrics的descent到ascent的距离。PS:以baseline为参考线,descent为正,ascent为负

   在这里插入图片描述

接下来show you code:

IconTextSpan.java

public class IconTextSpan extends ReplacementSpan {
    private Context mContext;
    private int mBgColorResId; //Icon背景颜色
    private String mText;  //Icon内文字
    private float mBgHeight;  //Icon背景高度
    private float mBgWidth;  //Icon背景宽度
    private float mRadius;  //Icon圆角半径
    private float mRightMargin; //右边距
    private float mTextSize; //文字大小
    private int mTextColorResId; //文字颜色

    private Paint mBgPaint; //icon背景画笔
    private Paint mTextPaint; //icon文字画笔

    public IconTextSpan(Context context, int bgColorResId, String text) {
        if (TextUtils.isEmpty(text)) {
            return;
        }
        //初始化默认数值
        initDefaultValue(context, bgColorResId, text);
        //计算背景的宽度
        this.mBgWidth = caculateBgWidth(text);
        //初始化画笔
        initPaint();
    }

    /**
     * 初始化画笔
     */
    private void initPaint() {
        //初始化背景画笔
        mBgPaint = new Paint();
        mBgPaint.setColor(mContext.getResources().getColor(mBgColorResId));
        mBgPaint.setStyle(Paint.Style.FILL);
        mBgPaint.setAntiAlias(true);

        //初始化文字画笔
        mTextPaint = new TextPaint();
        mTextPaint.setColor(mContext.getResources().getColor(mTextColorResId));
        mTextPaint.setTextSize(mTextSize);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
    }

    /**
     * 初始化默认数值
     *
     * @param context
     */
    private void initDefaultValue(Context context, int bgColorResId, String text) {
        this.mContext = context.getApplicationContext();
        this.mBgColorResId = bgColorResId;
        this.mText = text;
        this.mBgHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 17f, mContext.getResources().getDisplayMetrics());
        this.mRightMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, mContext.getResources().getDisplayMetrics());
        this.mRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, mContext.getResources().getDisplayMetrics());
        this.mTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 13, mContext.getResources().getDisplayMetrics());
        this.mTextColorResId = R.color.white_a;
    }

    /**
     * 计算icon背景宽度
     *
     * @param text icon内文字
     */
    private float caculateBgWidth(String text) {
        if (text.length() > 1) {
            //多字,宽度=文字宽度+padding
            Rect textRect = new Rect();
            Paint paint = new Paint();
            paint.setTextSize(mTextSize);
            paint.getTextBounds(text, 0, text.length(), textRect);
            float padding = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, mContext.getResources().getDisplayMetrics());
            return textRect.width() + padding * 2;
        } else {
            //单字,宽高一致为正方形
            return mBgHeight;
        }
    }

    /**
     * 设置右边距
     *
     * @param rightMarginDpValue
     */
    public void setRightMarginDpValue(int rightMarginDpValue) {
        this.mRightMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, rightMarginDpValue, mContext.getResources().getDisplayMetrics());
    }

    /**
     * 设置宽度,宽度=背景宽度+右边距
     */
    @Override
    public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
        return (int) (mBgWidth + mRightMargin);
    }

    /**
     * draw
     * @param text 完整文本
     * @param start setSpan里设置的start
     * @param end setSpan里设置的start
     * @param x
     * @param top 当前span所在行的上方y
     * @param y y其实就是metric里baseline的位置
     * @param bottom 当前span所在行的下方y(包含了行间距),会和下一行的top重合
     * @param paint 使用此span的画笔
     */
    @Override
    public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        //画背景
        Paint bgPaint = new Paint();
        bgPaint.setColor(mContext.getResources().getColor(mBgColorResId));
        bgPaint.setStyle(Paint.Style.FILL);
        bgPaint.setAntiAlias(true);
        Paint.FontMetrics metrics = paint.getFontMetrics();

        float textHeight = metrics.descent - metrics.ascent;
        //算出背景开始画的y坐标
        float bgStartY = y + (textHeight - mBgHeight) / 2 + metrics.ascent;

        //画背景
        RectF bgRect = new RectF(x, bgStartY, x + mBgWidth, bgStartY + mBgHeight);
        canvas.drawRoundRect(bgRect, mRadius, mRadius, bgPaint);

        //把字画在背景中间
        TextPaint textPaint = new TextPaint();
        textPaint.setColor(mContext.getResources().getColor(mTextColorResId));
        textPaint.setTextSize(mTextSize);
        textPaint.setAntiAlias(true);
        textPaint.setTextAlign(Paint.Align.CENTER);  //这个只针对x有效
        Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
        float textRectHeight = fontMetrics.bottom - fontMetrics.top;
        canvas.drawText(mText, x + mBgWidth / 2, bgStartY + (mBgHeight - textRectHeight) / 2 - fontMetrics.top, textPaint);
    }

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = (TextView) findViewById(R.id.tv);
        textView.setTextSize(20);

        List<ReplacementSpan> spans = new ArrayList<>();
        String content = "Android是一种基于Linux的自由及开放源代码的操作系统,主要使用于移动设备,如智能手机和平板电脑,由Google公司和开放手机联盟领导及开发。尚未有统一中文名称,中国大陆地区较多人使用“安卓”或“安致”。";
        StringBuilder stringBuilder = new StringBuilder();
        //第一个Span
        stringBuilder.append(" ");
        IconTextSpan topSpan = new IconTextSpan(getApplicationContext(), R.color.colorPrimary, "置顶");
        spans.add(topSpan);
        //第二个Span
        stringBuilder.append(" ");
        IconTextSpan hotSpan = new IconTextSpan(getApplicationContext(), R.color.colorAccent, "热");
        hotSpan.setRightMarginDpValue(5);
        spans.add(hotSpan);

        stringBuilder.append(content);
        SpannableString spannableString = new SpannableString(stringBuilder.toString());
        //循环遍历设置Span
        for (int i = 0; i < spans.size(); i++) {
            spannableString.setSpan(spans.get(i), i, i + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        textView.setText(spannableString);
    }
}

  最终的显示效果是这样的,聪明的小伙伴们,你们看懂了么~

   在这里插入图片描述

  • 20
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值