Android 客服聊天简单实现

        目前第三方的客服系统基本都开始收费了,但有些APP又离不开客服功能,就只能自己实现了,上一篇文章《Android Socket通信简单使用》就是为了实现客服功能做的准备,这里简单记录一下客服功能的实现。

一、主要功能

1、实现WebSocket通信保持长链接

2、能够发送文字、图片、商品等信息

3、显示消息发送时间、提示信息等

4、本地存储历史消息(Room实现本地数据库)

二、功能实现

        基本功能的实现大多在代码中进行注释,就不过多介绍了。

1、主页面布局

        这里就是简单的聊天页面,能够发送文字消息、图片和商品信息,并展示发送和收到的消息信息。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#f1f0f0">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:gravity="center"
        android:textColor="#333"
        android:textSize="16sp"
        android:background="#fff"
        android:text="客服"/>

    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefresh"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:background="#fff"
        android:gravity="center_vertical">

        <EditText
            android:id="@+id/et_input"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:background="@drawable/border_gray"
            android:layout_weight="1"
            android:hint="发送内容"
            android:textSize="14sp"
            android:textColor="#333740"
            android:textColorHint="#b9b9b9"
            android:padding="7dp"
            android:layout_marginLeft="5dp"/>

        <TextView
            android:id="@+id/tv_send"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="15dp"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="5dp"
            android:paddingBottom="5dp"
            android:layout_marginRight="5dp"
            android:background="@drawable/btn_green_corner"
            android:visibility="gone"
            android:textColor="#fff"
            android:text="发送"/>

        <TextView
            android:id="@+id/tv_more"
            android:layout_width="22dp"
            android:layout_height="22dp"
            android:layout_marginLeft="15dp"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="5dp"
            android:paddingBottom="5dp"
            android:layout_marginRight="5dp"
            android:background="@drawable/btn_more"/>
    </LinearLayout>

    <LinearLayout
        android:id="@+id/ll_more"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#fff"
        android:visibility="gone"
        android:orientation="vertical">

        <View
            android:layout_width="match_parent"
            android:layout_height="2px"
            android:background="#f2f4f7"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:id="@+id/tv_album"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:drawableTop="@mipmap/album"
                android:layout_marginLeft="30dp"
                android:layout_marginTop="15dp"
                android:layout_marginBottom="30dp"
                android:drawablePadding="8dp"
                android:gravity="center"
                android:text="相册"/>

            <TextView
                android:id="@+id/tv_photograph"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:drawableTop="@mipmap/photograph"
                android:layout_marginLeft="30dp"
                android:layout_marginTop="15dp"
                android:layout_marginBottom="30dp"
                android:drawablePadding="8dp"
                android:gravity="center"
                android:text="拍照"/>
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

2、主页面代码

        主要功能的实现都在这里,实现消息发送和接受功能,以及消息的展示等。

