背景
阳光沙滩摸鱼动态增加表情输入,是网站提供的一套内部表情(资源从github获取的),为达到多端统一展示,目前内容区嵌入了html元素展示表情,在web端应该是通用的也是正常的显示方式。移动端需要做适配处理。
先看看数据长什么样子。
{
"id":"1429265850",
"userId":"1139423796",
"nickname":"断点-含光君",
"avatar":"https://imgs.sunofbeaches.com/group1/M00/00/04/rBsADV2YuTKABc4DAABfJHgYqP8031.png",
"company":"巴拉巴拉",
"position":"Goolge高级CV工程师",
"content":"探索摸鱼技术<img class=\"emoji\" src=\"https://cdn.sunofbeaches.com/emoji/1.png\" width=\"20\" height=\"20\">",
"linkCover":""
}
移动端怎么解决文本中包含html元素显示?
我们把内容拿出来分析
"content":"探索摸鱼技术<img class=\"emoji\" src=\"https://cdn.sunofbeaches.com/emoji/1.png\" width=\"20\" height=\"20\">"
内容中如果附带了HTML数据,通过就是富文本了,Android原生api就支持这样的情况。
//要显示的文本
TextView textView = textView.setText(Html.fromHtml(content));
这个需求好像一个api就解决了。我们跑起来看看效果。
如果是普通元素大部分支持,当遇到img的时候,这里会替换成一个小方块,系统不会帮你下载img的src对应地址的图片来展示。
那就看看有没有支持显示图片的方法。
这应该就是了
/**
* Returns displayable styled text from the provided HTML string. Any <img> tags in the
* HTML will use the specified ImageGetter to request a representation of the image (use null
* if you don't want this) and the specified TagHandler to handle unknown tags (specify null if
* you don't want this).
*
* <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
*/
public static Spanned fromHtml(String source, int flags, ImageGetter imageGetter,
TagHandler tagHandler)
方法声明是这样的,还有注释。如果有图片,通过ImageGetter来获取图片返回给textview展示,TagHandler处理一些特殊元素(或者是自己自定义的元素解析)
那就看看怎么用。
使用demo写了代码跑一跑,直接跑看效果。
Spanned sp = Html.fromHtml(content, new Html.ImageGetter() {
@Override
public Drawable getDrawable(String source) {
return null;
}
}, null);
mTextView.setText(sp);
如果这个接口什么都不做,返回null,显示的效果和不使用这个接口一致。也就是我们需要把source对应的图片返回才有效果?先测试一波。
Drawable drawable = ContextCompat.getDrawable(HtmlTestActivity.this, R.drawable.ic_on);
//获取的drawable需要给bounds,不然就是0,看不到
drawable.setBounds(0, 0, 100, 100);
Spanned sp = Html.fromHtml(content, new Html.ImageGetter() {
@Override
public Drawable getDrawable(String source) {
return drawable;
}
}, null);
mTextView.setText(sp);
也就是这个ImageGetter需要返回当前内容中的图片的drawable,如果是n个同理的。
到这里我就发现不对劲,图片来自网络,但是列表是立马给到用户看的,中间加载网络的图片,不管你网络多好,或者说做什么缓存的,都需要下载,这个下载是会失败的,失败了怎么处理呢,下载过程和缓存过程做起来很复杂,如果不行邪的可以试试这样做,回调getDrawable的时候返回的就是src对应的地址,如果内容区有多个,那就回调多次。可以试试从网络获取,成功后进行回调显示,还得做快速滑动处理,缓存==。反正到这里我就觉得方向错了。
解决异步加载表情
在我们使用聊天工具的时候,表情是立马显示,而且我发现微信的表情是内置的!大佬都选择内置,也就是内置!才是解决方案!(表情更新怎么办?如果你在使用qq的时候发现,有些朋友发的表情到你这里会这样提示:[菜狗]使用新版本客户端才能显示的提示)就说明qq也是内置表情,当表情更新的情况下或者增加,旧客户端是不会显示的。
上面我写的demo是加载内置的一个图片来代替表情的显示,是可行的。
获取全部表情的资源
我们打开康师傅网站摸鱼区,按下浏览器f12,刷新一次摸鱼区。
从网络标签中查看请求情况,找到标签的源地址,是有规律的。
https://cdn.sunofbeaches.com/emoji/1.png
https://cdn.sunofbeaches.com/emoji/2.png
...
https://cdn.sunofbeaches.com/emoji/130.png
我们手动下载这么多表情资源太麻烦,本来问康师傅提供一份,由于他在挖坑没空,那只能我们自己动了,表情地址有规律的,重复的东西给机器做。把爬小姐姐图片的技术掏出来。
打开我们文本编辑器,写入如下python代码,开始批量下载脚本编写,保存文件名字emoji.py
import requests
# 图片地址
url = 'https://cdn.sunofbeaches.com/emoji/'
for num in range(1,131):
pngName = str(num) + '.png'
downloadUrl = url + pngName
html = requests.get(downloadUrl)
# 将图片保存到D盘
path = "D:/em/f" + str(num) + '.png'
with open(path,"wb")as f:
f.write(html.content)
d盘创建em文件夹,进入文件所在目录,命令行执行py emoji.py 进入em目录下查看下载表情的情况。
表情资源下载完成(python3环境)
表情加载实现
上面提示过的,这个getDrawable每次都会把src对应的图片地址返回,我们既然做了表情内置,就需要把这个地址 -> 转本地有的图片资源,如果没有,那直接返回null即可,或者返回一个默认的。
我们把下载好的图片资源都放到res的/drawable-xxhdpi文件夹中。开始我们提取表情的代码编写。
private Drawable parserEmoji(String source, int textSize) {
//https://cdn.sunofbeaches.com/emoji/1.png
//如果地址没有,直接返回null
if (TextUtils.isEmpty(source)) {
return null;
}
//如果地址有,是否是我们阳光沙滩的表情地址cdn.sunofbeaches.com/emoji
if (!source.contains("cdn.sunofbeaches.com/emoji")) {
//如果不是我们可以支持的表情地址,那放弃,直接返回null
return null;
}
//如果是我们地址,那就截取png的名字
int start = source.lastIndexOf("/");
int end = source.lastIndexOf(".");
if (start == -1 || end == -1) {
//出现-1情况就是找不到这个字符串,这样应影响到后面的截取api
return null;
}
String emojiName = source.substring(start + 1, end);
//判断名字是否在我们内置的范围内
Log.i("->>", "截取的表情名字:" + emojiName);
if (!TextUtils.isDigitsOnly(emojiName) || TextUtils.isEmpty(emojiName)) {
//如果表情名字不是数字,那不是我们的表情,如果后面规则有改变,可以做同步调整
return null;
}
//判断1-130范围
int emojiId = Integer.parseInt(emojiName);
if (emojiId <= 0 || emojiId > 130) {
return null;
}
try {
//因为图片名字是动态,这个需要反射获取内置表情
Field declaredField = R.drawable.class.getDeclaredField("f" + emojiName);
int emojiDrawableId = declaredField.getInt(R.drawable.class);
Drawable drawable = ContextCompat.getDrawable(this, emojiDrawableId);
if (drawable == null) {
return null;
}
//表情的大小和tv的文字大小一致,如果不设置就是宽高0/0
drawable.setBounds(0, 0, textSize, textSize);
return drawable;
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
动画替换表情的详细方法上面代码给出,也提供了注释。调用地方:
Spanned sp = Html.fromHtml(content, new Html.ImageGetter() {
@Override
public Drawable getDrawable(String source) {
Log.i("->>", "需要加载的图片地址:" + source);
return parserEmoji(source, (int) mTextView.getTextSize());
}
}, null);
mTextView.setText(sp);
Kotlin的项目可以把方法转扩展函数,方便textview使用,这个我就补贴代码了。最后代码放到阳光沙滩App中的效果。适配之前:
适配之后:
总结
1:实现方式选择决定难易程度,根据我对大厂App观察,我跟着他们做
2: 这是临时想出来的方案,希望其大佬也给出一些更成熟的方案吧我等着白嫖
3: 整个功能实现需要是图片资源,+反射获取资源,不使用下载远程表情方案
4: 缺点:如果网站的表情更新了APP也得更新资源,这需要升级才能做到
一起来探索更先进的摸鱼技术,我在阳光沙滩等你,体验下载地址点我下载
2022年03月15日15:58:15
新增了30个B站表情,只要摸鱼的评论中嵌入[]中文的表情名字,就可以在客户端解析出来表情。具体支持的表情看这个。
效果图