Android基于云信实现微信斗图

Posted microhex

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android基于云信实现微信斗图相关的知识,希望对你有一定的参考价值。

总体概览

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

大致实现的功能如下:

  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_widthurl资源的宽度,这里是为了解决在Recylerview中快速滑动时不至于过于卡顿,我们通过明确控制表情包的大小直接写死布局的大小,避免使用wrap_content造成测量时耗时问题
img_heighturl资源的高度
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,需要满足的条件是:

  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() 
        @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基于云信实现微信斗图的主要内容,如果未能解决你的问题,请参考以下文章

基于微信小程序+爬虫制作一个表情包小程序

spider_爬取斗图啦所有表情包(图片保存)

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

python 表情包下载器,轻松下载上万个表情包斗图不用愁...

非常火的斗图表情包小程序源码

Java实现QQ微信轰炸机1.2(斗图乞丐版)