因爲項目須要,最近開始借鑑學習下開源的Android即時通訊聊天UI框架,爲此結合市面上加上本項目需求列了ChatUI要實現的基本功能與擴展功能。node
爲了實現業務與UI分離,分析融雲UI部分代碼,下面主要從IMKit下的ConversationFragment,RongExtension,plugin包下類實現插件化;android
簡單點來說,上面是一個LIstView,下面是個帶有EditText擴展橫向佈局器--輸入聊天框,以下圖所示,相對佈局中存在兩個id爲:rc_layout_msg_list與rc_extension的佈局;
本文的重點是分析輸入聊天框以及擴展功能插件的代碼,涉及到IMLib的代碼會跳過,更好的分析UI是如何實現的;框架
直觀的來看佈局,它有4個部分組成,語音按鈕,輸入框,表情按鈕,擴展按鈕;ide
四個控件點擊事件須要控制其餘控件的顯示與隱藏,簡化圖以下:佈局
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) { return inflate(parser, root, root != null); } public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); if (DEBUG) { Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" (" + Integer.toHexString(resource) + ")"); } final XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot);//核心方法 } finally { parser.close(); } }
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate"); final Context inflaterContext = mContext; final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context) mConstructorArgs[0]; mConstructorArgs[0] = inflaterContext; View result = root; try { // Look for the root node. int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } if (type != XmlPullParser.START_TAG) { throw new InflateException(parser.getPositionDescription() + ": No start tag found!"); } final String name = parser.getName(); if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { throw new InflateException("<merge /> can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } rInflate(parser, root, inflaterContext, attrs, false); } else { // 關注下面一部分代碼 // Temp is the root view that was found in the xml final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; if (root != null) { if (DEBUG) { System.out.println("Creating params from root: " + root); } // Create layout params that match root, if supplied params = root.generateLayoutParams(attrs); if (!attachToRoot) { // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) temp.setLayoutParams(params); } } // Inflate all children under temp against its context. rInflateChildren(parser, temp, attrs, true); // We are supposed to attach all the views we found (int temp) // to root. Do that now. xml佈局添加到root返回xml佈局 if (root != null && attachToRoot) { root.addView(temp, params); } // Decide whether to return the root that was passed in or the // top view found in xml.result 賦值爲xml佈局對象 if (root == null || !attachToRoot) { result = temp; } } } catch (XmlPullParserException e) { InflateException ex = new InflateException(e.getMessage()); ex.initCause(e); throw ex; } catch (Exception e) { InflateException ex = new InflateException( parser.getPositionDescription() + ": " + e.getMessage()); ex.initCause(e); throw ex; } finally { // Don't retain static reference on context. mConstructorArgs[0] = lastContext; mConstructorArgs[1] = null; } Trace.traceEnd(Trace.TRACE_TAG_VIEW); return result; } }
layout.rc_ext_voice_input是錄取語音按鈕佈局,調用的方法是inflate(R.layout.rc_ext_voice_input, this.mContainerLayout, true)
,根據上面可知這個效果是把錄取語音按鈕加到mContainerLayout中並返回mContainerLayout對象,那麼如今存在兩個孩子View,由於Framelayout佈局中存在多個子控件會覆蓋,所以這個先隱藏起來學習
this.mExtensionBar = (ViewGroup)LayoutInflater.from(this.getContext()).inflate(R.layout.rc_ext_extension_bar, (ViewGroup)null); this.mMainBar = (LinearLayout)this.mExtensionBar.findViewById(R.id.ext_main_bar); this.mSwitchLayout = (ViewGroup)this.mExtensionBar.findViewById(R.id.rc_switch_layout); this.mContainerLayout = (ViewGroup)this.mExtensionBar.findViewById(R.id.rc_container_layout); this.mPluginLayout = (ViewGroup)this.mExtensionBar.findViewById(R.id.rc_plugin_layout); this.mEditTextLayout = LayoutInflater.from(this.getContext()).inflate(R.layout.rc_ext_input_edit_text, (ViewGroup)null); this.mEditTextLayout.setVisibility(VISIBLE); this.mContainerLayout.addView(this.mEditTextLayout); LayoutInflater.from(this.getContext()).inflate(R.layout.rc_ext_voice_input, this.mContainerLayout, true); this.mVoiceInputToggle = this.mContainerLayout.findViewById(R.id.rc_audio_input_toggle); this.mVoiceInputToggle.setVisibility(GONE); //省略若干代碼 this.addView(this.mExtensionBar);
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="48dp"> <LinearLayout android:id="@+id/ext_main_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" android:gravity="center_vertical" android:orientation="horizontal" android:paddingLeft="6dp" android:paddingRight="6dp"> <!-- 「語音」 「公衆號菜單」 佈局--> <LinearLayout android:id="@+id/rc_switch_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal"> <ImageView android:id="@+id/rc_switch_to_menu" android:layout_width="41dp" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginRight="6dp" android:scaleType="center" android:src="@drawable/rc_menu_text_selector" android:visibility="gone"/> <View android:id="@+id/rc_switch_divider" android:layout_width="1px" android:layout_height="48dp" android:background="@color/rc_divider_line" android:visibility="gone"/> <ImageView android:id="@+id/rc_voice_toggle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="6dp" android:layout_marginRight="6dp" android:src="@drawable/rc_voice_toggle_selector"/> </LinearLayout> <!-- 文本,表情輸入容器,經過控制「語音」,容器中填充不一樣的內容 --> <FrameLayout android:id="@+id/rc_container_layout" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:paddingTop="3dp" android:paddingBottom="3dp" android:gravity="center_vertical"/> <!-- 擴展欄 「+號」 佈局--> <LinearLayout android:id="@+id/rc_plugin_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="6dp" android:layout_marginRight="6dp" android:orientation="horizontal"> <ImageView android:id="@+id/rc_plugin_toggle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/rc_plugin_toggle_selector"/> </LinearLayout> </LinearLayout> <!-- 底部分割線 --> <View android:layout_width="match_parent" android:layout_height="2px" android:layout_alignParentTop="true" android:background="@color/rc_divider_color"/> </RelativeLayout>
//文本框輸入以前 public void beforeTextChanged(CharSequence s, int start, int count, int after); //文本框正在輸入 public void onTextChanged(CharSequence s, int start, int before, int count); //文本框輸入後 public void afterTextChanged(Editable s);
public interface IExtensionClickListener extends TextWatcher { //發送按鈕回調事件 void onSendToggleClick(View var1, String var2); //發送圖片回調事件 void onImageResult(List<Uri> var1, boolean var2); //發送地理位置回調事件 void onLocationResult(double var1, double var3, String var5, Uri var6); //語音按鈕切換回調事件 void onSwitchToggleClick(View var1, ViewGroup var2); //聲音按鈕觸摸回調事件 void onVoiceInputToggleTouch(View var1, MotionEvent var2); //表情按鈕點擊回調事件 void onEmoticonToggleClick(View var1, ViewGroup var2); //‘+’按鈕點擊回調事件 void onPluginToggleClick(View var1, ViewGroup var2); void onMenuClick(int var1, int var2); //文本框點擊回調事件 void onEditTextClick(EditText var1); //回調setOnKeyListener事件 boolean onKey(View var1, int var2, KeyEvent var3); void onExtensionCollapsed(); void onExtensionExpanded(int var1); //插件Item點擊回調事件 void onPluginClicked(IPluginModule var1, int var2); }