总体概览

最近项目出现了一个新的需求,需要实现类似微信的表情的斗图功能。由于我们是一家基于互联网+装修的公司,为了给业主创造一个好的印象和营造开工、完工的美好气象,这个需求我们还是默默的接了下来,虽然我们知道坑那是多得一比。下面我们就简单说明一下我们的方案。
已经实现的功能大概是这样的:

大致实现的功能如下:

  1. 服务器动态配置斗图的类型和风格。因为每家公司的主题和风格样式都是不一样的,这里需要后台配置,服务器有什么图,客户端才显示什么图。特别的节日气息我们可以增删一些图片。
  2. 斗图可以是jpg、png和gif图片,大小可是控制,但需要提供图片的尺寸(实际的宽和高)。
  3. 客户端按服务器端动态改变斗图样式。
  4. 客户端可以播放jpg、png和gif动图。

服务器端准备

我们的思路大概如下图所示:

大致的逻辑如下:

  1. 客户端首先请求服务器斗图md5值,这个斗图md5是根据斗图表情文件计算出来的。之所以要请求这个这个md5值,有以下几个目的:如果修改了斗图表情包里面的内容,像你新增了某些表情包,删除了某些表情包,那么表情包文件的md5值就会变化,我们客户端通过比较如果本地的md5值和服务器的md5值不一样,我们就知道了表情包更新了,就需要重新下载新的表情包了,比较简单,同时也很快捷。
  2. 通过比较新旧md5值,我们就可以判断是否需要下载服务器的表情包了。因为这个表情包一般都比较大,所以如果本地的表情包存在,而且没有损坏,能够加载出表情的内容,那我们直接使用本地的,不用直接请求就可以了。
  3. 如果本地不存在,我们可以看出,先是下载我们的表情包文件,此时的文件我们定义为json格式的,可以通过Gson或者FastJson等第三方工具直接构建我们的JavaBean对象,也是十分快的。下载完表情包文件之后,先存储,然后更新本的md5值,最后构建成我们新的java对象,供云信调用。

客户端

这里主要是大致讲一下Android的具体实现,IOS客户端大致上也是类似的相同模式。

自定义消息

首先我们使用的是云信,官网,猪场最近名声不怎么好,也没什么办法,主要是这个项目两年前就已经用云信了,综合起来看,效果还是可以的。话不多扯,我们也是集成了云信开源的第三方组件库UIKit, 这个bug还是蛮多的,这里不说了,各位集成了的,都希望自求多福了。
为了实现可以斗图功能,目前云信的几个消息类型显然是不能满足的。我们首先需要明确的是:

  1. 斗图其实就是显示一张图片,这张图片的url是我们服务器下发的,它可能是jpg、png或者gif。
  2. 我们需要合适的组件可以显示jpg、png和gif等不同图片类型。
  3. 在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";//自定义图片urlprivate String url ;//类型public String type ;//描述public String name ;//自定义表情宽度public String width;//自定义表情高度public String height;public CustomImageAttachment() {super(CustomAttachmentType.CUSTOM_IMAGE);}@Overrideprotected 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;}@Overrideprotected 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) { //gifGlide.load(attachment.getUrl()).asGif().info(id_iv_image);  }else { //png jpg and other formatGlide.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,需要满足的条件是:

  1. 针对同一个url需要自动过滤,相同的下载资源直接pass。
  2. 每个下载任务需要有一个TaskTag,通过下载TaskTag可以知道是哪一个url下发的任务,这样就能资源混乱。
  3. 多线程下下载,怎么才能保证回调的有序性?比如你的三个线程同时完成了下载任务,回调到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() {@Overridepublic void taskStart(@NonNull DownloadTask task) {BaseLog.i("taskStart : " + Thread.currentThread().getName() + "," + task.getUrl() + "," + task.getTag());}@Overridepublic 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);//需要加载的imageViewprivate GifImageView mImageView;//下载等待pbprivate ProgressBar  mProgressbar;public CustomImageViewHolder(BaseMultiItemFetchLoadAdapter adapter) {super(adapter);}@Overrideprotected int getContentResId() {return R.layout.item_custom_image_layout;}@Overrideprotected void inflateContentView() {mImageView = findViewById(R.id.id_custom_image);mProgressbar = findViewById(R.id.id_pb);}@Overrideprotected 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);
//开启线程去下载urlEmojiDownloadManager.getInstance().download(message.getUuid(),url,SystemFileUtils.getGlobalDataPath(),fileName);}}}}@Overrideprotected int leftBackground() {return 0;}@Overrideprotected int rightBackground() {return 0;}
}

大致就这么多了,写得比较逻辑,如果有什么问题,请各位留言。

