學問Chat UI(3)

前言

  • 上文學問Chat UI(2)分析了消息適配器的實現;
  • 本文主要學習下插件功能如何實現的.並以圖片插件功能做爲例子詳細說明,分析從具體代碼入手;

概要

  • 分析策略說明
  • 「+」功能UI佈局如何實現?分析總體思路與所用的哪些控件;
  • 分析DefaultExtensionModule與PluginAdapter兩個類
  • 圖片插件如何實現?

分析策略

  • 1.從融雲提供完整的demo,操做「+」按鈕,選擇圖片發送圖片消息;
  • 2.根據1的操做,尋找對應的控件與事件,理清邏輯;
  • 3.從總體把握,看如何實現插件功能;

「+」功能UI佈局如何實現

  • 從UI看是兩個部分:「+」按鈕與擴展面板,點擊會觸發事件,判斷擴展面板狀態,未顯示則顯示擴展面板,顯示狀態則隱藏擴展面板;
  • 代碼上mPluginToggle對象就是那個"+"按鈕,它是ImageView的實例,其中點擊會觸發RongExtension.this.setPluginBoard()方法;
this.mPluginToggle.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                if(RongExtension.this.mExtensionClickListener != null) {
                    RongExtension.this.mExtensionClickListener.onPluginToggleClick(v, RongExtension.this);
                }

                RongExtension.this.setPluginBoard();
            }
        });
  • 前面的是小菜,下面好好品下正菜,說明下後面全部代碼中出現的8與0,分別表明Gone(消失)與Visible(可見);
  • mPluginAdapter初始化狀態與mPluginAdapter中顯示狀態做爲主要判斷條件;
private void setPluginBoard() {
        if(this.mPluginAdapter.isInitialized()) {
            if(this.mPluginAdapter.getVisibility() == 0) {
              //省略部分代碼
            } else {
             //省略部分代碼
            }
        }  
    }
  • 看到這裏,咱們會有疑問,mPluginAdapter是用來幹什麼的?在解答這個疑問以前,先來看下DefaultExtensionModule類。

DefaultExtensionModule幹啥的

  • 英文翻譯下的意思默認的擴展功能模塊,實現了圖片,文件,地理位置3個基本插件;
  • DefaultExtensionModule實現了IExtensionModule接口,其中要重點講下onAttachedToExtension,onDetachedFromExtension,getPluginModules方法;
  • 1.簡單點說,onAttachedToExtension與onDetachedFromExtension負責管理其在RongExtension的生命週期,可是這裏有個問題會出現內存泄露;*
public void onAttachedToExtension(RongExtension extension) {
        this.mEditText = extension.getInputEditText();
        Context context = extension.getContext();
        RLog.i(TAG, "attach " + this.stack.size());
        //mEditText編輯框存放到stack棧對象中
        this.stack.push(this.mEditText);
        Resources resources = context.getResources();

        try {
            this.types = resources.getStringArray(resources.getIdentifier("rc_realtime_support_conversation_types", "array", context.getPackageName()));
        } catch (NotFoundException var5) {
            ;
        }

    }
    //判斷棧大小,若是大於0出棧,並返回mEditText
    public void onDetachedFromExtension() {
        RLog.i(TAG, "detach " + this.stack.size());
        if(this.stack.size() > 0) {
            this.stack.pop();
            this.mEditText = this.stack.size() > 0?(EditText)this.stack.peek():null;
        }

    }
  • 2.`getPluginModules方法`主要把功能插件對象--實現IPluginModule接口存放到ArrayList中,提供給外部使用;*
  • 須要說明的是地理位置在單聊的時候與羣聊功能略有不一樣,單聊多了位置共享的功能,那麼怎麼區別呢?經過ConversationType參數判斷;
  • 地理位置功能默認集成的是高德SDK,肯定AMapNetworkLocationClient存在後纔會把地理位置插件加到ArrayList;
