Android开发——即时通讯中自定义聊天页面封装实现过程记录
说明
本人大四党,菜鸟,毕业设计有即时通讯的部分,目前即时通讯的模块基本完成,记录一下在Android平台下实现整个即时通讯界面功能的过程,希望对大家有点帮助。
代码和博客都是热乎的!!
功能点
- 适应键盘高度
- 自定义灵活的功能面板
- 表情消息
- 语音消息
- 图片消息
话不多说系列
关键代码
底部聊天栏管理器ChatBarManager
/**
* 聊天底部栏管理器
* @author 赵陈淏
*/
public class ChatBarManager implements ChatBar.ChatBarAdapter
{
//功能/表情面板最大和最小高度限制
private static final int MIN_HEIGHT=720;
private static final int MAX_HEIGHT=950;
private Activity mActivity;
private ResizeLayout resizeLayout;
private ChatBar chatBar;
private int softKeyBoardHeight=0;
private InputMethodManager inputManager;
public ChatBarManager(Activity activity,ChatBar bar,ResizeLayout root)
{
this.mActivity=activity;
this.chatBar=bar;
this.resizeLayout=root;
inputManager=(InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE);
//这里对根部局加上尺寸调整监听,得以对键盘弹起时的高度进行记录
resizeLayout.setOnResizeListener(new ResizeLayout.OnResizeListener() {
@Override
public void OnResize(int w, int h, int oldw, int oldh) {
if (oldw != 0 && oldh != 0) {
if (h < oldh) {
int cur = oldh - h;
cur = Math.max(MIN_HEIGHT,
cur);
cur=Math.min(cur,MAX_HEIGHT);
if (cur!=softKeyBoardHeight)
{
softKeyBoardHeight=cur;
Log.e("CHAT","检测到键盘高度为"+softKeyBoardHeight);
chatBar.setFuncPanelHeight(softKeyBoardHeight);
}
}
}
}
});
chatBar.setChatBarAdapter(this);
}
/**
* 设置自定义的功能
*/
public void setUpFunctions(FunctionPanel.ChatFunction[] functions)
{
chatBar.setUpFunctions(functions);
}
/**
* 设置功能点击监听
* @param functionClickListener 功能监听
*/
public void setFunctionClickListener(FunctionPanel.FunctionClickListener functionClickListener) {
chatBar.setFunctionClickListener(functionClickListener);
}
@Override
public void sendText(CharSequence text) {
ToastUtils.showToast(mActivity.getApplicationContext(),"消息",text.toString());
}
@Override
public void funcPanelShow(boolean isShow) {
showFuncPanel(isShow);
}
/**
* 收起键盘与功能栏
* 仅用于
*/
public void hideSoftInput()
{
Log.e("Manager","调用隐藏键盘");
inputManager.hideSoftInputFromWindow(chatBar.getSendText().getWindowToken(), 0);
if (chatBar.isFuncPanelShow())
{
hideFuncPanelDelayed();
}
}
/**
* 显示/关闭 功能面板
* @param isShow 显示/关闭
*/
private void showFuncPanel(boolean isShow)
{
Log.e("Manager","调用了show"+isShow);
if (isShow)
{
mActivity.getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
inputManager.hideSoftInputFromWindow(chatBar.getSendText().getWindowToken(), 0);
chatBar.setFuncPanelVisible(View.VISIBLE);
} else {
inputManager
.showSoftInput(chatBar.getSendText(), InputMethodManager.HIDE_NOT_ALWAYS);
hideFuncPanelDelayed();
}
}
/**
* 延迟隐藏功能面板
*/
private void hideFuncPanelDelayed()
{
Log.e("ChatBarManager","延迟隐藏");
//这里要delayed执行否则依然会跳动,因为隐藏面板的同时要弹出键盘,
// 弹出键盘有个延迟,如果在弹出键盘前执行的话就会造成跳动
chatBar.postDelayed(()-> {
chatBar.setFuncPanelVisible(View.GONE);
mActivity.getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
chatBar.getSendText().requestFocus();
},300);
}
}
实现的功能面板类FunctionPanel.java
这里利用ViewPager+Fragment和public void setUpFunctions(ChatFunction[] functions)
装载功能项目后,计算功能数量并自动分页、适应布局。
其中的内部类ChatFunction
的成员变量:
-funcResId
-funcText
-funcFlag
分别用来代指功能面板的图片资源id、功能描述和标志int,Flag用来监听其点击事件
功能面板FunctionPanel中涵盖了FacePanel,通过设置两个ViewGroup的可见性来做切换。
其实更合理的设计是对两个类别的面板再作分离 :
- 底部面板
- 表情面板
- 功能面板
我这里的设计是:
- 功能面板
- 表情面板:FacePanel
- LinearLayout(子控件:ViewPager+FuncPointer)
其实是懒得改(小声BB),下面是功能面板代码
package com.biang.manage.widget.chat.func;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.GridLayout;
import android.widget.LinearLayout;
import com.biang.manage.R;
import com.biang.manage.core.base.BaseActivity;
import com.biang.manage.core.base.BaseChatActivity;
import com.biang.manage.core.base.BaseFragment;
import com.biang.manage.widget.chat.FacePanel;
import com.biang.manage.widget.common.iconbutton.IconButton;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
public class FunctionPanel extends LinearLayout
{
private FunctionClickListener functionClickListener;
private FacePanel facePanel;
private View functionPanel;
private ViewPager funcPager;
private FuncPointer mPointer;
public static final int FACE=0;
public static final int FUNC=1;
private int witchPanel=0;
private BaseChatActivity mActivity;
/**
* 切换面板
*/
public void setWitchPanel(int witchPanel)
{
if (witchPanel!=0&&witchPanel!=1)
return;
this.witchPanel=witchPanel;
switch (witchPanel)
{
case FACE:
facePanel.setVisibility(VISIBLE);
functionPanel.setVisibility(GONE);
break;
case FUNC:
facePanel.setVisibility(GONE);
functionPanel.setVisibility(VISIBLE);
break;
}
}
public FunctionPanel(Context context) {
super(context);
initView(context);
}
public FunctionPanel(Context context, AttributeSet set) {
super(context,set);
initView(context);
}
/**
* 设置表情点击监听
*/
public void setOnFaceClickListener(FacePanel.OnFaceClickListener listener) {
facePanel.setOnFaceClickListener(listener);
}
/**
* 设置功能
*/
public void setUpFunctions(ChatFunction[] functions)
{
List<FuncFragment> fragments=new ArrayList<>();
List<ChatFunction>list=new ArrayList<>();
for (int i=0;i<functions.length;i++)
{
if (i!=0&&i%8==0)
{
ChatFunction[] fs=new ChatFunction[list.size()];
list.toArray(fs);
fragments.add(FuncFragment.getInstance(fs,functionClickListener));
list=new ArrayList<>();
}
list.add(functions[i]);
}
ChatFunction[] fs=new ChatFunction[list.size()];
list.toArray(fs);
fragments.add(FuncFragment.getInstance(fs,functionClickListener));
Log.e("FuncPanel","装载了所有功能");
mPointer.setCount(fragments.size());
funcPager.setAdapter(new FuncFragmentAdapter(mActivity.getSupportFragmentManager(),fragments));
funcPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }
@Override
public void onPageSelected(int position) {
mPointer.point(position);
}
@Override
public void onPageScrollStateChanged(int state) { }
});
funcPager.setCurrentItem(0);
}
/**
* 初始化布局
*/
private void initView(Context context)
{
View view=LayoutInflater.from(context).inflate(R.layout.item_function_panel,this);
functionPanel=view.findViewById(R.id.func_func);
funcPager=view.findViewById(R.id.func_func_view_pager);
mActivity=(BaseChatActivity) view.getContext();
//funcPager.setAdapter(new FuncFragmentAdapter(activity.getSupportFragmentManager(),));
facePanel=view.findViewById(R.id.func_face);
mPointer=view.findViewById(R.id.func_func_pointer);
}
public void setFunctionClickListener(FunctionClickListener functionClickListener) {
this.functionClickListener = functionClickListener;
}
/**
* 获取当前显示面板
*/
public int getWitchPanel() {
return witchPanel;
}
public interface FunctionClickListener
{
void onFuncClick(ChatFunction function);
}
/**
* 私有类
* 实现fragment——viewpager切换
*/
private static class FuncFragmentAdapter extends FragmentPagerAdapter
{
private List<FuncFragment> mList;
private FuncFragmentAdapter(FragmentManager manager, List<FuncFragment> list) {
super(manager,FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
mList=list;
}
@NonNull
@Override
public Fragment getItem(int position) {
return this.mList == null ? new Fragment() : this.mList.get(position);
}
@Override
public int getCount() {
return this.mList == null ? 0 : this.mList.size();
}
}
public static class ChatFunction implements Serializable
{
public int funcFlag;
public String funcText;
public int funcResId;
}
}
功能页Fragment——FuncFragment,BaseFragment的实现和BaseActivity类似就是把布局文件设置和集中代码段分开了而已,不作赘述。
FuncFragment通过上述FuncPanel中的ViewPager来展示。
这里需要注意在使用GridLayout来适应我们灵活的功能项目时,为了避免布局错误,例如我们设想的功能项布局为4*2,当不足8个或不足4个时,还是像摆满一样挨个摆放,但是我们对GridLayout子控件设置了rowWeight和columnWeight相等,不足4个则会平均摆放。
为了解决这个问题我们需要在此功能页面不足4个的时候,填充INVISIBLE的控件,代码如下:
/**
* 功能面板fragment
* @author 赵陈淏
*/
public class FuncFragment extends BaseFragment
{
private static final int MAX_SIZE=8;
private static final int MIN_SIZE=4;
private FunctionPanel.FunctionClickListener listener;
@Override
protected void handleMsg(Message msg) {}
public static FuncFragment getInstance(FunctionPanel.ChatFunction[] functions, FunctionPanel.FunctionClickListener listener)
{
FuncFragment fragment=new FuncFragment(listener);
Bundle args=new Bundle();
args.putSerializable("functions",functions);
fragment.setArguments(args);
return fragment;
}
private FuncFragment(FunctionPanel.FunctionClickListener clickListener)
{
listener=clickListener;
}
@Override
protected int getContentViewId() {
return R.layout.frag_func_panel;
}
@Override
protected void initView(View root) {
GridLayout gridLayout = root.findViewById(R.id.frag_func_grid);
Bundle args=getArguments();
if (args!=null)
{
Object functions=args.getSerializable("functions");
if (functions instanceof FunctionPanel.ChatFunction[])
{
FunctionPanel.ChatFunction[] fs=(FunctionPanel.ChatFunction[])functions;
//为了布局的正确性,在这里需要注意,一页中不足4个功能项的页面,需要加入invisible无效项保证布局正确
for (int i=0;i<fs.length&&i<MAX_SIZE;i++)
{
IconButton btn=new IconButton(getContext());
btn.setType(IconButton.TYPE_RECT_ICON);
btn.setIcon(fs[i].funcResId);
btn.setText(fs[i].funcText);
int finalI = i;
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (listener!=null)
listener.onFuncClick(fs[finalI]);
}
});
//使用Spec定义子控件的位置和比重
GridLayout.Spec rowSpec = GridLayout.spec(i / 4, 1f);
GridLayout.Spec columnSpec = GridLayout.spec(i % 4, 1f);
GridLayout.LayoutParams param= new GridLayout.LayoutParams(rowSpec,columnSpec);
param.height= GridLayout.LayoutParams.WRAP_CONTENT;
param.width=GridLayout.LayoutParams.WRAP_CONTENT;
gridLayout.addView(btn,param);
}
//补充空项,使得布局正确
if (fs.length<MIN_SIZE)
{
for (int i=fs.length;i<MIN_SIZE;i++)
{
IconButton btn=new IconButton(getContext());
btn.setVisibility(View.INVISIBLE);
btn.setText("");
btn.setType(IconButton.TYPE_RECT_ICON);
//使用Spec定义子控件的位置和比重
GridLayout.Spec rowSpec = GridLayout.spec(i / 4, 1f);
GridLayout.Spec columnSpec = GridLayout.spec(i % 4, 1f);
GridLayout.LayoutParams param= new GridLayout.LayoutParams(rowSpec,columnSpec);
param.height= GridLayout.LayoutParams.WRAP_CONTENT;
param.width=GridLayout.LayoutParams.WRAP_CONTENT;
gridLayout.addView(btn,param);
}
}
}
}
}
}
FuncPointer类实现了下面的小点点,不重要,很简单!!
FacePanel表情面板实现:
通过RecyclerView+GridLayoutManager实现,实现onFaceClick(spannableString)
接口,把表情转换为ImageSpan加入SpannableString,在后面的ChatBar中调用并使用Edittext的append方法加入输入框即可实现表请输入。
表情的格式可以自己定义我这里是用方括号[face_1]这样子,到时候再从String转换为SpannableString时候利用正则表达式解析表情符就可以了。
这个功能实现网上有很多,方式也各不相同。
这里面还用到BaseRecyclerViewAdapterHelper,大名鼎鼎,不多说了。
/**
* 表情面板
* @author 赵陈淏
*/
public class FacePanel extends LinearLayout
{
private static final String TAG="FacePanel";
private static final String FILE_NAME_PREFIX="face_";
private static final List<String> FACE_FILES=initNames();
private static final int FACE_NUM=20;
private OnFaceClickListener listener;
private static int faceSize=80;
private static List<String> initNames()
{
List<String> list=new ArrayList<>();
for (int i=1;i<=FACE_NUM;i++)
{
list.add(FILE_NAME_PREFIX+i);
}
return list;
}
public FacePanel(Context context) {
super(context);
initView(context);
}
public FacePanel(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public FacePanel(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
private void initView(Context context)
{
faceSize=ScreenUtils.dp2px(context,30);
View root=LayoutInflater.from(context).inflate(R.layout.item_face_panel,this);
RecyclerView recyclerView=root.findViewById(R.id.item_face_recycler);
GridLayoutManager manager=new GridLayoutManager(context,7);
FaceListAdapter listAdapter=new FaceListAdapter(FACE_FILES);
recyclerView.setAdapter(listAdapter);
recyclerView.setLayoutManager(manager);
listAdapter.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
//获取表情图片文件名
try {
Field field=R.drawable.class.getDeclaredField(FACE_FILES.get(position));
int resourceId = Integer.parseInt(field.get(null).toString());
// 在android中要显示图片信息,必须使用Bitmap位图的对象来装载
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resourceId);
if (bitmap==null)
{
Log.e(TAG,"未获取到表情文件");
return;
}
bitmap=Bitmap.createScaledBitmap(bitmap,faceSize,faceSize,false);
//要让图片替代指定的文字就要用ImageSpan
ImageSpan imageSpan = new ImageSpan(getContext(), bitmap);
String source="[face_"+(position+1)+"]";
SpannableString spannableString = new SpannableString(source);//face就是图片的前缀名
spannableString.setSpan(imageSpan, 0, source.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
if (listener!=null)
{
listener.onFaceClick(spannableString);
Log.e(TAG,"选择了表情"+position+1);
}
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
Log.e(TAG,"没有找到表情图片文件");
}
}
});
}
public void setOnFaceClickListener(OnFaceClickListener listener) {
this.listener = listener;
}
private class FaceListAdapter extends BaseQuickAdapter<String, BaseViewHolder>
{
private FaceListAdapter(@org.jetbrains.annotations.Nullable List<String> data) {
super(R.layout.item_face, data);
}
@Override
protected void convert(@NotNull BaseViewHolder holder, @org.jetbrains.annotations.Nullable String s) {
try {
Field field=R.drawable.class.getDeclaredField(s);
int resourceId = Integer.parseInt(field.get(null).toString());
// 在android中要显示图片信息,必须使用Bitmap位图的对象来装载
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resourceId);
holder.setImageBitmap(R.id.item_face_icon,bitmap);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
Log.e(TAG,"没有找到表情图片文件");
}
}
}
public interface OnFaceClickListener
{
void onFaceClick(SpannableString faceSpan);
}
}
聊天栏的实现,这里面把功能面板的所有接口引进来,大部分的抽象方法和接口在这里进行了具体实现,并且实现收起和打开面板的公共方法,因为在这里不能实现虚拟键盘的管理,所以需要向外延申至文首提到的聊天栏管理器ChatBarManager类中。
public class ChatBar extends LinearLayout implements View.OnClickListener
{
private FunctionPanel funcPanel;
private ChatBarAdapter chatBarAdapter;
private EditText sendText;
private boolean isFuncPanelShow=false;
private ImageView btnFunc;
public void setFunctionClickListener(FunctionPanel.FunctionClickListener listener)
{
funcPanel.setFunctionClickListener(listener);
}
public ChatBar(Context context) {
super(context);
initView(context);
}
public ChatBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public ChatBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
private void initView(Context context)
{
View root=LayoutInflater.from(context).inflate(R.layout.item_chat_bar,this);
funcPanel=root.findViewById(R.id.item_chat_func);
sendText=root.findViewById(R.id.item_chat_send_text);
final TextView btnSend=root.findViewById(R.id.item_chat_btn_send);
btnFunc=root.findViewById(R.id.item_chat_btn_func);
sendText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
if (!s.toString().isEmpty())
{
btnFunc.setVisibility(View.GONE);
btnSend.setVisibility(View.VISIBLE);
}
else
{
btnFunc.setVisibility(View.VISIBLE);
btnSend.setVisibility(View.GONE);
}
}
});
sendText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (isFuncPanelShow&&hasFocus)
{
funcPanelShow(false);
Log.e("ChatBar","输入框获取焦点");
}
}
});
btnSend.setOnClickListener(this);
btnFunc.setOnClickListener(this);
root.findViewById(R.id.item_chat_audio).setOnClickListener(this);
root.findViewById(R.id.item_chat_btn_face).setOnClickListener(this);
funcPanel.setOnFaceClickListener((SpannableString faceSpan)-> {
sendText.append(faceSpan);
});
}
@Override
public void onClick(View v) {
if (chatBarAdapter==null)
return;
switch (v.getId())
{
case R.id.item_chat_btn_send:
chatBarAdapter.sendText(sendText.getText());
sendText.setText("");
break;
case R.id.item_chat_btn_func:
if (isFuncPanelShow&&funcPanel.getWitchPanel()==FunctionPanel.FACE) {
funcPanel.setWitchPanel(FunctionPanel.FUNC);
break;
}
boolean isShow=!isFuncPanelShow;
funcPanel.setWitchPanel(FunctionPanel.FUNC);
funcPanelShow(isShow);
break;
case R.id.item_chat_btn_face:
if (isFuncPanelShow&&funcPanel.getWitchPanel()==FunctionPanel.FUNC) {
funcPanel.setWitchPanel(FunctionPanel.FACE);
break;
}
isShow=!isFuncPanelShow;
funcPanel.setWitchPanel(FunctionPanel.FACE);
funcPanelShow(isShow);
break;
case R.id.item_chat_audio:
break;
}
}
private void funcPanelShow(boolean isFuncPanelShow)
{
this.isFuncPanelShow=isFuncPanelShow;
if (isFuncPanelShow)
{
sendText.clearFocus();
btnFunc.requestFocus();
}
chatBarAdapter.funcPanelShow(isFuncPanelShow);
}
/**
* 设置高度与键盘平齐
*/
public void setFuncPanelHeight(int height)
{
if (height != funcPanel.getMeasuredHeight())
{
ViewGroup.LayoutParams params = funcPanel.getLayoutParams();
params.height = height;
funcPanel.setLayoutParams(params);
}
}
public boolean isFuncPanelShow() {
return isFuncPanelShow;
}
public void setFuncPanelVisible(int visible)
{
funcPanel.setVisibility(visible);
}
public void setChatBarAdapter(ChatBarAdapter chatBarAdapter) {
this.chatBarAdapter = chatBarAdapter;
}
public EditText getSendText()
{
return sendText;
}
public interface ChatBarAdapter
{
void sendText(CharSequence text);
void funcPanelShow(boolean isShow);
}
/**
* 设置自定义功能
*/
public void setUpFunctions(FunctionPanel.ChatFunction[] functions)
{
funcPanel.setUpFunctions(functions);
}
}
聊天活动基础类BaseChatActivity,这个是最终到达非常方便使用的基础,也是封装的最后一步。
在这里我们需要装载管理器类,将后面需要使用的方法抽象化。
这里有个地方需要注意管理器需要先设置好监听,再去装载抽象的功能列表,因为直到装载这一步时才会生成Fragment并把监听器置于Fragment之中,如果不提前设置好监听器,会出现监听器接口无效。
/**
* 聊天活动基础类
* @author 赵陈淏
*/
public abstract class BaseChatActivity extends BaseActivity implements FunctionPanel.FunctionClickListener
{
protected abstract void sendText(CharSequence text);
protected abstract FunctionPanel.ChatFunction[] getFunctions();
@Override
protected void initView()
{
ResizeLayout root=findViewById(R.id.chat_base_root);
ChatBar bar=findViewById(R.id.chat_base_chat_bar);
RecyclerView recyclerView=findViewById(R.id.chat_message_list);
final ChatBarManager manager=new ChatBarManager(this,bar,root)
{
@Override
public void sendText(CharSequence text) {
BaseChatActivity.this.sendText(text);
}
};
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
//避免弹起键盘触发又下落
if (!recyclerView.canScrollVertically(-1)||!recyclerView.canScrollVertically(1))
return;
if (!recyclerView.canScrollVertically(-1)||dy<0) {
manager.hideSoftInput();
}
}
});
//先设置好监听
manager.setFunctionClickListener(this);
//装载功能项
manager.setUpFunctions(getFunctions());
}
@Override
protected int getContentViewId() {
return R.layout.activity_chat_base;
}
}
//下面的是BaseActivity的少部分的基本片段,只是简单的将布局分离
public abstract class BaseActivity extends AppCompatActivity
{
private static final String TAG="BaseActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(getContentViewId());
initView();
}
protected abstract void initView();
protected abstract int getContentViewId();
}
聊天活动界面的布局activity_chat_base.xml
<?xml version="1.0" encoding="utf-8"?>
<com.biang.manage.widget.chat.ResizeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/chat_base_root"
android:orientation="vertical"
android:background="@color/colorBackground">
<com.biang.manage.widget.common.titlebar.TitleBar
android:id="@+id/chat_base_title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/chat_message_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<com.biang.manage.widget.chat.ChatBar
android:elevation="3dp"
android:id="@+id/chat_base_chat_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.biang.manage.widget.chat.ResizeLayout>
根布局下的ResizeLayout是对LinearLayout的onSizeChanged(int w, int h, int oldw, int oldh) 函数添加外部接口的一个类,很简单
public class ResizeLayout extends LinearLayout {
private OnResizeListener mListener;
public interface OnResizeListener {
void OnResize(int w, int h, int oldw, int oldh);
}
public void setOnResizeListener(OnResizeListener l) {
mListener = l;
}
public ResizeLayout(Context context) {
super(context);
}
public ResizeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ResizeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mListener != null) {
mListener.OnResize(w, h, oldw, oldh);
}
}
}
功能面板的灵活控制基本实现,实现protected FunctionPanel.ChatFunction[] getFunctions()
和public void onFuncClick(FunctionPanel.ChatFunction function)
两个抽象方法,就可以实现功能面板的灵活添加,比如群聊活动需要n项功能,而普通聊天功能只有n-2个,这样就非常容易控制,表情点击的接口已经在ChatBar下实现了,键盘弹起落下的动态逻辑在管理器和BaseChatActivity实现。
所以,继承了BaseChatActivity后只需要实现sendText()抽象方法,recyclerview的下滚逻辑实现的时候,由于recyclerview自身通过getChildCount()
方法获取的item数量有问题,所以只能放到这里调用Adapter的getItemCount()
实现,不能封装。
当然也有封装的办法,可以把Adapter的实现抽象化在父类里面,父类再调用抽象Adapter的getItemCount()
方法,不是完美主义就免了吧。
下面的是群聊活动的功能实现代码
public class GroupChatActivity extends BaseChatActivity implements View.OnClickListener
{
private static final int RECORD_LOAD_OK_FLAG=20;
private static final String TAG="GroupChat";
@Override
protected void sendText(CharSequence text)
{
//发送消息逻辑
Log.e(TAG,"我发送了"+text.toString());
}
@Override
protected void initView()
{
//这个不能少
super.initView();
//...省略
//消息的recyclerView配置好后,实现发送消息recycler滚动至最后一个
//避免弹出输入法不变化
//manager.setStackFromEnd(true);
recyclerView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (bottom < oldBottom) {
recyclerView.post(()-> {
//避免弹起键盘触发又下落
//如果本来就在底部,不动
if (!recyclerView.canScrollVertically(1))
return;
if (mAdapter.getItemCount() > 0) {
recyclerView.smoothScrollToPosition(mAdapter.getItemCount() - 1);
}
});
}
}
});
}
private static final int FUNC_CAMERA=0;
private static final int FUNC_GALLERY=1;
private static final int FUNC_POSITION=2;
private static final int FUNC_MEETING=3;
private static final int FUNC_LOG=4;
@Override
public void onFuncClick(FunctionPanel.ChatFunction function)
{
switch (function.funcFlag)
{
case FUNC_CAMERA:
Log.e(TAG,"点击了相机");
break;
case FUNC_GALLERY:
Log.e(TAG,"点击了相册");
break;
}
}
@Override
protected FunctionPanel.ChatFunction[] getFunctions()
{
FunctionPanel.ChatFunction[] functions=new FunctionPanel.ChatFunction[11];
for (int i=0;i<functions.length;i++)
{
functions[i]=new FunctionPanel.ChatFunction();
}
functions[0].funcFlag=FUNC_CAMERA;
functions[0].funcText="相机";
functions[0].funcResId=R.mipmap.ic_launcher;
functions[1].funcFlag=FUNC_GALLERY;
functions[1].funcText="相册";
functions[1].funcResId=R.mipmap.ic_launcher;
functions[2].funcFlag=FUNC_POSITION;
functions[2].funcText="位置";
functions[2].funcResId=R.mipmap.ic_launcher;
functions[3].funcFlag=FUNC_MEETING;
functions[3].funcText="视频会议";
functions[3].funcResId=R.mipmap.ic_launcher;
functions[4].funcFlag=FUNC_LOG;
functions[4].funcText="日志";
functions[4].funcResId=R.mipmap.ic_launcher;
functions[5].funcFlag=FUNC_LOG;
functions[5].funcText="日志";
functions[5].funcResId=R.mipmap.ic_launcher;
functions[6].funcFlag=FUNC_LOG;
functions[6].funcText="日志";
functions[6].funcResId=R.mipmap.ic_launcher;
functions[7].funcFlag=FUNC_LOG;
functions[7].funcText="日志";
functions[7].funcResId=R.mipmap.ic_launcher;
functions[8].funcFlag=FUNC_LOG;
functions[8].funcText="日志";
functions[8].funcResId=R.mipmap.ic_launcher;
functions[9].funcFlag=FUNC_LOG;
functions[9].funcText="日志";
functions[9].funcResId=R.mipmap.ic_launcher;
functions[10].funcFlag=FUNC_LOG;
functions[10].funcText="日志";
functions[10].funcResId=R.mipmap.ic_launcher;
return functions;
}
}
综上就完成了功能面板和聊天栏的封装,还算比较好用了,至于即时通讯的功能我使用Websocket实现的,消息格式等的东西内容多,逻辑还比较简单,这里写就写不完了。
后面马上需要实现语音消息,加上语音消息的接口,整个聊天栏控制器算是基本完成了。
源码还没有整理,功能还不完整,如果有需要的朋友可以私信我。
然后
等我弄完了立刻整理。