我报名参加金石方案1期应战——瓜分10万奖池,这是我的第2篇文章,点击查看活动概况

上一篇文章 Android Span 原理解析 介绍了 Span 的原理。这一篇文章将介绍 Span 的运用,运用 Span 来给 App 增加自界说表情。

原理

增加自界说表情的原理其实很简略,便是运用 ImageSpan 对文字进行替换。代码如下:

ImageSpan imageSpan = new ImageSpan(this, R.drawable.emoji_kelian);
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder("哈哈哈哈[不幸]");
spannableStringBuilder.setSpan(imageSpan, 4, spannableStringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannableStringBuilder);

上面的代码把 [不幸] 文字替换成了对应的表情图片。效果如下图,能够看到图片的巨细不符合预期,这是由于 ImageSpan 会显现成图片原来的巨细。

给你的 Android App 添加自定义表情

ImageSpan 的继承联系图如下,呈现了 ReplacementSpanDynamicDrawableSpan 两个新的类,先来看一下它们。MetricAffectingSpanCharacterStyle 接口在 Android Span 原理解析 介绍了,这儿就不赘述了。

给你的 Android App 添加自定义表情

ReplacementSpan 接口

ReplacementSpan 是一个接口,看名字是用来替换文字的。它里边界说了两个办法,如下所示。


public abstract int getSize(@NonNull Paint paint,
                                    CharSequence text,
                                    @IntRange(from = 0) int start,
                                    @IntRange(from = 0) int end,
                                    @Nullable Paint.FontMetricsInt fm);

回来替换后 Span 的宽,上面的例子中便是回来图片的宽度,参数作用如下:

  • paint: Paint 的实例
  • text: 当时文本,上面的例子中它的值是是 哈哈哈哈[不幸]
  • start: Span 的开端方位,这儿是 4
  • end: Span 的结束方位,这儿是 8
  • fm: FontMetricsInt 的实例

FontMetricsInt 是描绘给定文本巨细的字体的各种衡量的类。内部特点代表的含义如下图:

  • Top:图中紫线的方位
  • Ascent: 图中绿线的方位
  • Descent: 图中蓝线的方位
  • Bottom: 图中黄线的方位
  • Leading: 未在图中标出,是指上一行的 Bottom 与下一行的 Top 之间的距离。

图片来历 Meaning of top, ascent, baseline, descent, bottom, and leading in Android’s FontMetrics

给你的 Android App 添加自定义表情

Baseline 是文字制作的基准线。它不界说在 FontMetricsInt 中,但能够经过 FontMetricsInt 的特点获取。

上面讲到 getSize 办法只回来宽度,那高度是怎么确定的呢?其实它是经过 FontMetricsInt 来控制,不过这儿有个坑,后面会说到。


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

在 Canvas 中制作 Span。参数如下:

  • canvas:Canvas 实例
  • text:当时文本
  • start:Span 的开端方位
  • end:Span 的结束方位
  • x:[不幸] 的 x 坐标方位
  • top:当时行的 “Top“ 特点值
  • y:当时行的 Baseline
  • bottom: 当时行的 ”Bottom“ 特点值
  • paint:Paint 实例,可能为 null

这儿需求特殊留意 TopBottom,跟上面说的有点不同这儿先记住,后面会一同介绍。

DynamicDrawableSpan

DynamicDrawableSpan 完成了 ReplacementSpan 接口的办法。同时它是一个笼统类,界说了 getDrawable 笼统办法,由 ImageSpan 完成来获取 Drawable 实例。源码如下:

@Override
public int getSize(@NonNull Paint paint, CharSequence text,
    @IntRange(from = 0) int start, @IntRange(from = 0) int end,
    @Nullable Paint.FontMetricsInt fm) {
    Drawable d = getCachedDrawable();
    Rect rect = d.getBounds();
    //设置图片的高
    if (fm != null) {
    fm.ascent = -rect.bottom;
    fm.descent = 0;
    fm.top = fm.ascent;
    fm.bottom = 0;
    }
    return rect.right;
}
@Override
public 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) {
    Drawable b = getCachedDrawable();
    canvas.save();
    int transY = bottom - b.getBounds().bottom;
    //设置对齐办法,有三种分别是
    //ALIGN_BOTTOM 底部对齐,默认
    //ALIGN_BASELINE 基线对齐
    //ALIGN_CENTER 居中对齐
    if (mVerticalAlignment == ALIGN_BASELINE) {
        transY -= paint.getFontMetricsInt().descent;
    } else if (mVerticalAlignment == ALIGN_CENTER) {
        transY = top + (bottom - top) / 2 - b.getBounds().height() / 2;
    }
    canvas.translate(x, transY);
    b.draw(canvas);
    canvas.restore();
}
public abstract Drawable getDrawable();