package com.app.socketdemo;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.json.JSONException;
import org.json.JSONObject;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.room.Room;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class KefuActivity extends AppCompatActivity {
    @BindView(R.id.rv_content)
    RecyclerView rvContent;
    @BindView(R.id.et_input)
    EditText etInput;
    @BindView(R.id.tv_send)
    TextView tvSend;
    @BindView(R.id.tv_more)
    TextView tvMore;
    @BindView(R.id.tv_album)
    TextView tvAlbum;
    @BindView(R.id.tv_photograph)
    TextView tvPhotograph;
    @BindView(R.id.ll_more)
    LinearLayout llMore;
    @BindView(R.id.swipeRefresh)
    SwipeRefreshLayout swipeRefresh;

    private AppDatabase db;
    private int mLimit = 2;//历史消息页数
    private MsgAdapter adapter;
    private List<MsgBean> mList = new ArrayList<>();

    private WebSocketClient client;

    private final int MESSAGE_ERROR = 0; //错误信息
    private final int MESSAGE_SUCCEED = 1; //连接成功
    private final int MESSAGE_RECEIVE = 2; //收到消息
    private final int MESSAGE_FINISH = 3;  //会话结束
    private final int MESSAGE_REFRESH = 4; //初始化消息
    private final int MESSAGE_HISTORY = 5; //更多历史数据

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_kefu);
        ButterKnife.bind(this);

        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        rvContent.setLayoutManager(linearLayoutManager);

        //下拉加载更多数据
        swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        //数据库查询userId=134的用户消息
                        List<MsgBean> dbList = db.msgDao().getUserMsg(mLimit, "134");
                        mList.addAll(0, dbList); //消息放到列表顶部
                        setMessage(dbList.size() + "", MESSAGE_HISTORY);
                    }
                }).start();
            }
        });

        //监听消息发送框
        etInput.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) {
                tvMore.setVisibility(View.VISIBLE);
                tvSend.setVisibility(View.GONE);
            }

            @Override
            public void afterTextChanged(Editable s) {
                if (TextUtils.isEmpty(s.toString())) {
                    tvMore.setVisibility(View.VISIBLE);
                    tvSend.setVisibility(View.GONE);
                } else {
                    tvMore.setVisibility(View.GONE);
                    tvSend.setVisibility(View.VISIBLE);
                }
            }
        });

        //加载历史消息第一页数据
        new Thread(new Runnable() {
            @Override
            public void run() {
                db = Room.databaseBuilder(getApplicationContext(),
                        AppDatabase.class, "message.db").build();
                mList.addAll(db.msgDao().getUserMsg(1, "134"));//获取第一页数据
                setMessage("", MESSAGE_REFRESH);
            }
        }).start();
    }

    /**
     * 初始化并连接服务
     * 注意:WebSocketClient不能重复初始化
     */
    private void initSocketClient() {
        //修改为自己的服务器地址
        URI uri = URI.create("ws://39.102.143.88:9090");
        client = new WebSocketClient(uri) {
            @Override
            public void onOpen(ServerHandshake handshakedata) {
                setMessage("您好,请描述您遇到的问题!", MESSAGE_SUCCEED);
            }

            @Override
            public void onMessage(String message) {
                setMessage(message, MESSAGE_RECEIVE);
            }

            @Override
            public void onClose(int code, String reason, boolean remote) {
                setMessage("会话结束", MESSAGE_FINISH);
            }

            @Override
            public void onError(Exception ex) {
                setMessage("客服连接异常,请稍后再试", MESSAGE_ERROR);
            }
        };
        try {
            client.connectBlocking();//连接服务
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 消息处理
     */
    private void setMessage(String obj, int arg1) {
        Message message = new Message();
        message.arg1 = arg1;
        message.obj = obj;
        handler.sendMessage(message);
    }

    @SuppressLint("HandlerLeak")
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            MsgBean bean = new MsgBean();
            switch (msg.arg1) {
                case MESSAGE_SUCCEED://连接成功
                    bean.setData(MsgBean.SYSTEM_MSG, (String) msg.obj);
                    mList.add(bean);
                    adapter.notifyItemInserted(mList.size() - 1);
                    rvContent.scrollToPosition(mList.size() - 1);
                    try {
                        JSONObject jsonObject = new JSONObject();
                        jsonObject.put("userId", "134");
                        client.send(jsonObject.toString());
                    } catch (Exception e) {
                        e.getMessage();
                    }
                    break;
                case MESSAGE_RECEIVE: //收到消息
                    bean.setData(MsgBean.RECEIVE_MSG, (String) msg.obj);
                    mList.add(bean);
                    adapter.notifyItemInserted(mList.size() - 1);
                    rvContent.scrollToPosition(mList.size() - 1);

                    insertData(bean);
                    break;

                case MESSAGE_ERROR: //异常消息
                case MESSAGE_FINISH: //会话结束
                    bean.setData(MsgBean.SYSTEM_MSG, (String) msg.obj);
                    mList.add(bean);
                    adapter.notifyItemInserted(mList.size() - 1);
                    rvContent.scrollToPosition(mList.size() - 1);
                    break;

                case MESSAGE_REFRESH: //初始化消息
//                String content = getIntent().getStringExtra("content");//模拟是否从商品页进入
                    String content = getGoodsData();
                    if (!TextUtils.isEmpty(content)) {
                        bean = new MsgBean();
                        bean.setData(MsgBean.SYSTEM_GOODS, content);
                        mList.add(bean);
                    }

                    adapter = new MsgAdapter(KefuActivity.this, mList);
                    rvContent.setAdapter(adapter);

                    initSocketClient();
                    break;
                case MESSAGE_HISTORY: //更多历史数据
                    mLimit++;
                    swipeRefresh.setRefreshing(false);
                    adapter.notifyDataSetChanged();

                    rvContent.scrollToPosition(Integer.valueOf((String) msg.obj));
                    LinearLayoutManager mLayoutManager =
                            (LinearLayoutManager) rvContent.getLayoutManager();
                    mLayoutManager.scrollToPositionWithOffset(Integer.valueOf((String) msg.obj), 0);
                    break;
                default:
                    break;
            }
        }
    };

    /**
     * 模拟商品详情页进入
     */
    private String getGoodsData() {
        JSONObject jsonObject = new JSONObject();
        try {
            jsonObject.put("id", "1");
            jsonObject.put("title", "模拟商品标题名称");
            jsonObject.put("desc", "商品规格/型号");
            jsonObject.put("price", "399.00");
            jsonObject.put("imgurl", "https://s3.bmp.ovh/imgs/2021/10/30f369d8980bd070_thumb.jpg");
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return jsonObject.toString();
    }

    @OnClick({R.id.tv_send, R.id.tv_more, R.id.tv_album, R.id.tv_photograph})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.tv_send:
                if (TextUtils.isEmpty(etInput.getText().toString())) {
                    Toast.makeText(this, "消息不能为空", Toast.LENGTH_SHORT).show();
                    return;
                }
                sendMessage(MsgBean.SEND_MSG, etInput.getText().toString());
                break;
            case R.id.tv_more:
                if (tvMore.isSelected()) {
                    tvMore.setSelected(false);
                    llMore.setVisibility(View.GONE);
                } else {
                    tvMore.setSelected(true);
                    llMore.setVisibility(View.VISIBLE);
                }
                break;
            case R.id.tv_album:
                //调用相册,这里模拟发送图片
                sendMessage(MsgBean.SEND_PIC, "https://s3.bmp.ovh/imgs/2021/10/df0c80c09a29cead.jpeg");
                tvMore.setSelected(false);
                llMore.setVisibility(View.GONE);
                break;
            case R.id.tv_photograph:
                //调用拍照,这里模拟收到图片
                sendMessage(MsgBean.RECEIVE_PIC, "https://ftp.bmp.ovh/imgs/2020/02/85414fc7ade712f1.jpg");
                tvMore.setSelected(false);
                llMore.setVisibility(View.GONE);
                break;
        }
    }

    /**
     * 数据库插入消息
     */
    private void insertData(MsgBean bean) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                db.msgDao().insertAll(bean);
            }
        }).start();
    }

    /**
     * 发送消息
     */
    public void sendMessage(int type, String msg) {
        if (client != null && client.isOpen()) {
            try {
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("nickname", "cx");
                jsonObject.put("userId", "134");
                jsonObject.put("content", msg.trim());

                client.send(jsonObject.toString());

                MsgBean bean = new MsgBean();
                bean.setData(type, msg);
                mList.add(bean);
                adapter.notifyItemInserted(mList.size() - 1);
                rvContent.scrollToPosition(mList.size() - 1);
                etInput.setText("");

                insertData(bean);
            } catch (Exception e) {
                e.getMessage();
            }
        } else {
            setMessage("客服连接异常,请稍后再试", MESSAGE_ERROR);
        }
    }

    /**
     * 关闭服务
     */
    private void downServer() {
        try {
            if (null != client) {
                client.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            client = null;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        downServer();
    }

    private long p = 0; // 连续点击两次退出

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            long timeMillis = SystemClock.uptimeMillis();
            if (timeMillis - p < 1500) {
                this.finish();
                return true;
            }
            p = SystemClock.uptimeMillis();
            Toast.makeText(this, "再按退出客服", Toast.LENGTH_SHORT).show();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }
}