public List<IPluginModule> getPluginModules(ConversationType conversationType) {
        ArrayList pluginModuleList = new ArrayList();
        ImagePlugin image = new ImagePlugin();
        FilePlugin file = new FilePlugin();
        pluginModuleList.add(image);

        String e;
        Class cls;
        try {
            //判斷高德定位服務類是否存在,存在的話根據ConversationType類型把位置共享插件與個人位置插件添加ArrayList中;
            e = "com.amap.api.netlocation.AMapNetworkLocationClient";
            cls = Class.forName(e);
            if(cls != null) {
                CombineLocationPlugin constructor = new CombineLocationPlugin();
                DefaultLocationPlugin recognizer = new DefaultLocationPlugin();
                boolean typesDefined = false;
                if(this.types != null && this.types.length > 0) {
                    String[] arr$ = this.types;
                    int len$ = arr$.length;

                    for(int i$ = 0; i$ < len$; ++i$) {
                        String type = arr$[i$];
                        if(conversationType.getName().equals(type)) {
                            typesDefined = true;
                            break;
                        }
                    }
                }

                if(typesDefined) {
                    pluginModuleList.add(constructor);
                } else if(this.types == null && conversationType.equals(ConversationType.PRIVATE)) {
                    pluginModuleList.add(constructor);
                } else {
                    pluginModuleList.add(recognizer);
                }
            }
        } catch (Exception var15) {
            RLog.i(TAG, "Not include AMap");
            var15.printStackTrace();
        }

        if(conversationType.equals(ConversationType.GROUP) || conversationType.equals(ConversationType.DISCUSSION) || conversationType.equals(ConversationType.PRIVATE)) {
            pluginModuleList.addAll(InternalModuleManager.getInstance().getExternalPlugins(conversationType));
        }

        pluginModuleList.add(file);
        //判斷科大訊飛sdk是否存在,存在的話經過反射實例化語音識別插件並加入到ArraryList中
        try {
            e = "com.iflytek.cloud.SpeechUtility";
            cls = Class.forName(e);
            if(cls != null) {
                cls = Class.forName("io.rong.recognizer.RecognizePlugin");
                Constructor var16 = cls.getConstructor(new Class[0]);
                IPluginModule var17 = (IPluginModule)var16.newInstance(new Object[0]);
                pluginModuleList.add(var17);
            }
        } catch (Exception var14) {
            RLog.i(TAG, "Not include Recognizer");
            var14.printStackTrace();
        }

        return pluginModuleList;
    }

關於PluginAdapter

  • 繼續上面提到的關於PluginAdapter的疑問,首先看下PluginAdapter這個類,代碼以下:
  • 這裏暫時不去關注網格效果實現方式,關注mInitialized布爾類型值與addPlugins方法
  • mInitialized值在bindView被寫入爲true,說明被初始化了,而addPlugins方法把DefaultExtensionModule的插件集合加到mPluginModules中,並在initView使用到;
public class PluginAdapter {
    private static final String TAG = "PluginAdapter";
    private List<IPluginModule> mPluginModules = new ArrayList();
    private boolean mInitialized;

    public PluginAdapter() {
    }

    public boolean isInitialized() {
        return this.mInitialized;
    }

     //省略部分方法

    public void addPlugins(List<IPluginModule> plugins) {
        for(int i = 0; plugins != null && i < plugins.size(); ++i) {
            this.mPluginModules.add(plugins.get(i));
        }
    }
     //省略部分方法
    public void bindView(ViewGroup viewGroup) {
        this.mInitialized = true;
        this.initView(viewGroup.getContext(), viewGroup);
    }

    private void initView(Context context, ViewGroup viewGroup) {
     //省略部分方法
    }

    public int getVisibility() {
        return this.mPluginPager != null?this.mPluginPager.getVisibility():8;
    }

