给你的 Android App 添加自定义表情
我报名参加金石方案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 会显现成图片原来的巨细。
ImageSpan 的继承联系图如下,呈现了 ReplacementSpan
和 DynamicDrawableSpan
两个新的类,先来看一下它们。MetricAffectingSpan
和 CharacterStyle
接口在 Android Span 原理解析 介绍了,这儿就不赘述了。
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
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
这儿需求特殊留意 Top 和 Bottom,跟上面说的有点不同这儿先记住,后面会一同介绍。
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()
获取的不是一个对象。也便是说,无论咱们在 getSize
的 Paint.FontMetricsInt
中设置什么值,都不会影响到 paint.getFontMetricsInt()
获取对象中的值。它影响的是 top 和 bottom 的值,这也是刚才介绍参数时给 Top 和 Bottom 打引号的原因。
第二个坑是 ALIGN_CENTER
在图片巨细超越文字巨细时“不起作用”。如下图所示,为了方便显现我加了辅助线,白线是代表参数 top,bottom,可是 bottom 被其它颜色覆盖了。能够看到,图片是居中的,是文字没有居中让咱们看上去 ALIGN_CENTER
没有效果一样。
去掉辅助线后,看上去更显着一些。
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;
}
}
上面看上去很完美,可是事情没有那么简略。由于咱们仅仅写死了图片的巨细,并没有改变图片方位制作的算法。假如其他地方运用了 EmojiSpan
,可是文字的巨细小于图片巨细时还是会出问题。如下图,当文字的 textsize 为 10sp 时的状况。
实际上,文字大于图片巨细时也有问题。如下图所示,多行的状况下,只有表情的行间距显着小于其他行的间距。
假如我们对这个的解决办法感兴趣的话,点赞+收藏数 >= 40,我就复刻一下B站的自界说表情,加上会动的自界说表情(实际上是 Gif 图)。
参考
- Meaning of top, ascent, baseline, descent, bottom, and leading in Android’s FontMetrics