3、Adapter代码

        用来实现消息列表的展示,包括发送信息、接收信息、提示信息和商品信息的展示。

package com.app.socketdemo;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;

import org.json.JSONObject;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;

public class MsgAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private Context mContext;
    private List<MsgBean> mDatas;

    private String currentDate;//当前日期
    private String beforeDate;//昨天

    public MsgAdapter(Context context, List<MsgBean> datas) {
        mContext = context;
        mDatas = datas;

        SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd");
        Date date = new Date(System.currentTimeMillis());
        currentDate = formatter.format(date);
        date = new Date(System.currentTimeMillis() - 86400000);
        beforeDate = formatter.format(date);
    }

    @Override
    public int getItemViewType(int position) {
        return mDatas.get(position).getType();
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        if (viewType == MsgBean.SYSTEM_MSG){ //系统消息
            View itemView = LayoutInflater.from(mContext).inflate(R.layout.item_msg_centre, parent, false);
            return new CentreHolder(itemView);
        }else if (viewType == MsgBean.RECEIVE_MSG){ //收到消息
            View itemView = LayoutInflater.from(mContext).inflate(R.layout.item_msg_left, parent, false);
            return new LeftHolder(itemView);
        }else if (viewType == MsgBean.SEND_MSG){ //发送消息
            View itemView = LayoutInflater.from(mContext).inflate(R.layout.item_msg_right, parent, false);
            return new RightHolder(itemView);
        }else if (viewType == MsgBean.RECEIVE_PIC){ //收到图片
            View itemView = LayoutInflater.from(mContext).inflate(R.layout.item_picture_left, parent, false);
            return new PicLeftHolder(itemView);
        }else if (viewType == MsgBean.SEND_PIC){ //发送图片
            View itemView = LayoutInflater.from(mContext).inflate(R.layout.item_picture_right, parent, false);
            return new PicRightHolder(itemView);
        }else if (viewType == MsgBean.SYSTEM_GOODS){ //展示商品
            View itemView = LayoutInflater.from(mContext).inflate(R.layout.item_goods_centre, parent, false);
            return new GoodsCentreHolder(itemView);
        }else if (viewType == MsgBean.SEND_GOODS){ //发送商品
            View itemView = LayoutInflater.from(mContext).inflate(R.layout.item_goods_right, parent, false);
            return new GoodsRightHolder(itemView);
        }
        return null;
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof CentreHolder){
            ((CentreHolder) holder).tvMsgcentre.setText(mDatas.get(position).getMsg());
        }else if (holder instanceof LeftHolder){
            setTime(((LeftHolder) holder).tvTime, position);
            ((LeftHolder) holder).tvMsgleft.setText(mDatas.get(position).getMsg());
        }else if (holder instanceof RightHolder){
            setTime(((RightHolder) holder).tvTime, position);
            ((RightHolder) holder).tvMsgRight.setText(mDatas.get(position).getMsg());
        }else if (holder instanceof PicLeftHolder){
//            ((PicLeftHolder) holder).ivPicLeft.
            showImage(((PicLeftHolder) holder).ivPicLeft, mDatas.get(position).getMsg());
        }else if (holder instanceof PicRightHolder){
//            ((PicRightHolder) holder).ivPicRight
            showImage(((PicRightHolder) holder).ivPicRight, mDatas.get(position).getMsg());
        }else if (holder instanceof GoodsCentreHolder){
            try {
                JSONObject obj = new JSONObject(mDatas.get(position).getMsg());
                ((GoodsCentreHolder) holder).tvTitle.setText(obj.optString("title"));
                ((GoodsCentreHolder) holder).tvType.setText(obj.optString("desc"));
                ((GoodsCentreHolder) holder).tvPrice.setText(obj.optString("price"));
                Glide.with(mContext).load(obj.optString("imgurl")).into(((GoodsCentreHolder) holder).ivPicture);
                ((GoodsCentreHolder) holder).tvSend.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        ((KefuActivity)mContext).sendMessage(MsgBean.SEND_GOODS, mDatas.get(position).getMsg());
                    }
                });
            } catch (Exception e) {
                e.getMessage();
            }
        }else if (holder instanceof GoodsRightHolder){
            try {
                JSONObject obj = new JSONObject(mDatas.get(position).getMsg());
                ((GoodsRightHolder) holder).tvTitle.setText(obj.optString("title"));
                ((GoodsRightHolder) holder).tvType.setText(obj.optString("desc"));
                ((GoodsRightHolder) holder).tvPrice.setText(obj.optString("price"));
                Glide.with(mContext).load(obj.optString("imgurl")).into(((GoodsRightHolder) holder).ivPicture);
                ((GoodsRightHolder) holder).itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        //根据商品id跳转详情页;obj.optString("id")
                    }
                });
            } catch (Exception e) {
                e.getMessage();
            }
        }
    }

    @Override
    public int getItemCount() {
        return mDatas == null ? 0 : mDatas.size();
    }

    /**
     * 处理展示图片大小,这里有待优化
     * @param iv 图片显示控件
     * @param url 图片url
     */
    private void showImage(ImageView iv, String url){
        Glide.with(mContext)
                .asBitmap()
                .load(url)
                .into(new CustomTarget<Bitmap>() {
                    @Override
                    public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
                        int maxWidth = resource.getWidth();
                        int maxHeight = resource.getHeight();

                        DisplayMetrics dm = new DisplayMetrics();
                        dm = mContext.getResources().getDisplayMetrics();
                        int width = dm.widthPixels;

                        LinearLayout.LayoutParams viewParams;
                        if(maxWidth >= maxHeight){
                            if (maxWidth < DensityUtil.dip2px(230)){
                                viewParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                                        ViewGroup.LayoutParams.WRAP_CONTENT);
                            }else {
                                viewParams = new LinearLayout.LayoutParams(DensityUtil.dip2px(230),
                                        maxHeight * DensityUtil.dip2px(230) / maxWidth);
                            }
                        }else{
                            if (maxHeight < DensityUtil.dip2px(230)){
                                viewParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                                        ViewGroup.LayoutParams.WRAP_CONTENT);
                            }else {
                                viewParams = new LinearLayout.LayoutParams(maxWidth * DensityUtil.dip2px(230) / maxHeight,
                                        DensityUtil.dip2px(230));
                            }
                        }
                        iv.setLayoutParams(viewParams);
                        iv.setImageBitmap(resource);
                    }

                    @Override
                    public void onLoadCleared(@Nullable Drawable placeholder) {

                    }
                });
    }

    /**
     * 显示消息时间
     * 这里只做了文字消息的时间展示,其他消息同理
     * @param tv 显示时间控件
     * @param position 位置
     */
    private void setTime(TextView tv, int position){
        tv.setVisibility(View.GONE);
        long time = mDatas.get(position).getTime();
        if (position == 0){
            showTime(tv, time);
        }else{
            long time1 = mDatas.get(position - 1).getTime();
            if (time - time1 > 120000){ //两条消息间隔超过2分钟显示时间
                showTime(tv, time);
            }
        }
    }

    /**
     * 展示时间
     */
    private void showTime(TextView tv, long time){
        tv.setVisibility(View.VISIBLE);

        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Date date = new Date(time);
        String msgDate = dateFormat.format(date);

        SimpleDateFormat formatter = new SimpleDateFormat("HH:mm");
        if (msgDate.equals(currentDate)){
            tv.setText(formatter.format(date));
        }else if(msgDate.equals(beforeDate)){
            tv.setText("昨天 " + formatter.format(date));
        }else{
            tv.setText(msgDate + " " + formatter.format(date));
        }
    }

    /**
     * 系统消息ViewHolder
     */
    class CentreHolder extends RecyclerView.ViewHolder {
        public TextView tvMsgcentre;

        public CentreHolder(@NonNull View itemView) {
            super(itemView);

            tvMsgcentre = itemView.findViewById(R.id.tv_msgcentre);
        }
    }

    /**
     * 收到消息ViewHolder
     */
    class LeftHolder extends RecyclerView.ViewHolder {
        public TextView tvTime;
        public TextView tvMsgleft;

        public LeftHolder(@NonNull View itemView) {
            super(itemView);

            tvTime = itemView.findViewById(R.id.tv_time);
            tvMsgleft = itemView.findViewById(R.id.tv_msgleft);
        }
    }

    /**
     * 发送消息ViewHolder
     */
    class RightHolder extends RecyclerView.ViewHolder {
        public TextView tvTime;
        public TextView tvMsgRight;

        public RightHolder(@NonNull View itemView) {
            super(itemView);

            tvTime = itemView.findViewById(R.id.tv_time);
            tvMsgRight = itemView.findViewById(R.id.tv_msgright);
        }
    }

    /**
     * 收到图片ViewHolder
     */
    class PicLeftHolder extends RecyclerView.ViewHolder {
        public ImageView ivPicLeft;

        public PicLeftHolder(@NonNull View itemView) {
            super(itemView);

            ivPicLeft = itemView.findViewById(R.id.iv_picleft);
        }
    }

    /**
     * 发送图片ViewHolder
     */
    class PicRightHolder extends RecyclerView.ViewHolder {
        public ImageView ivPicRight;

        public PicRightHolder(@NonNull View itemView) {
            super(itemView);

            ivPicRight = itemView.findViewById(R.id.iv_picright);
        }
    }

    /**
     * 展示商品ViewHolder
     */
    class GoodsCentreHolder extends RecyclerView.ViewHolder {
        public ImageView ivPicture;
        public TextView tvTitle;
        public TextView tvType;
        public TextView tvPrice;
        public TextView tvSend;

        public GoodsCentreHolder(@NonNull View itemView) {
            super(itemView);

            ivPicture = itemView.findViewById(R.id.iv_picture);
            tvTitle = itemView.findViewById(R.id.tv_title);
            tvType = itemView.findViewById(R.id.tv_type);
            tvPrice = itemView.findViewById(R.id.tv_price);
            tvSend = itemView.findViewById(R.id.tv_send);
        }
    }

    /**
     * 发送商品ViewHolder
     */
    class GoodsRightHolder extends RecyclerView.ViewHolder {
        public ImageView ivPicture;
        public TextView tvTitle;
        public TextView tvType;
        public TextView tvPrice;

        public GoodsRightHolder(@NonNull View itemView) {
            super(itemView);

            ivPicture = itemView.findViewById(R.id.iv_picture);
            tvTitle = itemView.findViewById(R.id.tv_title);
            tvType = itemView.findViewById(R.id.tv_type);
            tvPrice = itemView.findViewById(R.id.tv_price);
        }
    }
}

