Android基于云信实现微信斗图
Posted microhex
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android基于云信实现微信斗图相关的知识,希望对你有一定的参考价值。
总体概览
最近项目出现了一个新的需求,需要实现类似微信的表情的斗图功能。由于我们是一家基于互联网+装修的公司,为了给业主创造一个好的印象和营造开工、完工的美好气象,这个需求我们还是默默的接了下来,虽然我们知道坑那是多得一比。下面我们就简单说明一下我们的方案。
已经实现的功能大概是这样的:
大致实现的功能如下:
- 服务器动态配置斗图的类型和风格。因为每家公司的主题和风格样式都是不一样的,这里需要后台配置,服务器有什么图,客户端才显示什么图。特别的节日气息我们可以增删一些图片。
- 斗图可以是jpg、png和gif图片,大小可是控制,但需要提供图片的尺寸(实际的宽和高)。
- 客户端按服务器端动态改变斗图样式。
- 客户端可以播放jpg、png和gif动图。
服务器端准备
我们的思路大概如下图所示:
大致的逻辑如下:
- 客户端首先请求服务器斗图md5值,这个斗图md5是根据斗图表情文件计算出来的。之所以要请求这个这个md5值,有以下几个目的:如果修改了斗图表情包里面的内容,像你新增了某些表情包,删除了某些表情包,那么表情包文件的md5值就会变化,我们客户端通过比较如果本地的md5值和服务器的md5值不一样,我们就知道了表情包更新了,就需要重新下载新的表情包了,比较简单,同时也很快捷。
- 通过比较新旧md5值,我们就可以判断是否需要下载服务器的表情包了。因为这个表情包一般都比较大,所以如果本地的表情包存在,而且没有损坏,能够加载出表情的内容,那我们直接使用本地的,不用直接请求就可以了。
- 如果本地不存在,我们可以看出,先是下载我们的表情包文件,此时的文件我们定义为json格式的,可以通过Gson或者FastJson等第三方工具直接构建我们的JavaBean对象,也是十分快的。下载完表情包文件之后,先存储,然后更新本的md5值,最后构建成我们新的java对象,供云信调用。
客户端
这里主要是大致讲一下android的具体实现,ios客户端大致上也是类似的相同模式。
自定义消息
首先我们使用的是云信,官网,猪场最近名声不怎么好,也没什么办法,主要是这个项目两年前就已经用云信了,综合起来看,效果还是可以的。话不多扯,我们也是集成了云信开源的第三方组件库UIKit, 这个bug还是蛮多的,这里不说了,各位集成了的,都希望自求多福了。
为了实现可以斗图功能,目前云信的几个消息类型显然是不能满足的。我们首先需要明确的是:
- 斗图其实就是显示一张图片,这张图片的url是我们服务器下发的,它可能是jpg、png或者gif。
- 我们需要合适的组件可以显示jpg、png和gif等不同图片类型。
- 在RecyclerView中作为自定义类型,需要滑动流畅,不能卡顿,有停顿的感觉。
我们假设是很熟悉云信的架构,如果不熟悉,可以先听我瞎比比一下也是可以的。
首先我们自定义一下表情包的消息。先展示一下消息体:
"type" : 29,
"data" :
"url" : "http://www.shigong.com/32.gif",
"type" : "0",
"name" : "鼓掌",
"img_width": 204,
"img_height" : 304,
"extend" : null
大致含义如下:
字段 | 属性 |
---|---|
type | 这个是云信自定义消息分类值,你可以自己定义,因为我们自定义的消息非常多, 所以这个达到了29 |
url | 资源的url地址 |
type | 内部type值,表明是表情包的类别,比如是jpg、png还是gif等格式图片 |
name | 定义表情包的意义,每一个表情包都有自己的灵魂和意义 |
img_width | url资源的宽度,这里是为了解决在Recylerview中快速滑动时不至于过于卡顿,我们通过明确控制表情包的大小直接写死布局的大小,避免使用wrap_content造成测量时耗时问题 |
img_height | url资源的高度 |
extend | 暂时无意义,用于以后可能的扩展使用 |
大致自定义表情结构类如下:
public class CustomImageAttachment extends CustomAttachment
private static final String CUSTOM_URL = "url";
private static final String CUSTOM_TYPE = "type";
private static final String CUSTOM_NAME = "name";
private static final String CUSTOM_IMG_WIDTH = "img_width" ;
private static final String CUSTOM_IMG_HEIGHT = "img_height";
private static final String CUSTOM_EXTEND = "extend";
//自定义图片url
private String url ;
//类型
public String type ;
//描述
public String name ;
//自定义表情宽度
public String width;
//自定义表情高度
public String height;
public CustomImageAttachment()
super(CustomAttachmentType.CUSTOM_IMAGE);
@Override
protected JSONObject packData()
JSONObject jsonObject = new JSONObject();
jsonObject.put(CUSTOM_URL, url);
jsonObject.put(CUSTOM_NAME, name);
jsonObject.put(CUSTOM_TYPE, type);
jsonObject.put(CUSTOM_IMG_WIDTH,width);
jsonObject.put(CUSTOM_IMG_HEIGHT,height);
jsonObject.put(CUSTOM_EXTEND,null);
return jsonObject;
@Override
protected void parseData(JSONObject data)
url = data.getString(CUSTOM_URL);
name = data.getString(CUSTOM_NAME);
type = data.getString(CUSTOM_TYPE);
width = data.getString(CUSTOM_IMG_WIDTH);
height = data.getString(CUSTOM_IMG_HEIGHT);
public String getUrl()
return url;
public void setUrl(String url)
this.url = url;
public String getCustomType()
return type;
public void setCustomType(String type)
this.type = type;
public String getName()
return name;
public void setName(String name)
this.name = name;
public void setWidth(String width)
this.width = width;
public void setHeight(String height)
this.height = height;
public int getIntWidth()
if(TextUtils.isEmpty(width)) return 0;
return BaseStringUtils.safe2Int(width);
public int getIntHeight()
if(TextUtils.isEmpty(height)) return 0;
return BaseStringUtils.safe2Int(height);
实体类好了,我们现在需要想到布局了。刚开始想到了很简单,只使用一个ImageView就可以了,比如这样的:
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/id_iv_image"
/>
同时也google到了Glide也是可以加载gif图片的,同时Glide的天然属性是可以加载jpg、png的属性图片的,所以关键代码如下:
CustomImageAttachment attachment = (CustomImageAttachment) message.getAttachment();
if(attachment.getCustomType() == 0) //gif
Glide.load(attachment.getUrl()).asGif().info(id_iv_image);
else //png jpg and other format
Glide.load(attachment.getUrl()).info(id_iv_image);
//...code ignore
大致就这么多?我觉得应该很简单了,直接撸就完事,但是看到一张图之后:
有些奇怪的是,使用Glide加载出gif图片之后,左边的图片发现背景全变黑色了,但是我们实际的gif图片却没有黑色背景,搞得我百思不得其解,查了很多相关资料,也没能搞出个完美的解释(也许我google的方式不对,如果你有更好的方式,请留言告诉我,最终UI小姐姐告诉我,这张图的背景是透明的,但是是透明的为啥Glide加载就变成了黑色的呢?不太明白,要改!!!暂定。
在测试使用一段时间,发现机子内存有些吃不消了,有些低端机型直接报出OOM了,原因是我们的后台有很多gif图片都是五六百KB的,还有一个是2M多,这感觉有些吃不消了啊,可能Glide本身对Gif图片的支持并没有那么好吧。如果后台没有控制图片的大小,任由管理人员CRUD,我担心这APP这个功能就比较鸡肋了。
基于以上两个原因,我的想法是既然Glide会更改Gif图片的属性(会使背景变黑,这个虽然可以通过UI小姐姐切图更改,但总觉得不是长久之计),Glide对Gif图片的支持没有想象中那么优秀,那么我们可不可以使用更加专业的Gif插件支持呢?
答案是有!
在github上找到了android-gif-drawable,但是坑爹的事情又来:
它支持文件,支持Bitmap,支持数组,支持Uri,就是不支持URL!!!在观察微信客户端 但是的确是这么个意思呀!微信在发送动图的时候,对方好像也是先要下载到本地,下载完成之后才开始播放的。那我们是不是也需要这么做呢?现将url资源下载到本地,然后下载,下载完成之后再播放呢?
说干就干吧,不过又需要重新面对各种问题了:
1.RecyclerView中同时会下载好多个Item,如果每个Item都是需要去下载的,伴随中滑动,会不会有重复下载的?
2.加入我们将一个URL资源下载成功?怎么回调到RecyclerView中,告知哪一个Item下载成功,然后重新更新界面?
3.如果已经下载过一次了,但是下载资源失败,我们应该采用什么样的方式去兜底,此时该如何显示该图片呢?
4.如果下载会持续一段时间,那我们的PlaceHolder应该怎么显示呢?
这只是我当前在开发和测试中遇到的一些问题,在解决了这些问题之后,大致可以进行播放了。
这里先画一张草图:
现在,最主要的目标是 该如何设计我们的DownLoadManager,需要满足的条件是:
- 针对同一个url需要自动过滤,相同的下载资源直接pass。
- 每个下载任务需要有一个TaskTag,通过下载TaskTag可以知道是哪一个url下发的任务,这样就能资源混乱。
- 多线程下下载,怎么才能保证回调的有序性?比如你的三个线程同时完成了下载任务,回调到RecyclerView中,那样会同时调用adapter.notifyDataChanged(),这样也是不怎么科学的?
考虑到只是在一个RecyclerView中的数据,在第三条保证回调的有序性时,此时使用了一个SerialQueue序列,将任务线性排列执行,虽然牺牲了多线程的优势,但是为在Adapter执行notifyDataChanged()刷新数据不混乱,这种牺牲还是可以接受的。
这里使用了 第三方多线程下载框架:okdownload
基本下载框架如下:
/**
* created by microHx
* <p>
* 纵然万劫不复,纵然相思入骨,我依然待你眉眼如初,岁月如故。
* <p>
* date : 2019-07-09
* <p>
* version :
* <p>
* desc : Emoji下载管理工具类
*/
public class EmojiDownloadManager
/**
* 下载单例模式
*/
private static EmojiDownloadManager manager = new EmojiDownloadManager();
private EmojiDownloadManager()
public static EmojiDownloadManager getInstance() return manager;
/**
* 下载监听器集合
*/
private List<OnEmojiDownloadListener> mListContainer = new ArrayList<>();
/**
* 下载的url容器 避免重复下载
*/
private Set<String> mUUIdContainer = new HashSet<>();
/**
* 下载序列 execute task one by one
**/
private DownloadSerialQueue mSerialQueue = new DownloadSerialQueue(new DownloadListener2()
@Override
public void taskStart(@NonNull DownloadTask task)
BaseLog.i("taskStart : " + Thread.currentThread().getName() + "," + task.getUrl() + "," + task.getTag());
@Override
public void taskEnd(@NonNull DownloadTask task, @NonNull EndCause cause, @Nullable Exception realCause)
BaseLog.i("taskEnd:" + cause.name() + "," + realCause + "," + task.getTag() + "," + Thread.currentThread().getName());
String uuid = String.valueOf(task.getTag());
mUUIdContainer.remove(uuid);
if(BaseCommonUtils.checkCollection(mListContainer))
for(OnEmojiDownloadListener listener : mListContainer)
if(null != listener)
listener.onDownloadFinished(uuid, cause == EndCause.COMPLETED);
);
/**
*
* 下载管理
*
* @param uuid 唯一下载标识
* @param url 下载目标URL
* @param parentPath 文件目录地址
* @param fileName 存储文件名
*/
public void download(String uuid, String url , String parentPath , String fileName)
if(mUUIdContainer.contains(uuid)) return;
mUUIdContainer.add(uuid);
DownloadTask downloadTask = new DownloadTask.Builder(url, parentPath, fileName).
setPassIfAlreadyCompleted(true).
setMinIntervalMillisCallbackProcess(5000).
setWifiRequired(false).build();
downloadTask.setTag(uuid);
mSerialQueue.enqueue(downloadTask);
/**
* 注册下载监听器
* @param listener 监听器
* @param register 是否注册 true进行注册 false取消注册
*/
public void registerEmojiDownloadListener(OnEmojiDownloadListener listener, boolean register)
if(register)
if(!mListContainer.contains(listener))
mListContainer.add(listener);
else
mListContainer.remove(listener);
OnEmojiDownloadListener 回调比较简单:
/**
* created by microHx
* <p>
* 纵然万劫不复,纵然相思入骨,我依然待你眉眼如初,岁月如故。
* <p>
* date : 2019-11-29
* <p>
* version :
* <p>
* desc :
*/
public interface OnEmojiDownloadListener
/**
* 文件下载完成 回调
* @param uuid 文件唯一uuid
* @param result 下载结果 成功为true 失败为false
*/
void onDownloadFinished(String uuid, boolean result);
下载逻辑写好了,我们最重要的自定义ViewHolder核心代码也就贴一下:
/**
* created by microHx
* <p>
* 纵然万劫不复,纵然相思入骨,我依然待你眉眼如初,岁月如故。
* <p>
* date : 2019-11-27
* <p>
* version :
* <p>
* desc :
*/
public class CustomImageViewHolder extends MsgViewHolderBase
//最大的图片大小
private static final int MAX_IMAGE_WIDTH = (int) (ScreenUtil.screenWidth * 0.45f);
//最小自图片大小
private static final int MIN_IMAGE_WIDTH = (int) (ScreenUtil.screenWidth * 0.15f);
//需要加载的imageView
private GifImageView mImageView;
//下载等待pb
private ProgressBar mProgressbar;
public CustomImageViewHolder(BaseMultiItemFetchLoadAdapter adapter)
super(adapter);
@Override
protected int getContentResId()
return R.layout.item_custom_image_layout;
@Override
protected void inflateContentView()
mImageView = findViewById(R.id.id_custom_image);
mProgressbar = findViewById(R.id.id_pb);
@Override
protected void bindContentView()
CustomImageAttachment attachment = (CustomImageAttachment) message.getAttachment();
if(null != attachment)
String url = attachment.getUrl();
int targetWidth = attachment.getIntWidth();
int targetHeight = attachment.getIntHeight();
// 如果我们的目标宽度和高度同时存在
// 直接设置目标的宽度和高度
// 此时布局很省时
if(targetHeight > 0 && targetWidth > 0)
if(targetWidth > MAX_IMAGE_WIDTH)
targetWidth = MAX_IMAGE_WIDTH;
targetHeight = MAX_IMAGE_WIDTH * targetHeight / targetWidth;
if(targetWidth < MIN_IMAGE_WIDTH)
targetWidth = MIN_IMAGE_WIDTH;
targetHeight = MIN_IMAGE_WIDTH * targetHeight / targetWidth;
setLayoutParams(targetWidth,targetHeight, mImageView);
String fileName = MD5.getStringMD5("." + url);
File localFile = new File(SystemFileUtils.getGlobalDataPath(), fileName);
// 如果本msg已经被下载过了
if(IMessageManager.msgHasDownload(message))
//如果本地文件存在 且文件是 图片文件
if(BaseIOUtils.fileExist(localFile) && BaseIOUtils.fileIsImage(localFile))
mProgressbar.setVisibility(View.GONE);
mImageView.setImageURI(Uri.fromFile(localFile));
else
mProgressbar.setVisibility(View.GONE);
//文件已经下载过了 但是文件损坏了 不见了 此时我们就使用 兜底模式 使用Glide加载
BaseImageLoader.loadWithPlaceHolder(mImageView, url);
else
//如果本地文件存在 且文件是 图片文件
if(BaseIOUtils.fileExist(localFile) && BaseIOUtils.fileIsImage(localFile))
mProgressbar.setVisibility(View.GONE);
mImageView.setImageURI(Uri.fromFile(localFile));
else
mProgressbar.setVisibility(View.VISIBLE);
mImageView.setImageResource(R.drawable.ic_default_icon);
IMessageManager.clearDownloadTag(message);
//开启线程去下载url
EmojiDownloadManager.getInstance().download(message.getUuid(),url,SystemFileUtils.getGlobalDataPath(),fileName);
@Override
protected int leftBackground()
return 0;
@Override
protected int rightBackground()
return 0;
大致就这么多了,写得比较逻辑,如果有什么问题,请各位留言。
以上是关于Android基于云信实现微信斗图的主要内容,如果未能解决你的问题,请参考以下文章