注册

高仿B站自定义表情

在之前的文章给你的 Android App 添加自定义表情中我们介绍了自定义表情的原理,没看过的建议看一下。这一篇文章将介绍它的应用,这里以B站的自定义表情面板为例,效果如下:

b2a76c1f590e41f6a0a5c94f04ada053~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp

自定义表情的大小

当我们写死表情的大小时,文字的 textSize 变大变小时都会有一点问题。

文字大于图片大小时,在多行的情况下,只有表情的行间距明显小于其他行的间距。如图:

0a8ee27c39754b37b85c4e68aba31ffe~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp

为什么会出现这种情况呢?如下图所示,我在top, ascent, baseline, descent, bottom的位置标注了辅助线。

d388760f5fdc4947a325b2dc9314491d~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp

可以很清晰的看到,在只有表情的情况下,top, ascent, descent, bottom的位置有明显的问题。原因是 DynamicDrawableSpangetSize 方法里面对 FontMetricsInt 进行了修改。解决的方式很简单,就是注释掉修改代码就行,代码如下。修改后,效果如下图所示。

@Override
   public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable FontMetricsInt fm) {
         Drawable d = getDrawable();
         Rect rect = d.getBounds();
//
//       if (fm != null) {
//           fm.ascent = -rect.bottom;
//           fm.descent = 0;
//
//           fm.top = fm.ascent;
//           fm.bottom = 0;
//       }

       return rect.right;
  }

cda307cdf3394611a35e2586abac6522~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp

不知道你还记不记得,我们说过getSize 的返回值是表情的宽度。上面的注释代码其实是设置了表情的高度,如果文本的大小少于表情时,就会显示不全,如下图所示:

c86025f60e5f477495bbfc0af2ce5809~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp

那这种情况下,应该怎么办?这里不卖关子了,最终代码如下。解决方式非常简单就是分情况来判断。当文本的高度小于表情的高度时,设置 fmtop, ascent, descent, bottom的值,让行的高度变大的同时让大的 emoji 图片居中。

 @Override
  public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable FontMetricsInt fm) {
      Drawable d = getDrawable();
      Rect rect = d.getBounds();

      float drawableHeight = rect.height();
      Paint.FontMetrics paintFm = paint.getFontMetrics();

      if (fm != null) {
          int textHeight = fm.bottom - fm.top;
          if(textHeight <= drawableHeight) {//当文本的高度小于表情的高度时
              //解决文字的大小小于图片大小的情况
              float textCenter = (paintFm.descent + paintFm.ascent) / 2;
              fm.ascent = fm.top = (int) (textCenter - drawableHeight / 2);
              fm.descent = fm.bottom = (int) (textCenter + drawableHeight / 2);
          }
      }
  return rect.right;
}

当然,你可能发现了,B站的 emoji 表情好像不是居中的。如下图所示,B站对 emoji 表情的处理类似基于 baseline 对齐。

3959fb13f5c845e5be7af860b7f6dd81~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp

上面最难理解的居中已经介绍,对于其他方式比如 baseline 和 bottom 就简单了。完整代码如下:

@Override
  public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable FontMetricsInt fm) {
      Drawable d = getDrawable();
      if(d == null) {
          return 48;
      }
      Rect rect = d.getBounds();

      float drawableHeight = rect.height();
      Paint.FontMetrics paintFm = paint.getFontMetrics();

      if (fm != null) {
          if (mVerticalAlignment == ALIGN_BASELINE) {
              fm.ascent = fm.top = (int) (paintFm.bottom - drawableHeight);
              fm.bottom = (int) (paintFm.bottom);
              fm.descent = (int) paintFm.descent;
          } else if(mVerticalAlignment == ALIGN_BOTTOM) {
              fm.ascent = fm.top = (int) (paintFm.bottom - drawableHeight);
              fm.bottom = (int) (paintFm.bottom);
              fm.descent = (int) paintFm.descent;
          } else if (mVerticalAlignment == ALIGN_CENTER) {
              int textHeight = fm.bottom - fm.top;
              if(textHeight <= rect.height()) {
                  float textCenter = (paintFm.descent + paintFm.ascent) / 2;
                  fm.ascent = fm.top = (int) (textCenter - drawableHeight / 2);
                  fm.descent = fm.bottom = (int) (textCenter + drawableHeight / 2);
              }
          }
      }

      return rect.right;
  }

动态表情

动态表情实际上就是 gif 图。我们可以使用 android-gif-drawable 来实现。在 build.gradle 中增加依赖:

dependencies {
  ...
  implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.25'
}

然后在我们创建自定义 ImageSpan 的时候传入参数就可以了:

val size = 192
val gifFromResource = GifDrawable(getResources(), gifData.drawableResource)
gifFromResource.stop()
gifFromResource.setBounds(0,0, size, size)
val content = mBinding.editContent.text as SpannableStringBuilder
val stringBuilder = SpannableStringBuilder(gifData.text)
stringBuilder.setSpan(BilibiliEmojiSpan(gifFromResource, ALIGN_BASELINE),
  0, stringBuilder.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

关于 android-gif-drawable 更具体用法可以看 Android加载Gif动画android-gif-drawable的使用

作者:小墙程序员

来源:juejin.cn/post/7196592276159823931

0 个评论

要回复文章请先登录注册