4、消息布局

        实现发送、接收、提示和商品等信息的布局。

(1)item_msg_centre.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="8dp">

    <TextView
        android:id="@+id/tv_msgcentre"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="7dp"
        android:layout_marginBottom="7dp"
        android:textSize="12sp"
        android:textColor="#666"
        android:layout_gravity="center_horizontal"
        android:text="可以开始跟客服聊天了"/>
</LinearLayout>

(2)item_msg_left.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >

    <TextView
        android:id="@+id/tv_time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="7dp"
        android:layout_marginBottom="7dp"
        android:textSize="12sp"
        android:textColor="#666"
        android:visibility="gone"
        android:layout_gravity="center_horizontal"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp">

        <ImageView
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_marginRight="5dp"
            android:src="@mipmap/appkefu_ic_sex_female"/>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/ease_chatfrom_bg_normal"
            android:layout_marginRight="50dp"
            android:layout_gravity="left">

            <TextView
                android:id="@+id/tv_msgleft"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:layout_marginRight="10dp"
                android:layout_marginTop="7dp"
                android:layout_marginBottom="7dp"
                android:textSize="15sp"
                android:textColor="#000"
                android:text="消息"/>
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

(3)item_msg_right.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tv_time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="7dp"
        android:layout_marginBottom="7dp"
        android:textSize="12sp"
        android:textColor="#666"
        android:visibility="gone"
        android:layout_gravity="center_horizontal"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="right"
        android:padding="8dp">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="50dp"
            android:background="@drawable/ease_chatto_bg_normal">

            <TextView
                android:id="@+id/tv_msgright"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:layout_marginRight="10dp"
                android:layout_marginTop="7dp"
                android:layout_marginBottom="7dp"
                android:textSize="15sp"
                android:textColor="#000"
                android:text="消息"/>
        </LinearLayout>

        <ImageView
            android:id="@+id/iv_head"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_marginLeft="5dp"
            android:src="@mipmap/appkefu_ic_sex_male"/>
    </LinearLayout>
