IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 如何给你的 Android App 添加自定义表情 -> 正文阅读

[移动开发]如何给你的 Android App 添加自定义表情

上一篇文章 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 的继承关系图如下,出现了 ReplacementSpanDynamicDrawableSpan 两个新的类,先来看一下它们。MetricAffectingSpanCharacterStyle 接口在 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

这里需要特殊注意 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 没有效果一样。

在这里插入图片描述
去掉辅助线后,看上去更明显一些。
在这里插入图片描述

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 图)。

参考

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-09-24 21:08:24  更:2022-09-24 21:11:15 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 23:20:21-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码