     //省略部分代碼
}
  • 下面從RongExtension看PluginAdapter如何被使用?
    1.PluginAdapter在RongExtension的構造函數中被實例化,而後initPlugins方法把插件加到PluginAdapter對象中;
    2.接下來,重點分析是上面提到的setPluginBoard方法;長話多說,若是mPluginAdapter(插件適配器)未初始化,先進行初始化;
    不然,根據擴展面板是否顯示,顯示則隱藏鍵盤與擴展面板,隱藏的話顯示擴展面板並隱藏表面面板與鍵盤;最後要作的是,把語音輸入隱藏,mEditTextLayout佈局顯示;
private void setPluginBoard() {
        if(this.mPluginAdapter.isInitialized()) {
            if(this.mPluginAdapter.getVisibility() == 0) {
                View pager = this.mPluginAdapter.getPager();
                if(pager != null) {
                    pager.setVisibility(pager.getVisibility() == 8?0:8);
                } else {
                    this.mPluginAdapter.setVisibility(8);
                    this.mContainerLayout.setSelected(true);
                    this.showInputKeyBoard();
                }
            } else {
                this.mEmoticonToggle.setImageResource(drawable.rc_emotion_toggle_selector);
                if(this.isKeyBoardActive()) {
                    this.getHandler().postDelayed(new Runnable() {
                        public void run() {
                            RongExtension.this.mPluginAdapter.setVisibility(0);
                        }
                    }, 200L);
                } else {
                    this.mPluginAdapter.setVisibility(0);
                }

                this.hideInputKeyBoard();
                this.hideEmoticonBoard();
                this.mContainerLayout.setSelected(false);
            }
        } else {
            this.mEmoticonToggle.setImageResource(drawable.rc_emotion_toggle_selector);
            this.mPluginAdapter.bindView(this);
            this.mPluginAdapter.setVisibility(0);
            this.mContainerLayout.setSelected(false);
            this.hideInputKeyBoard();
            this.hideEmoticonBoard();
        }

        this.hideVoiceInputToggle();
        this.mEditTextLayout.setVisibility(0);
    }

圖片插件如何實現?

  • 前面的內容爲後面理解圖片插件的實現提供了鋪墊,上面的getPluginModules方法提到的ImagePlugin類是講解的重點;
  • 在看ImagePlugin以前先來看下DefaultExtensionModule中的插件如何與PluginAdapter關聯起來的?
DefaultExtensionModule中的插件如何與PluginAdapter關聯

1 1.點擊「+」的時候插件功能已經可使用了,那麼說明在聊天界面渲染以前插件已經被創建起來,很容易,想到初始化聊天IM服務是最好的時機;html

//調用RongIM的public靜態init方法,參數呢是實例化的DefaultExtensionModule
RongExtensionManager.getInstance().registerExtensionModule(new DefaultExtensionModule());
  • RongExtensionManager中有一個List 對象,簡單點說,調用registerExtensionModule就是把對象加入到List 中;
public void registerExtensionModule(IExtensionModule extensionModule) {
        if(mExtModules == null) {
            RLog.e("RongExtensionManager", "Not init in the main process.");
        } else if(extensionModule != null && !mExtModules.contains(extensionModule)) {
            RLog.i("RongExtensionManager", "registerExtensionModule " + extensionModule.getClass().getSimpleName());
            if(mExtModules.size() <= 0 || !((IExtensionModule)mExtModules.get(0)).getClass().getCanonicalName().equals("com.jrmf360.rylib.modules.JrmfExtensionModule") && !((IExtensionModule)mExtModules.get(0)).getClass().getCanonicalName().equals("com.melink.bqmmplugin.rc.BQMMExtensionModule")) {
                mExtModules.add(extensionModule);
            } else {
                mExtModules.add(0, extensionModule);
            }

            extensionModule.onInit(mAppKey);
        } else {
            RLog.e("RongExtensionManager", "Illegal extensionModule.");
        }
    }

2 2.再看RongExtension的initData方法,把RongExtensionManager中的List 對象賦值給mExtensionModuleList,並實例化了PluginAdapter--插件適配器類 android

private void initData() {
        this.mExtensionModuleList = RongExtensionManager.getInstance().getExtensionModules();
        this.mPluginAdapter = new PluginAdapter();//省略若干代碼
}