</LinearLayout>

(4)item_goods_centre.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/round_white_bg"
        android:padding="15dp"
        android:layout_margin="15dp">

        <ImageView
            android:id="@+id/iv_picture"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:scaleType="centerCrop"/>

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@+id/iv_picture"
            android:layout_marginLeft="10dp"
            android:textColor="#5c5c5b"
            android:maxLines="1"
            android:ellipsize="end"
            android:textSize="16sp"
            android:text="商品名称" />

        <TextView
            android:id="@+id/tv_type"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@+id/iv_picture"
            android:layout_below="@+id/tv_title"
            android:layout_marginLeft="10dp"
            android:layout_marginTop="10dp"
            android:textColor="#8c8c8c"
            android:textSize="12sp"
            android:text="WLA005" />

        <TextView
            android:id="@+id/tv_symbol"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@+id/iv_picture"
            android:layout_below="@+id/tv_type"
            android:layout_alignBottom="@+id/iv_picture"
            android:layout_marginLeft="10dp"
            android:textColor="#ff621b"
            android:paddingBottom="2dp"
            android:gravity="bottom"
            android:textSize="12sp"
            android:text="¥" />

        <TextView
            android:id="@+id/tv_price"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@+id/tv_symbol"
            android:layout_below="@+id/tv_type"
            android:layout_alignBottom="@+id/iv_picture"
            android:layout_marginLeft="5dp"
            android:textColor="#ff621b"
            android:gravity="bottom"
            android:textSize="18sp"
            android:text="399" />

        <TextView
            android:id="@+id/tv_send"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignBottom="@+id/iv_picture"
            android:layout_alignParentRight="true"
            android:layout_marginLeft="5dp"
            android:background="@drawable/btn_green_corner"
            android:paddingLeft="10dp"
            android:paddingTop="5dp"
            android:paddingBottom="5dp"
            android:paddingRight="10dp"
            android:textColor="#fff"
            android:gravity="bottom"
            android:textSize="12sp"
            android:text="发送链接" />
    </RelativeLayout>
