目前第三方的客服系统基本都开始收费了,但有些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);
}
}
运行效果: