服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

可以有,给你的App添加自定义表情!

日期: 来源:刘望舒收集编辑:点击蓝字关注☞
 安卓进阶涨薪训练营,让一部分人先进大厂


大家好,我是皇叔,最近开了一个安卓进阶涨薪训练营,可以帮助大家突破技术&职场瓶颈,从而度过难关,进入心仪的公司。


详情见文章:没错!皇叔开了个训练营



周五啦,来篇轻松的文章~


作者: 小墙程序员
https://juejin.cn/post/7144527505394368525

原理

添加自定义表情的原理其实很简单,就是使用 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 会显示成图片原来的大小。

image.png

ImageSpan 的继承关系图如下,出现了 ReplacementSpan 和 DynamicDrawableSpan 两个新的类,先来看一下它们。MetricAffectingSpan 和 CharacterStyle 接口在 Android Span 原理解析[3] 介绍了,这里就不赘述了。

image.png

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[4]

image.png

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 没有效果一样。

image.png

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

image.png

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

image.png

上面看上去很完美,但是事情没有那么简单。因为我们只是写死了图片的大小,并没有改变图片位置绘制的算法。如果其他地方使用了 EmojiSpan ,但是文字的大小小于图片大小时还是会出问题。如下图,当文字的 textsize 为 10sp 时的情况。

image.png

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

image.png

如果大家对这个的解决办法感兴趣的话,可以查看这篇文章:

https://juejin.cn/post/7196592276159823931

参考资料

[1]

https://s.juejin.cn/ds/jooSN7t: https://s.juejin.cn/ds/jooSN7t

[2]

https://juejin.cn/post/7141730415399665671: https://juejin.cn/post/7141730415399665671

[3]

https://juejin.cn/post/7141730415399665671: https://juejin.cn/post/7141730415399665671

[4]

https://stackoverflow.com/questions/27631736/meaning-of-top-ascent-baseline-descent-bottom-and-leading-in-androids-font: https://link.juejin.cn/?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F27631736%2Fmeaning-of-top-ascent-baseline-descent-bottom-and-leading-in-androids-font

[5]

https://stackoverflow.com/questions/27631736/meaning-of-top-ascent-baseline-descent-bottom-and-leading-in-androids-font: https://link.juejin.cn/?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F27631736%2Fmeaning-of-top-ascent-baseline-descent-bottom-and-leading-in-androids-font




为了防止失联,欢迎关注我防备的小号

 

               微信改了推送机制,真爱请星标本公号

相关阅读

  • TikTok二面:聊聊二维码扫码登录的原理

  • 在日常生活中,二维码出现在很多场景,比如超市支付、系统登录、应用下载等等。了解二维码的原理,可以为技术人员在技术选型时提供新的思路。对于非技术人员呢,除了解惑,还可以引导
  • 泰山游客爆满!从山脚堵到山顶

  • 3月25日是泰山免门票开放的最后一个周末,吸引了全国各地的游客前来,据报道,景区游客爆满,内部人挤人,工作人员高喊“两棵松树之间往回走”。从现场拍摄的视频看,游客们可谓是摩肩
  • 西域健康丨甲流来袭,这些预防对策要牢记!

  • 近期,全国多个省市流感疫情呈现明显上升趋势,根据国家疾控中心报告,流感活动水平上升主要是由甲型流感病毒中的甲型H1N1亚型所致(摘自2023年3月8日中国疾病预防控制中心发布信
  • “市局的”掏手铐威胁他人?当地回应

  • 近日,有网友发文称,河北保定一男子在路上与人发生冲突后,自称“是市局的人”,还掏出一副手铐威胁对方。25日,保定晚报发文称,警方回应该人系某平台网约车司机,非公职人员,已被公安
  • 课程合集

  • ✔️更新完毕的课程1.《AKShare-初阶-使用教学(第一期) 》-1小时上手AKShare快速搭建起使用环境并成功获取数据。无论是否有 Python 编程经验,通过学习该视频课程,都可以快速上手
  • AKShare-基金数据-香港基金净值及分红配送

  • 作者寄语本次更新香港基金净值及分红配送接口。该接口主要包括 历史净值明细,分红送配详情 等指标的历史数据。相关视频教程已经发布:《AKShare-初阶-使用教学》、《AKShare-
  • 利用Python实现B站自动签到

  • 利用python实现B站自动签到,快速提高你的等级。签到内容包括,登录签到、
    视频观看、5次投币(需消耗5个币)、分享视频(不会发在个人空间动态里)。使用1、浏览器登入哔哩网站2、F12

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 对第三方测评要更有耐心

  • 第三方测评的确具有较强的主观性,但这是由于商品性质和消费者需求决定的,其实有利于市场的多元化发展。而对于以商养测、广告带货的现象,在消费者自身加强鉴别的同时,要适当扩张
  • TikTok二面:聊聊二维码扫码登录的原理

  • 在日常生活中,二维码出现在很多场景,比如超市支付、系统登录、应用下载等等。了解二维码的原理,可以为技术人员在技术选型时提供新的思路。对于非技术人员呢,除了解惑,还可以引导
  • 可以有,给你的App添加自定义表情!

  • 安卓进阶涨薪训练营,让一部分人先进大厂大家好,我是皇叔,最近开了一个安卓进阶涨薪训练营,可以帮助大家突破技术&职场瓶颈,从而度过难关,进入心仪的公司。详情见文章:没错!皇叔开了
  • 中国人,不要再炒房了!

  • ‍‍明天下午1点直播,与大家一起聊聊2023年的楼市走势,同时分析如何判断地段潜力以及解答大家关于贷款的疑难问题,马上点击预约,及时收看,将有机会免费获赠2023年一共30万字的新
  • 结婚有淡季,但离婚的人永不缺席

  • 小李是南方某小城民政局一位负责婚姻登记的工作人员。她在那里工作了六年,隔着玻璃窗,看人来结婚、离婚,结婚、离婚……为超过一万对当事人办理了婚姻登记。在婚姻登记大厅,小李