</LinearLayout>

(5)item_goods_right.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="right"
    android:padding="10dp">

    <RelativeLayout
        android:layout_width="220dp"
        android:layout_height="wrap_content"
        android:background="@drawable/round_white_bg"
        android:padding="10dp">

        <ImageView
            android:id="@+id/iv_picture"
            android:layout_width="70dp"
            android:layout_height="70dp"
            android:scaleType="centerCrop"/>

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@+id/iv_picture"
            android:layout_marginLeft="10dp"
            android:textColor="#5c5c5b"
            android:textSize="14sp"
            android:maxLines="1"
            android:ellipsize="end"
            android:text="全蛋白提取试剂盒" />

        <TextView
            android:id="@+id/tv_type"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@+id/iv_picture"
            android:layout_below="@+id/tv_title"
            android:layout_marginLeft="10dp"
            android:layout_marginTop="5dp"
            android:textColor="#8c8c8c"
            android:textSize="12sp"
            android:text="WLA005" />

        <TextView
            android:id="@+id/tv_symbol"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@+id/iv_picture"
            android:layout_below="@+id/tv_type"
            android:layout_alignBottom="@+id/iv_picture"
            android:layout_marginLeft="10dp"
            android:textColor="#ff621b"
            android:gravity="bottom"
            android:textSize="12sp"
            android:paddingBottom="2dp"
            android:text="¥" />

        <TextView
            android:id="@+id/tv_price"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@+id/tv_symbol"
            android:layout_below="@+id/tv_type"
            android:layout_alignBottom="@+id/iv_picture"
            android:layout_marginLeft="5dp"
            android:textColor="#ff621b"
            android:gravity="bottom"
            android:textSize="16sp"
            android:text="399" />
    </RelativeLayout>

    <ImageView
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_marginLeft="10dp"
        android:src="@mipmap/appkefu_ic_sex_male"/>
