因为项目须要,最近开始借鉴学习下开源的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); }