欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android开发——即时通讯中自定义聊天页面封装实现过程记录

程序员文章站 2022-07-13 14:41:35
...

说明

本人大四党,菜鸟,毕业设计有即时通讯的部分,目前即时通讯的模块基本完成,记录一下在Android平台下实现整个即时通讯界面功能的过程,希望对大家有点帮助。
代码和博客都是热乎的!!Android开发——即时通讯中自定义聊天页面封装实现过程记录

功能点

  • 已完成功能点

  1. 适应键盘高度
  2. 自定义灵活的功能面板
  3. 表情消息
  • 尚未完成的功能点

  1. 语音消息
  2. 图片消息

话不多说系列

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用来监听其点击事件
Android开发——即时通讯中自定义聊天页面封装实现过程记录
功能面板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类实现了下面的小点点,不重要,很简单!!
Android开发——即时通讯中自定义聊天页面封装实现过程记录
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实现的,消息格式等的东西内容多,逻辑还比较简单,这里写就写不完了。

后面马上需要实现语音消息,加上语音消息的接口,整个聊天栏控制器算是基本完成了。

源码还没有整理,功能还不完整,如果有需要的朋友可以私信我。
然后
等我弄完了立刻整理。