</LinearLayout>

省略了两个图片布局,比较简单就不粘贴了

5、数据库功能实现

        用于存储消息历史数据。

(1)MsgBean

package com.app.socketdemo;

import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;

@Entity
public class MsgBean {
    public static final int SYSTEM_MSG = 0; //系统消息
    public static final int RECEIVE_MSG = 1; //收到消息
    public static final int SEND_MSG = 2; //发送消息
    public static final int RECEIVE_PIC = 3; //收到图片
    public static final int SEND_PIC = 4; //发送图片
    public static final int SYSTEM_GOODS = 5; //展示商品
    public static final int SEND_GOODS = 6; //发送商品

    @PrimaryKey(autoGenerate = true)
    private int id; //主键ID

    @ColumnInfo(name = "userId")
    private String userId = "134"; //根据用户显示历史消息,这里默认

    @ColumnInfo(name = "type")
    private int type;//0:系统消息;1:收到消息;2:发送消息;3:收到图片;4:发送图片;5:展示商品;6:发送商品

    @ColumnInfo(name = "msg")
    private String msg; //消息内容

    @ColumnInfo(name = "time")
    private long time = System.currentTimeMillis(); //消息时间

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public void setTime(long time) {
        this.time = time;
    }

    public long getTime() {
        return time;
    }

