Xây dựng úng dụng chát đơn giản bằng RecyclerView
Hầu hết các ứng dụng di động bây giờ đều có tính năng chát, với những ứng dụng chát phức tạp thì đã có khá nhiều thư viện hỗ trợ, nhưng nếu bạn chỉ cần 1 ứng dụng đơn giản mà phải thêm những lib cồng kềnh vào thì sẽ kiến ứng dụng của bạn nặng nề. Dưới đây mình sẽ hướng dẫ các bạn sử dụng ...
Hầu hết các ứng dụng di động bây giờ đều có tính năng chát, với những ứng dụng chát phức tạp thì đã có khá nhiều thư viện hỗ trợ, nhưng nếu bạn chỉ cần 1 ứng dụng đơn giản mà phải thêm những lib cồng kềnh vào thì sẽ kiến ứng dụng của bạn nặng nề. Dưới đây mình sẽ hướng dẫ các bạn sử dụng RecyclerView để tạo 1 chức năng Chat đơn giản.
Cấu trúc thư mục.
thêm các thư viện hỗ trợ.
android { ... // Sử dụng data binding dataBinding { enabled = true } } dependencies { ... implementation 'com.android.support:recyclerview-v7:27.1.0' implementation 'com.android.support:cardview-v7:27.1.0' // Thư viện sử dụng để hiển thị ảnh implementation "com.github.bumptech.glide:glide:4.5.0" }
Bây giờ chúng ta sẽ đi vào chi tiết từng file.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" > <data> <variable name="viewModel" type="com.demochatrecyclerview.MainActivity" /> </data> <LinearLayout android:layout_awidth="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.demochatrecyclerview.MainActivity" > <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" android:layout_awidth="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <View android:layout_awidth="wrap_content" android:layout_height="1dp" android:background="##FFE3E3E3" /> <LinearLayout android:layout_awidth="match_parent" android:layout_height="wrap_content" android:minHeight="56dp" android:orientation="horizontal" > <android.support.v7.widget.AppCompatImageView android:layout_awidth="wrap_content" android:layout_height="match_parent" android:layout_gravity="center" android:layout_marginStart="10dp" android:onClick="@{ viewModel::chooseImage }" android:src="@mipmap/ic_camera" /> <android.support.v7.widget.AppCompatEditText android:layout_awidth="0dp" android:layout_height="wrap_content" android:layout_margin="10dp" android:layout_weight="1" android:background="@drawable/bg_edit_text_talk" android:gravity="center_vertical" android:hint="Write something" android:padding="6dp" android:text="@={ viewModel.message }" android:textSize="14sp" /> <android.support.v7.widget.AppCompatTextView android:layout_awidth="wrap_content" android:layout_height="match_parent" android:layout_marginEnd="16dp" android:gravity="center" android:onClick="@{ viewModel::sendMessage }" android:text="Send" android:textColor="#FF318496" android:textSize="16sp" android:textStyle="bold" /> </LinearLayout> </LinearLayout> </layout>
ở đây mình chỉ thiết kế layout đơn giản gồm 1 RecyclerView để hiển thị nội dung chát và 1 ô nhâp dữ liệu ở dưới.
MainActivity.java
public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding; private RecyclerView recyclerView; private ChatAdapter chatAdapter; private List<Mesage> messageList; public ObservableField<String> message = new ObservableField<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_main); binding.setViewModel(this); messageList = new ArrayList<>(); chatAdapter = new ChatAdapter(messageList); recyclerView = binding.recyclerView; LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); linearLayoutManager.setReverseLayout(true); linearLayoutManager.setSmoothScrollbarEnabled(true); linearLayoutManager.setAutoMeasureEnabled(true); linearLayoutManager.setStackFromEnd(true); recyclerView.setLayoutManager(linearLayoutManager); recyclerView.setNestedScrollingEnabled(false); recyclerView.setAdapter(chatAdapter); } public void chooseImage(View view) { } public void sendMessage(View view) { } }
Cài đặt cơ bản cho file MainActivity
Cài đặt file ChatAdapter.java
public class ChatAdapter extends RecyclerView.Adapter<ChatViewHolder> { private static final int TYPE_MESSAGE_RECEIVE = 1; private static final int TYPE_MESSAGE_SEND = 2; private List<Message> messageList; public ChatAdapter(List<Message> messageList){ this.messageList = messageList; } @NonNull @Override public ChatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { case TYPE_MESSAGE_RECEIVE: ItemMessageReceiveBinding itemMessageReceiveBinding = ItemMessageReceiveBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); return new ViewHolderReceive(itemMessageReceiveBinding); case TYPE_MESSAGE_SEND: ItemMessageSendBinding itemMessageSendBinding = ItemMessageSendBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); return new ViewHolderSend(itemMessageSendBinding); } return null; } @Override public void onBindViewHolder(@NonNull ChatViewHolder holder, int position) { Message messageNext = position == 0 ? null : messageList.get(position - 1); Message messagePrevious = position >= getItemCount() - 1 ? null : messageList.get(position + 1); switch (holder.getItemViewType()) { case TYPE_MESSAGE_RECEIVE: ((ViewHolderReceive) holder).bind(messageNext, messageList.get(position), messagePrevious); break; case TYPE_MESSAGE_SEND: ((ViewHolderSend) holder).bind(messageNext, messageList.get(position), messagePrevious); break; } } @Override public int getItemViewType(int position) { if (messageList.get(position).isMyMessage()) { return TYPE_MESSAGE_SEND; } else { return TYPE_MESSAGE_RECEIVE; } } @Override public int getItemCount() { return messageList == null ? 0 : messageList.size(); } }
Ở đây chúng ta sẽ có 2 kiểu tin nhắn là tin nhắn do mình gửi đi và tin nhắn nhận về TYPE_MESSAGE_RECEIVE và TYPE_MESSAGE_SEND chúng ta sẽ lựa chọn từng kiểu tin nhắn để hiển thị sao cho đúng vị trí(giả sử tin nhắn gửi đi sẽ hiển thị ở bên phải còn con nhắn nhận về sẽ hiển thị ở bên trái)
Ở trong hàm onBindViewHolder mình có truyền vào messagePrevious để khi hiển thị chúng ta có thể kiểm tra các điều kiện.
File ViewHolderReceive.java và ViewHolderSend.java
public class ViewHolderReceive extends ChatViewHolder<ItemMessageReceiveBinding> { public ViewHolderReceive(ItemMessageReceiveBinding itemView) { super(itemView); } @Override public void bind(Message messageCurrent, Message messagePrevious) { if (binding.getViewModel() == null) { binding.setViewModel(this); } super.bind(messageCurrent, messagePrevious); } } ... public class ViewHolderSend extends ChatViewHolder<ItemMessageSendBinding> { public ViewHolderSend(ItemMessageSendBinding itemView) { super(itemView); } @Override public void bind(Message messageCurrent, Message messagePrevious) { if (binding.getViewModel() == null) { binding.setViewModel(this); } super.bind(messageCurrent, messagePrevious); } }
File item_message_send.xml
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tool="http://schemas.android.com/tools" > <data> <variable name="viewModel" type="com.demochatrecyclerview.ViewHolderSend" /> <import type="android.view.View"/> <import type="android.text.TextUtils"/> </data> <LinearLayout android:layout_awidth="match_parent" android:layout_height="wrap_content" android:gravity="end" > <LinearLayout android:layout_awidth="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:background="#FFFFFF" android:orientation="vertical" > <android.support.v7.widget.AppCompatTextView android:layout_awidth="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="20dp" android:gravity="center" android:text="@{ viewModel.date }" android:textColor="#FF909090" android:visibility="@{ TextUtils.isEmpty(viewModel.date) ? View.GONE : View.VISIBLE }" tool:text="2018-02-28" /> <LinearLayout android:layout_awidth="match_parent" android:layout_height="wrap_content" android:layout_marginEnd="10dp" android:gravity="end" android:minHeight="36dp" android:orientation="horizontal" > <android.support.v7.widget.AppCompatTextView android:id="@+id/time" android:layout_awidth="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:minWidth="40dp" android:text="@{ viewModel.time }" android:textSize="14sp" android:visibility="@{ TextUtils.isEmpty(viewModel.time) ? View.INVISIBLE : View.VISIBLE }" tool:text="15:03" /> <android.support.v7.widget.AppCompatTextView android:id="@+id/content" android:layout_awidth="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginStart="10dp" android:background="@drawable/bg_edit_text_talk" android:gravity="center_vertical" android:minHeight="36dp" android:text="@{ viewModel.content }" android:textColor="#000000" android:textSize="14sp" android:visibility="@{TextUtils.isEmpty(viewModel.content) ? View.GONE : View.VISIBLE }" /> </LinearLayout> </LinearLayout> </LinearLayout> </layout>
file item_message_receive.xml có thiết kế gần giống với file item_message_send.xml nên mình sẽ không nêu ở đây. Và các bạn áp dụng và project của mình thì nên căn chỉnh lại chút cho đẹp nhé.
Thực chất các tin nhắn hiển thị có logic giống nhau nên mình sẽ viết chung logic của chúng trong file ChatViewHolder.java còn 2 file này mục đích là chia View mà thôi.
File logic chính để hiển thị Message là trong file ChatViewHolder.java
public class ChatViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder { @BindingAdapter({ "glideImageUrl" }) public static void setGlideImageUrl(ImageView imageview, String url) { RequestOptions requestOptions = new RequestOptions().placeholder(R.mipmap.ic_launcher_round) .diskCacheStrategy(DiskCacheStrategy.RESOURCE); Glide.with(imageview.getContext()).load(url).apply(requestOptions).into(imageview); } protected T binding; protected Message messageCurrent; public ObservableField<String> date = new ObservableField<>(); public ObservableField<String> time = new ObservableField<>(); public ObservableField<String> content = new ObservableField<>(); public ObservableField<String> fullName = new ObservableField<>(); public ObservableField<String> imageAvatar = new ObservableField<>(); public ObservableBoolean isShowAvatar = new ObservableBoolean(true); public ChatViewHolder(T itemView) { super(itemView.getRoot()); binding = itemView; } public void bind(Message message, Message messagePrevious) { this.messageCurrent = messageCurrent; // Show Date // Hiển thị thời gian khi 2 tin nhắn nằm ở 2 ngày khác nhau if (messagePrevious == null || DateUtils.compareTwoDate(messagePrevious.getCreatedAt(), messageCurrent.getCreatedAt(), DateUtils.TIMEZONE_FORMAT_YYYY_MM_DD)) { date.set(DateUtils.convertStringToStringFormat(messageCurrent.getCreatedAt(), DateUtils.TIMEZONE_FORMAT_MESSAGE, DateUtils.TIMEZONE_FORMAT_YYYY_MM_DD_VIEW)); } else { date.set(""); } // Show Avatar if (messagePrevious == null || messageCurrent.getUser() == null || messagePrevious.getUser() == null || messageCurrent.getUser().getId() != messagePrevious.getUser().getId()) { isShowAvatar.set(true); if (messageCurrent.getUser() != null) { imageAvatar.set(messageCurrent.getUser().getAvata()); fullName.set(TextUtils.isEmpty(messageCurrent.getUser().getName()) ? "User unknown" : messageCurrent.getUser().getName()); } else { imageAvatar.set(""); fullName.set("User unknown"); } } else { isShowAvatar.set(false); } // Show Time // Hiển thị thời gian gửi hoặc nhận tin nhắn if (messagePrevious == null || messagePrevious.getUser() == null || messageCurrent.getUser() == null || messageCurrent.getUser().getId() != messagePrevious.getUser().getId() || Math.abs(DateUtils.compareTwoTime(messagePrevious.getCreatedAt(), messageCurrent.getCreatedAt())) > 0) { time.set(DateUtils.convertStringToStringFormat(messageCurrent.getCreatedAt(), DateUtils.TIMEZONE_FORMAT_MESSAGE, DateUtils.TIMEZONE_FORMAT_HH_MM)); } else { time.set(""); } // Hiển thị nội dung tin nhắn văn bản content.set(messageCurrent.getMessage()); } }
Vậy là chúng ta đã hoàn thành phần viết code chính cho tính năng chát. Bây giờ sẽ quay lại file MainActivity.java để thêm phần code cho hàm sendMessage
public void sendMessage(View view) { long time = System.currentTimeMillis(); User user = User.fakeUser(); Message message = new Message(); message.setMessageId(time); message.setUser(user); message.setMessage(messageChat.get()); message.setMyMessage(time % 2 == 0); message.setCreatedAt(dateToString(time, "yyyy/MM/dd HH:mm:ss")); // Thêm item vào List và cập nhật lại hiển thị trên View messageList.add(0, message); chatAdapter.notifyItemInserted(0); } public String dateToString(long timestamp, String format) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format, Locale.getDefault()); Date date = new Date(timestamp); return simpleDateFormat.format(date); }
Đến đây gần như là chúng ta đã hoàn thành 1 tính năng chát cơ bản rồi. Hi vọng thông qua bài viết này các bạn có thể tự xây dựng cho mình 1 app chát đơn giản để nói chuyện với bạn bè.