3 3.再看RongExtension的setConversation方法調用this.initPlugins(),噹噹噹的,調用了實例化插件對象的addPlugins把插件加入到其中,從而造成關聯;git

private void initPlugins() {
        Iterator i$ = this.mExtensionModuleList.iterator();

        while(i$.hasNext()) {
            IExtensionModule module = (IExtensionModule)i$.next();
            List pluginModules = module.getPluginModules(this.mConversationType);
            if(pluginModules != null && this.mPluginAdapter != null) {
                this.mPluginAdapter.addPlugins(pluginModules);
            }
        }

    }
ImagePlugin
  • ImagePlugin實現了IPluginModule接口,總共四個方法,代碼以下:
public interface IPluginModule {
    Drawable obtainDrawable(Context var1);

    String obtainTitle(Context var1);

    void onClick(Fragment var1, EditExtension var2);

    void onActivityResult(int var1, int var2, Intent var3);
}
  • 看到這裏你可能對四個方法是幹什麼產生疑問?彆着急,欲知此事,請往下閱讀;
  • 貼上ImagePlugin的具體代碼,這裏看具體實現的代碼,請看代碼中註釋;
public class ImagePlugin implements IPluginModule {
    ConversationType conversationType;
    String targetId;

    public ImagePlugin() {
    }

    //item的背景圖片
    public Drawable obtainDrawable(Context context) {
        return ContextCompat.getDrawable(context, R.drawable.rc_ext_plugin_image_selector);
    }
    //item的插件標題
    public String obtainTitle(Context context) {
        return context.getString(R.string.rc_plugin_image);
    }
    //item點擊事件
    public void onClick(Fragment currentFragment, EditExtension extension) {
        String[] permissions = new String[]{"android.permission.READ_EXTERNAL_STORAGE"};
        //這裏考慮android6.0權限變動,不只須要聲明權限,並且敏感權限須要容許時申請
        if(PermissionCheckUtil.requestPermissions(currentFragment, permissions)) {
            this.conversationType = extension.getConversationType();
            this.targetId = extension.getTargetId();
            Intent intent = new Intent(currentFragment.getActivity(), PictureSelectorActivity.class);
            //回調Fragment 中的onActivityResult
            extension.startActivityForPluginResult(intent, 23, this);
        }
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
    }
}
  • 下面看下ConversationFragment選完圖片之後回調如何進行?
  • 首先對requestCode作了判斷,若是不是102則回調了mRongExtension對象的onActivityPluginResult方法,而後根據請求代碼分析是哪一個插件回調回來的,在調用IExtensionClickListener接口對應的方法;
public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(requestCode == 102) {
            this.getActivity().finish();
        } else {
            this.mRongExtension.onActivityPluginResult(requestCode, resultCode, data);
        }

    }
  • 這裏有幾個問題?onActivityResult可不能夠直接處理?requestCode 的如何做用?
    1 第一個問題,能夠進行直接數據處理,可是須要約定好requestCode,若是經過融雲回調的話不須要約定;
    2 第二個問題,單獨一個int類型的值容納得信息有限,作過處理的就不同凡響了,融雲的方法是把後8位做爲requestCode,前24位做爲postion,爲什麼要+1不是很懂,有知道請再評論中指出;
this.mFragment.startActivityForResult(intent, (position + 1 << 8) + (requestCode & 255));
  • 上述代碼完成之後能夠經過回調ConversationFragment實現的`this.mExtensionClickListener.onImageResult(list, lat1);`方法發送圖片消息了,代碼就不貼了;

總結

  • 插件實現經過接口方式,耦合度下降,擴展性好;
  • 添加插件時,無需大改RongExtension代碼只要實現IPluginModule接口並註冊到實現IExtensionModule的插件模塊中,並在初始化RongIM時註冊插件模塊;
  • 考慮功能的時候須要考慮到兼容性,擴展性;
相關文章
相關標籤/搜索