TextView里画世界——ReplacementSpan实践

来源:互联网 发布:微信小视频特效软件 编辑:程序博客网 时间:2024/06/18 11:35

需求&效果

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

   example

  上图里自带背景的“精”、“热”这两个小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为负

   fontMetric

接下来show your 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);    }    @Override    public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {        //获取当前TextView所用画笔的FontMetrics        Paint.FontMetrics metrics = paint.getFontMetrics();        //计算画icon背景的起始y坐标,计算公式:TextView文字开始画的位置 + (文字高度 - icon背景高度) / 2;        float bgStartY = metrics.ascent - metrics.top + ((metrics.descent - metrics.ascent) - mBgHeight) / 2;         if (bgStartY < 0) {            bgStartY = 0;        }        //画背景        RectF bgRect = new RectF(x, bgStartY, x + mBgWidth, bgStartY + mBgHeight);        canvas.drawRoundRect(bgRect, mRadius, mRadius, mBgPaint);        //计算画icon文字起始y坐标,计算公式:画icon背景的起始坐标 + (icon背景高度 - icon文字高度 / 2) - TextView paint的fontMetrics.top        //注意:最后多减去一个fontMetrics.top,是因为drawText方法的指定的y是画文字baseline的y,fontMetrics里baseline和top之间的距离为-top        Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();        float iconTextHeight = fontMetrics.bottom - fontMetrics.top;        float iconTextStartY = bgStartY + ((mBgHeight - iconTextHeight) / 2 - fontMetrics.top);        //画文字        canvas.drawText(mText, x + mBgWidth / 2, iconTextStartY, mTextPaint);    }}

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);    }}

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

   result

1 0