Android基于云信实现微信斗图相关推荐

  1. 基于Android输入法开发,制作一个微信斗图APP

    本文字数:5191字 预计阅读时间:20分钟 目录: 1 导读: 2 Android 输入法开发简介及流程: 3 斗图 APP 开发介绍: 4 斗图 APP 功能优化: 5 总结. 01 导读 微信斗 ...

  2. android edittext_基于Android输入法开发,制作一个微信斗图APP

    刘望舒 读完需要 20分钟 速读仅需12分钟 作者:小学生° 来源:搜狐技术产品 01 导读 微信斗图的应用有很多,但大部分都是通过微信分享来实现的,需下载 APP,下载表情并分享到微信联系人,操作步 ...

  3. android studio开发app实例_基于Android输入法开发,制作一个微信斗图APP

    刘望舒 读完需要 20分钟 速读仅需12分钟 作者:小学生° 来源:搜狐技术产品 01 导读 微信斗图的应用有很多,但大部分都是通过微信分享来实现的,需下载 APP,下载表情并分享到微信联系人,操作步 ...

  4. 这样制作一个Android输入法开发微信斗图APP,斗图就再也没输过!

    目录: 1.导读: 2.Android 输入法开发简介及流程: 3.斗图 APP 开发介绍: 4.斗图 APP 功能优化: 5.总结 1.导读 微信斗图的应用有很多,但大部分都是通过微信分享来实现的, ...

  5. Python爬取20w+表情包,微信斗图谁怕谁!

    源 / python教程       文 / 小雨 有一次想要斗图 就在网上搜索表情包 然后发现了一个表情巨多的网站 不小心动起了邪念 产生了兴趣 那就 把它们存下来吧 用 requests 请求了一 ...

  6. 输入法斗图发送微信以及qq实现参考资料

    (20条消息) android 微信输入法表情,分析Android 搜狗输入法在微信和QQ中发送图片和表情_whatis真实的博客-CSDN博客 (20条消息) 这样制作一个Android输入法开发微 ...

  7. 微信群「斗图」总输,Python助我超神!

    微信斗图 微信斗图,大家最喜欢的就是发布Gif动图了.它的原理其实很简单,就是一帧一帧的图片拼接在一起,组合而成的.当他们来回切换的速度很快时,就相当于在看电影. 假如有这样一组图片: 如果制作完成后 ...

  8. 微信群「斗图」总输,用Python教你做最靓的仔!!!

    本文转自:数据分析与统计学之美 微信斗图 微信斗图,大家最喜欢的就是发布Gif动图了.它的原理其实很简单,就是一帧一帧的图片拼接在一起,组合而成的.当他们来回切换的速度很快时,就相当于在看电影. 假如 ...

  9. 深夜,我用python爬取了整个斗图网站,不服来斗

    深夜,我用python爬取了整个斗图网站,不服来斗 QQ.微信斗图总是斗不过,索性直接来爬斗图网,我有整个网站的图,不服来斗. 废话不多说,选取的网站为斗图啦,我们先简单来看一下网站的结构 网页信息 ...

最新文章

  1. 今天去团省委维护网站
  2. 给IT新人的15个建议:苦逼程序员的辛酸反省与总结
  3. 基于modelsim的十个Verilog入门试验程序(1)(7人表决器+算术逻辑单元)—程序+测试代码+波形+结果分析
  4. 恭祝月圆·家圆·团圆
  5. SQL Server 2012笔记分享-28:使用日志文件查看器
  6. vue项目中使用cn打印组件
  7. eplan连接定义点不显示_EPLAN电气图实例-控制回路(电机回路6)
  8. Navicat Premium 15破解
  9. Power bi_商品销售案例分析
  10. 游戏ui设计,游戏ui设计需要学什么
  11. python四大数据类型——python筑基系列
  12. uni-app 161朋友圈查看权限
  13. direct show
  14. 百度地图BMap API的应用实例
  15. html写大学课程表
  16. SEO整站优化方案制作
  17. 还说Facebook创始人扎克伯格开飞度?这回被打脸了
  18. 什么是BOS? BOS 与 ERP 是什么关系?
  19. 关于MathType生成的公式插入Word显示错误的原因和处理
  20. matlab开机自动启动的设置

热门文章

  1. Switch下载免费游戏Asphalt9
  2. 微信封号的原因有几个?
  3. 电子设计教程1:电容充放电指示电路
  4. 集成电路中的ESD防护,一篇入门
  5. 如何让自己脚本只打其中一行_如果我在其中,自己拥有照片吗?
  6. 大型商贸系统(进货管理)技术解析(二)自营进仓单录入
  7. Android开发错误——Could not find class
  8. matlab 画涡量场,起旋器内部的流速场和涡量场特性
  9. vue pc移动页面自适应
  10. html控制台源码,可以在浏览器控制台中执行源码字符动画的js插件