DynamicDrawableSpan 有两个坑需求特别留意。

第一个坑便是在 getSize 中的 Paint.FontMetricsInt 对象和 draw 办法中经过 paint.getFontMetricsInt() 获取的不是一个对象。也便是说,无论咱们在 getSizePaint.FontMetricsInt 中设置什么值,都不会影响到 paint.getFontMetricsInt() 获取对象中的值。它影响的是 topbottom 的值,这也是刚才介绍参数时给 Top 和 Bottom 打引号的原因。

第二个坑是 ALIGN_CENTER图片巨细超越文字巨细时“不起作用”。如下图所示,为了方便显现我加了辅助线,白线是代表参数 top,bottom,可是 bottom 被其它颜色覆盖了。能够看到,图片是居中的,是文字没有居中让咱们看上去 ALIGN_CENTER 没有效果一样。

给你的 Android App 添加自定义表情

去掉辅助线后,看上去更显着一些。

给你的 Android App 添加自定义表情

ImageSpan

ImageSpan 就简略多了,它只完成了 getDrawable() 办法来获取 Drawable 实例,代码如下:

@Override
public Drawable getDrawable() {
    Drawable drawable = null;
    if (mDrawable != null) {
        drawable = mDrawable;
    } else if (mContentUri != null) {
        Bitmap bitmap = null;
        try {
            InputStream is = mContext.getContentResolver().openInputStream(
            mContentUri);
            bitmap = BitmapFactory.decodeStream(is);
            drawable = new BitmapDrawable(mContext.getResources(), bitmap);
            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
            drawable.getIntrinsicHeight());
            is.close();
        } catch (Exception e) {
            Log.e("ImageSpan", "Failed to loaded content " + mContentUri, e);
        }
    } else {
        try {
            drawable = mContext.getDrawable(mResourceId);
            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
            drawable.getIntrinsicHeight());
        } catch (Exception e) {
            Log.e("ImageSpan", "Unable to find resource: " + mResourceId);
        }
    }
    return drawable;
}

这儿代码很简略,咱们仅有需求重视的便是获取 Drawable 时,需求设置它的宽高,让它别超越文字的巨细。

完成

说完前面的原理后,完成起来就非常简略了。咱们只需求继承 DynamicDrawableSpan,完成 getDrawable() 办法,让图片的宽高别超越文字的巨细就行了。效果如下图所示:


public class EmojiSpan extends DynamicDrawableSpan {
    @DrawableRes
    private int mResourceId;
    private Context mContext;
    private Drawable mDrawable;
    public EmojiSpan(@NonNull Context context, int resourceId) {
        this.mResourceId = resourceId;
        this.mContext = context;
    }
    @Override
    public Drawable getDrawable() {
        Drawable drawable = null;
        if (mDrawable != null) {
            drawable = mDrawable;
        } else {
            try {
                drawable = mContext.getDrawable(mResourceId);
                drawable.setBounds(0, 0, 48, 48);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return drawable;
    }
}

给你的 Android App 添加自定义表情

上面看上去很完美,可是事情没有那么简略。由于咱们仅仅写死了图片的巨细,并没有改变图片方位制作的算法。假如其他地方运用了 EmojiSpan ,可是文字的巨细小于图片巨细时还是会出问题。如下图,当文字的 textsize 为 10sp 时的状况。

给你的 Android App 添加自定义表情

实际上,文字大于图片巨细时也有问题。如下图所示,多行的状况下,只有表情的行间距显着小于其他行的间距。

给你的 Android App 添加自定义表情

假如我们对这个的解决办法感兴趣的话,点赞+收藏数 >= 40,我就复刻一下B站的自界说表情,加上会动的自界说表情(实际上是 Gif 图)。

参考

  • Meaning of top, ascent, baseline, descent, bottom, and leading in Android’s FontMetrics