    public void setData(int type, String msg){
        this.type = type;
        this.msg = msg;
    }
}

(2)AppDatabase

package com.app.socketdemo;

import androidx.room.Database;
import androidx.room.RoomDatabase;

@Database(entities = {MsgBean.class}, version = 2, exportSchema=false)
public abstract class AppDatabase extends RoomDatabase {
    public abstract MessageDao msgDao();
}

(3)MessageDao

package com.app.socketdemo;

import java.util.List;

import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;

@Dao
public interface MessageDao {

    //查询全部数据
    @Query("SELECT * FROM MsgBean")
    List<MsgBean> getAll();

    //指定userId,分页查询数据
    @Query("SELECT * FROM (SELECT * FROM MsgBean WHERE userId = (:userId) ORDER BY id DESC LIMIT ((:limit) - 1) * 10, (:limit) * 10) ORDER BY id")
    List<MsgBean> getUserMsg(int limit, String userId);

    //插入数据
    @Insert
    void insertAll(MsgBean... msg);

    //删除数据
    @Delete
    void delete(MsgBean msg); //删除单条数据

    @Query("delete from MsgBean") //删除全部信息
    void deletAll();
}

6、引入依赖

//懒人框架
implementation 'com.jakewharton:butterknife:10.2.3'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
//图片框架
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
//RecyclerView
implementation 'androidx.recyclerview:recyclerview:1.1.0'
//WebSocket
implementation "org.java-websocket:Java-WebSocket:1.5.1"
//Room
implementation "android.arch.persistence.room:runtime:1.1.1"
annotationProcessor "android.arch.persistence.room:compiler:1.1.1"

7、工具类

package com.app.socketdemo;


public class DensityUtil {

    /**
     * dp转px
     */
    public static int dip2px(float dpValue) {
        final float scale = MyApp.getInstance().mContext.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
}

运行效果:

源码下载

  • 10
    点赞
  • 75
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

c小旭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值