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_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,需要滿足的條件是:

  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;
    }
}

大致就這麼多了,寫得比較邏輯,如果有什麼問題,請各位留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章