텍스트 메시지를 주고 받는 기능을 추가해보았으니, 이제 사진을 주고 받는 기능도 추가해보도록 하겠습니다. 채팅 구현이 어렵다는 인식이 많아 이미지 전송까지는 엄두도 못내는 경우가 많은 것 같은데, 사실 일반 텍스트 메시지 전송과 크게 다르지 않습니다. 물론, 이미지 전송을 텍스트 전송과 비슷하게 구현하는 것이 효율적인 방법은 아니겠지만, 효율성은 차치하고 이미지 전송을 구현하는 데에 초점을 맞추는 것으로 하겠습니다.
① ChatRoomController에 아래의 API를 추가한다.
// 이미지 S3 업로드
@PostMapping("/image")
public BaseResponse<String> uploadImage(@RequestPart(value = "image", required = false) MultipartFile multipartFile) {
try {
return new BaseResponse<>(chatRoomService.uploadImage(multipartFile));
} catch (BaseException exception) {
return new BaseResponse<>(exception.getStatus());
}
}
② ChatRoomService에 아래의 메서드를 추가한다.
@Transactional
public String uploadImage(MultipartFile multipartFile) throws BaseException {
try {
if(multipartFile == null) {
throw new BaseException(BaseResponseStatus.REQUEST_ERROR);
}
GetS3Res getS3Res = s3Service.uploadSingleFile(multipartFile);
return getS3Res.getImgUrl();
} catch (BaseException exception) {
throw new BaseException(exception.getStatus());
}
}
① layout 디렉토리 하위로, image_recycler_view_item, image_recycler_view_item2를 추가한다.
② drawable 디렉토리 하위로, broken_image라는 이름의 깨진 이미지 아이콘을 추가한다.
③ ImageView에 round corner를 적용하기 위해 drawable 디렉토리 하위로 round_ractangle이라는 이름의 리소스 파일을 추가한다.
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/white"/>
<corners android:radius="15dp"/>
</shape>
</item>
<item android:right="3dp" android:left="3dp" android:top="3dp" android:bottom="3dp">
<shape android:shape="rectangle">
<solid android:color="@color/white"/>
<corners android:radius="14dp"/>
</shape>
</item>
</layer-list>
④ image_recycler_view_item.xml에 아래의 내용을 입력한다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageContentsArea"
android:layout_width="230dp"
android:layout_height="wrap_content"
android:layout_marginTop="37dp"
android:layout_marginEnd="8dp"
android:src="@drawable/broken_image"
android:layout_gravity="left"
android:background="@drawable/round_ractangle"
android:ellipsize="end"
android:maxWidth="230dp"
app:layout_constraintEnd_toStartOf="@+id/imageProfileArea"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/imageUnreadUserCountContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/imageContentsArea">
<TextView
android:id="@+id/imageUnreadUserCountTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="5dp"
android:layout_marginBottom="20dp"
android:gravity="center"
android:text="10"
android:textColor="#ffd400"
android:textSize="10sp" />
</FrameLayout>
<TextView
android:id="@+id/imageDateTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginEnd="15dp"
android:text="9/7 7:01 AM"
android:textSize="10sp"
app:layout_constraintEnd_toStartOf="@+id/imageProfileArea"
app:layout_constraintTop_toBottomOf="@+id/imageContentsArea" />
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/imageProfileArea"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="15dp"
android:layout_marginRight="15dp"
android:src="@drawable/profile"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:civ_border_color="@color/skyBlue"
app:civ_border_overlay="true"
app:civ_border_width="1dp" />
<TextView
android:id="@+id/imageNickName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:layout_marginBottom="2dp"
android:gravity="center"
android:text="nickname"
android:textColor="@color/black"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@+id/imageContentsArea"
app:layout_constraintEnd_toStartOf="@+id/imageProfileArea" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
⑤ image_recycler_view_item2.xml에 아래의 내용을 입력한다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageContentsArea2"
android:layout_width="230dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="37dp"
android:src="@drawable/broken_image"
android:background="@drawable/round_ractangle"
android:ellipsize="end"
android:maxWidth="230dp"
app:layout_constraintStart_toEndOf="@+id/imageProfileArea2"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/imageUnreadUserCountContainer2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageContentsArea2">
<TextView
android:id="@+id/imageUnreadUserCountTextView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="5dp"
android:layout_marginBottom="20dp"
android:gravity="center"
android:text="10"
android:textColor="#ffd400"
android:textSize="10sp" />
</FrameLayout>
<TextView
android:id="@+id/imageDateTime2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="9/7 7:01 AM"
android:textSize="10sp"
android:layout_marginTop="5dp"
android:layout_marginStart="15dp"
app:layout_constraintStart_toEndOf="@+id/imageProfileArea2"
app:layout_constraintTop_toBottomOf="@+id/imageContentsArea2" />
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/imageProfileArea2"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="15dp"
android:layout_marginLeft="15dp"
android:src="@drawable/profile"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:civ_border_color="@color/skyBlue"
app:civ_border_overlay="true"
app:civ_border_width="1dp" />
<TextView
android:id="@+id/imageNickName2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginBottom="2dp"
android:gravity="center"
android:text="nickname"
android:textColor="@color/black"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@+id/imageContentsArea2"
app:layout_constraintStart_toEndOf="@+id/imageProfileArea2" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
⑥ MessageModel에 imageUrl 필드를 추가하고, viewType도 수정한다.
data class MessageModel (
val senderUid : String = "",
val senderNickName : String = "",
val senderProfileUrl : String = "",
val contents : String = "",
val sendTime : String = "",
val readerUids: MutableMap<String, Boolean> = mutableMapOf(), // 메시지 읽은 유저의 UID
var unreadUserCount : Int = 0, // 메시지를 읽지 않은 사람의 수
val imageUrl : String? = "null",
var viewType: Int = VIEW_TYPE_ME
) {
companion object {
const val VIEW_TYPE_YOU = 0
const val VIEW_TYPE_ME = 1
const val VIEW_TYPE_YOU_IMAGE = 3
const val VIEW_TYPE_ME_IMAGE = 4
}
}
⑦ MessageAdapter에 아래의 내용을 입력한다.
class MessageAdapter(private val context: Context, val items: MutableList<MessageModel>)
: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class ItemViewHolder1(private val view: View) : RecyclerView.ViewHolder(view) {
val contents : TextView = view.findViewById(R.id.messageContentsArea)
val dateTime : TextView = view.findViewById(R.id.dateTime)
val nickName : TextView = view.findViewById(R.id.messageNickName)
val profile : ImageView = view.findViewById(R.id.messageProfileArea)
val unreadUserContainer : FrameLayout = view.findViewById(R.id.unreadUserCountContainer)
val unreadUserCount : TextView = view.findViewById(R.id.unreadUserCountTextView)
}
class ItemViewHolder2(private val view: View) : RecyclerView.ViewHolder(view) {
val contents : TextView = view.findViewById(R.id.messageContentsArea2)
val dateTime : TextView = view.findViewById(R.id.dateTime2)
val nickName : TextView = view.findViewById(R.id.messageNickName2)
val profile : ImageView = view.findViewById(R.id.messageProfileArea2)
val unreadUserContainer : FrameLayout = view.findViewById(R.id.unreadUserCountContainer2)
val unreadUserCount : TextView = view.findViewById(R.id.unreadUserCountTextView2)
}
class ItemViewHolder3(private val view: View) : RecyclerView.ViewHolder(view) {
val image : ImageView = view.findViewById(R.id.imageContentsArea)
val dateTime : TextView = view.findViewById(R.id.imageDateTime)
val nickName : TextView = view.findViewById(R.id.imageNickName)
val profile : ImageView = view.findViewById(R.id.imageProfileArea)
val unreadUserContainer : FrameLayout = view.findViewById(R.id.imageUnreadUserCountContainer)
val unreadUserCount : TextView = view.findViewById(R.id.imageUnreadUserCountTextView)
}
class ItemViewHolder4(private val view: View) : RecyclerView.ViewHolder(view) {
val image : ImageView = view.findViewById(R.id.imageContentsArea2)
val dateTime : TextView = view.findViewById(R.id.imageDateTime2)
val nickName : TextView = view.findViewById(R.id.imageNickName2)
val profile : ImageView = view.findViewById(R.id.imageProfileArea2)
val unreadUserContainer : FrameLayout = view.findViewById(R.id.imageUnreadUserCountContainer2)
val unreadUserCount : TextView = view.findViewById(R.id.imageUnreadUserCountTextView2)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val adapterLayout : View?
return when(viewType) {
MessageModel.VIEW_TYPE_ME -> {
adapterLayout = LayoutInflater.from(parent.context).inflate(R.layout.message_recycler_view_item, parent, false)
ItemViewHolder1(adapterLayout)
}
MessageModel.VIEW_TYPE_YOU -> {
adapterLayout = LayoutInflater.from(parent.context).inflate(R.layout.message_recycler_view_item2, parent, false)
ItemViewHolder2(adapterLayout)
}
MessageModel.VIEW_TYPE_ME_IMAGE -> {
adapterLayout = LayoutInflater.from(parent.context).inflate(R.layout.image_recycler_view_item, parent, false)
ItemViewHolder3(adapterLayout)
}
MessageModel.VIEW_TYPE_YOU_IMAGE -> {
adapterLayout = LayoutInflater.from(parent.context).inflate(R.layout.image_recycler_view_item2, parent, false)
ItemViewHolder4(adapterLayout)
}
else -> throw RuntimeException("Invalid View Type Error")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = items[position]
when (item.viewType) {
MessageModel.VIEW_TYPE_ME -> {
(holder as ItemViewHolder1).contents.text = item.contents
holder.dateTime.text = item.sendTime
holder.nickName.text = item.senderNickName
holder.unreadUserCount.text = item.unreadUserCount.toString()
if (item.unreadUserCount.toString().toInt() > 0) {
holder.unreadUserContainer!!.visibility = View.VISIBLE
holder.unreadUserCount.visibility = View.VISIBLE
} else {
holder.unreadUserContainer!!.visibility = View.GONE
holder.unreadUserCount.visibility = View.GONE
}
if (item.senderProfileUrl != "null") {
Glide.with(context)
.load(item.senderProfileUrl)
.into(holder.profile)
} else {
holder.profile.setImageResource(R.drawable.profile)
}
holder.itemView.setOnClickListener {
itemClickListener.onClick(it, position)
}
}
MessageModel.VIEW_TYPE_YOU -> {
(holder as ItemViewHolder2).contents.text = item.contents
holder.dateTime.text = item.sendTime
holder.nickName.text = item.senderNickName
holder.unreadUserCount.text = item.unreadUserCount.toString()
if (item.unreadUserCount.toString().toInt() > 0) {
holder.unreadUserContainer!!.visibility = View.VISIBLE
holder.unreadUserCount.visibility = View.VISIBLE
} else {
holder.unreadUserContainer!!.visibility = View.GONE
holder.unreadUserCount.visibility = View.GONE
}
if (item.senderProfileUrl != "null") {
Glide.with(context)
.load(item.senderProfileUrl)
.into(holder.profile)
} else {
holder.profile.setImageResource(R.drawable.profile)
}
holder.itemView.setOnClickListener {
itemClickListener.onClick(it, position)
}
}
MessageModel.VIEW_TYPE_ME_IMAGE -> {
(holder as ItemViewHolder3).dateTime.text = item.sendTime
holder.nickName.text = item.senderNickName
holder.unreadUserCount.text = item.unreadUserCount.toString()
if(item.imageUrl != "null") {
Glide.with(context)
.load(item.imageUrl)
.into(holder.image)
holder.image.clipToOutline = true
} else {
holder.profile.setImageResource(R.drawable.broken_image)
}
if (item.unreadUserCount.toString().toInt() > 0) {
holder.unreadUserContainer!!.visibility = View.VISIBLE
holder.unreadUserCount.visibility = View.VISIBLE
} else {
holder.unreadUserContainer!!.visibility = View.GONE
holder.unreadUserCount.visibility = View.GONE
}
if (item.senderProfileUrl != "null") {
Glide.with(context)
.load(item.senderProfileUrl)
.into(holder.profile)
} else {
holder.profile.setImageResource(R.drawable.profile)
}
holder.itemView.setOnClickListener {
itemClickListener.onClick(it, position)
}
}
MessageModel.VIEW_TYPE_YOU_IMAGE -> {
(holder as ItemViewHolder4).dateTime.text = item.sendTime
holder.nickName.text = item.senderNickName
holder.unreadUserCount.text = item.unreadUserCount.toString()
if (item.unreadUserCount.toString().toInt() > 0) {
holder.unreadUserContainer!!.visibility = View.VISIBLE
holder.unreadUserCount.visibility = View.VISIBLE
} else {
holder.unreadUserContainer!!.visibility = View.GONE
holder.unreadUserCount.visibility = View.GONE
}
if (item.senderProfileUrl != "null") {
Glide.with(context)
.load(item.senderProfileUrl)
.into(holder.profile)
} else {
holder.profile.setImageResource(R.drawable.profile)
}
if(item.imageUrl != "null") {
Glide.with(context)
.load(item.imageUrl)
.into(holder.image)
holder.image.clipToOutline = true
} else {
holder.profile.setImageResource(R.drawable.broken_image)
}
holder.itemView.setOnClickListener {
itemClickListener.onClick(it, position)
}
}
}
}
...
⑧ 이미지 전송에 사용할 아이콘을 send_image라는 이름으로 drawable 디렉토리 하위에 넣는다. 이후 activity_chat_room.xml 파일에 이미지 전송 버튼을 추가한다.
<ImageView
android:id="@+id/send_image"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginLeft="10dp"
android:src="@drawable/send_image"
android:layout_gravity="center"/>
⑨ ChatApi에 아래의 API를 추가한다.
@Multipart
@POST("/chat/image")
suspend fun uploadImage(
@Part image : MultipartBody.Part?
) : BaseResponse<String>
⑩ 이제 ChatRoomActivity를 아래와 같이 수정한다.
...
val sendImageBtn = findViewById<ImageView>(R.id.send_image)
val getAction = registerForActivityResult(
ActivityResultContracts.GetContent(),
ActivityResultCallback { uri ->
CoroutineScope(Dispatchers.IO).launch {
val response = uploadImage(uri)
if (response.isSuccess) {
val selectedImageUri = response.result!!
Log.d("selectedImageUri", selectedImageUri)
val sendTime = getSendTime()
CoroutineScope(Dispatchers.IO).launch {
val response = getUserInfo(myUid)
if (response.isSuccess) {
myNickName = response.result?.nickName.toString()
myProfileUrl = response.result?.imgUrl.toString()
val readerUids = mutableMapOf<String, Boolean>()
Log.d("selectedImageUriChk", selectedImageUri)
messageModel = MessageModel(myUid, myNickName, myProfileUrl, "이미지",
sendTime, readerUids, 0, selectedImageUri)
Log.d("readerUid", readerUids.size.toString())
FirebaseRef.message.child(chatRoomId!!).push().setValue(messageModel)
val connectedUidsRef = FirebaseRef.connected.child(chatRoomId!!)
val connectedUidsList: MutableList<String> = mutableListOf() // 현재 접속 중인 유저의 UID 목록
connectedUidsRef.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
for (childSnapshot in dataSnapshot.children) { // 난수 값
val originalString = childSnapshot.value.toString()
Log.d("originalString", originalString)
// originalString이 {sRuRu1YVJMSj4csAUPOmaFfKfWJ2=true}와 같이 나오므로,
// 앞에서 1글자, 뒤에서 6글자를 제거해 UID만 추출
val modifiedString = originalString.substring(1, originalString.length - 6)
Log.d("modifiedString", modifiedString)
connectedUidsList.add(modifiedString)
Log.d("connectedUidsList", connectedUidsList.toString())
}
CoroutineScope(Dispatchers.IO).launch {
val response = getUserUidList(chatRoomId!!) // 채팅방에 참여한 모든 유저의 UID 목록
Log.d("userUidList", response.toString())
if (response.isSuccess) {
val uidList = response.result!!
Log.d("userUidList", response.result.toString())
Log.d("userUidList", uidList.toString())
val difference = uidList.subtract(connectedUidsList) // 차집합 List
Log.d("difference", connectedUidsList.toString())
Log.d("difference", difference.toString())
val deviceTokenList = mutableListOf<String>()
val addList = difference.map { uid ->
async(Dispatchers.IO) {
val response = getDeviceTokenByUid(uid)
if (response.isSuccess) {
deviceTokenList.add(response.result.toString())
} else {
Log.d("ChatRoomActivity", "디바이스 토큰 정보를 불러오지 못함")
}
}
}
// 모든 작업이 완료될 때까지 대기
addList.awaitAll()
Log.d("deviceTokenList", deviceTokenList.toString())
val noticeModel = NoticeModel(myNickName, "이미지 파일")
for (token in deviceTokenList) {
val pushNotice = PushNotice(noticeModel, token)
Log.d("Push", pushNotice.toString())
Log.d("Push", tokenList.toString())
createNotificationChannel()
pushNotification(pushNotice)
}
} else {
Log.d("userUidList", "유저의 정보를 불러오지 못함")
}
}
}
override fun onCancelled(databaseError: DatabaseError) {
// 처리 중 오류 발생 시 처리
}
})
} else {
Log.d("ChatRoomActivity", "유저의 정보를 불러오지 못함")
}
}
message.text?.clear()
} else {
Log.d("ChatRoomActivity", "이미지 정보를 불러오지 못함")
}
}
}
)
sendImageBtn.setOnClickListener {
getAction.launch("image/*")
}
}
private suspend fun uploadImage(uri: Uri?) : BaseResponse<String> {
val imagePath = getImagePathFromUri(uri!!)
val imageFile = imagePath?.let { File(it) }
Log.d("ProfileActivity", "path : " + imagePath.toString())
Log.d("ProfileActivity", "file : " + imageFile.toString())
val requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), imageFile)
val imagePart = MultipartBody.Part.createFormData("image", imageFile?.name, requestFile)
return RetrofitInstance.chatApi.uploadImage(imagePart)
}
private fun getImagePathFromUri(uri: Uri): String? {
val projection = arrayOf(MediaStore.Images.Media.DATA)
val cursor = contentResolver.query(uri, projection, null, null, null)
cursor?.moveToFirst()
val columnIndex = cursor?.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val imagePath = cursor?.getString(columnIndex!!)
cursor?.close()
return imagePath
}
이제 코드를 실행시켜보자. 이미지가 잘 전송될 것이며, 기존 텍스트 메시지와 마찬가지로, 읽지 않은 유저의 수가 나타나고 푸시 알림이 전송될 것이다. 그리고 아래와 같이 사진의 크기에 알맞게 메시지 박스의 크기가 잘 조절되어야 한다. (물론, 극단적으로 얇고 긴 사진의 경우는 알맞게 안 나오겠지만, 일반적인 사진을 담는 데에는 무리가 없어야 한다.)
이미지를 받는 입장에서는 메시지가 좌측에 표시된다. 또한, 채팅방 목록에서 마지막 메시지를 보여줄 때, 마지막 메시지가 이미지 파일이면, "이미지"라고 뜨게 된다.
현재 상태에서 메지지를 클릭하면 메시지를 전송한 유저의 세부 정보 화면으로 전환된다. 이는 RecyclerView 전체에 클릭 이벤트 리스너를 부여했기 때문이다. 프로필을 클릭하면 유저의 세부 정보 화면으로 전환되고, 메시지(이미지)를 클릭하면 이미지 크게 보기 화면(ProfileImageActivity)으로 전환되게 만들자.
① MessageAdapter의 onBindViewHolder를 아래와 같이 수정한다.
class MessageAdapter(private val context: Context, val items: MutableList<MessageModel>)
: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class ItemViewHolder1(private val view: View) : RecyclerView.ViewHolder(view) {
val contents : TextView = view.findViewById(R.id.messageContentsArea)
val dateTime : TextView = view.findViewById(R.id.dateTime)
val nickName : TextView = view.findViewById(R.id.messageNickName)
val profile : ImageView = view.findViewById(R.id.messageProfileArea)
val unreadUserContainer : FrameLayout = view.findViewById(R.id.unreadUserCountContainer)
val unreadUserCount : TextView = view.findViewById(R.id.unreadUserCountTextView)
}
class ItemViewHolder2(private val view: View) : RecyclerView.ViewHolder(view) {
val contents : TextView = view.findViewById(R.id.messageContentsArea2)
val dateTime : TextView = view.findViewById(R.id.dateTime2)
val nickName : TextView = view.findViewById(R.id.messageNickName2)
val profile : ImageView = view.findViewById(R.id.messageProfileArea2)
val unreadUserContainer : FrameLayout = view.findViewById(R.id.unreadUserCountContainer2)
val unreadUserCount : TextView = view.findViewById(R.id.unreadUserCountTextView2)
}
class ItemViewHolder3(private val view: View) : RecyclerView.ViewHolder(view) {
val image : ImageView = view.findViewById(R.id.imageContentsArea)
val dateTime : TextView = view.findViewById(R.id.imageDateTime)
val nickName : TextView = view.findViewById(R.id.imageNickName)
val profile : ImageView = view.findViewById(R.id.imageProfileArea)
val unreadUserContainer : FrameLayout = view.findViewById(R.id.imageUnreadUserCountContainer)
val unreadUserCount : TextView = view.findViewById(R.id.imageUnreadUserCountTextView)
}
class ItemViewHolder4(private val view: View) : RecyclerView.ViewHolder(view) {
val image : ImageView = view.findViewById(R.id.imageContentsArea2)
val dateTime : TextView = view.findViewById(R.id.imageDateTime2)
val nickName : TextView = view.findViewById(R.id.imageNickName2)
val profile : ImageView = view.findViewById(R.id.imageProfileArea2)
val unreadUserContainer : FrameLayout = view.findViewById(R.id.imageUnreadUserCountContainer2)
val unreadUserCount : TextView = view.findViewById(R.id.imageUnreadUserCountTextView2)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val adapterLayout : View?
return when(viewType) {
MessageModel.VIEW_TYPE_ME -> {
adapterLayout = LayoutInflater.from(parent.context).inflate(R.layout.message_recycler_view_item, parent, false)
ItemViewHolder1(adapterLayout)
}
MessageModel.VIEW_TYPE_YOU -> {
adapterLayout = LayoutInflater.from(parent.context).inflate(R.layout.message_recycler_view_item2, parent, false)
ItemViewHolder2(adapterLayout)
}
MessageModel.VIEW_TYPE_ME_IMAGE -> {
adapterLayout = LayoutInflater.from(parent.context).inflate(R.layout.image_recycler_view_item, parent, false)
ItemViewHolder3(adapterLayout)
}
MessageModel.VIEW_TYPE_YOU_IMAGE -> {
adapterLayout = LayoutInflater.from(parent.context).inflate(R.layout.image_recycler_view_item2, parent, false)
ItemViewHolder4(adapterLayout)
}
else -> throw RuntimeException("Invalid View Type Error")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = items[position]
when (item.viewType) {
MessageModel.VIEW_TYPE_ME -> {
(holder as ItemViewHolder1).contents.text = item.contents
holder.dateTime.text = item.sendTime
holder.nickName.text = item.senderNickName
holder.unreadUserCount.text = item.unreadUserCount.toString()
if (item.unreadUserCount.toString().toInt() > 0) {
holder.unreadUserContainer!!.visibility = View.VISIBLE
holder.unreadUserCount.visibility = View.VISIBLE
} else {
holder.unreadUserContainer!!.visibility = View.GONE
holder.unreadUserCount.visibility = View.GONE
}
if (item.senderProfileUrl != "null") {
Glide.with(context)
.load(item.senderProfileUrl)
.into(holder.profile)
} else {
holder.profile.setImageResource(R.drawable.profile)
}
holder.profile.setOnClickListener {
profileClickListener.onClick(it, position)
}
}
MessageModel.VIEW_TYPE_YOU -> {
(holder as ItemViewHolder2).contents.text = item.contents
holder.dateTime.text = item.sendTime
holder.nickName.text = item.senderNickName
holder.unreadUserCount.text = item.unreadUserCount.toString()
if (item.unreadUserCount.toString().toInt() > 0) {
holder.unreadUserContainer!!.visibility = View.VISIBLE
holder.unreadUserCount.visibility = View.VISIBLE
} else {
holder.unreadUserContainer!!.visibility = View.GONE
holder.unreadUserCount.visibility = View.GONE
}
if (item.senderProfileUrl != "null") {
Glide.with(context)
.load(item.senderProfileUrl)
.into(holder.profile)
} else {
holder.profile.setImageResource(R.drawable.profile)
}
holder.profile.setOnClickListener {
profileClickListener.onClick(it, position)
}
}
MessageModel.VIEW_TYPE_ME_IMAGE -> {
(holder as ItemViewHolder3).dateTime.text = item.sendTime
holder.nickName.text = item.senderNickName
holder.unreadUserCount.text = item.unreadUserCount.toString()
if(item.imageUrl != "null") {
Glide.with(context)
.load(item.imageUrl)
.into(holder.image)
holder.image.clipToOutline = true
} else {
holder.profile.setImageResource(R.drawable.broken_image)
}
if (item.unreadUserCount.toString().toInt() > 0) {
holder.unreadUserContainer!!.visibility = View.VISIBLE
holder.unreadUserCount.visibility = View.VISIBLE
} else {
holder.unreadUserContainer!!.visibility = View.GONE
holder.unreadUserCount.visibility = View.GONE
}
if (item.senderProfileUrl != "null") {
Glide.with(context)
.load(item.senderProfileUrl)
.into(holder.profile)
} else {
holder.profile.setImageResource(R.drawable.profile)
}
holder.profile.setOnClickListener {
profileClickListener.onClick(it, position)
}
holder.image.setOnClickListener {
imageClickListener.onClick(it, position)
}
}
MessageModel.VIEW_TYPE_YOU_IMAGE -> {
(holder as ItemViewHolder4).dateTime.text = item.sendTime
holder.nickName.text = item.senderNickName
holder.unreadUserCount.text = item.unreadUserCount.toString()
if (item.unreadUserCount.toString().toInt() > 0) {
holder.unreadUserContainer!!.visibility = View.VISIBLE
holder.unreadUserCount.visibility = View.VISIBLE
} else {
holder.unreadUserContainer!!.visibility = View.GONE
holder.unreadUserCount.visibility = View.GONE
}
if (item.senderProfileUrl != "null") {
Glide.with(context)
.load(item.senderProfileUrl)
.into(holder.profile)
} else {
holder.profile.setImageResource(R.drawable.profile)
}
if(item.imageUrl != "null") {
Glide.with(context)
.load(item.imageUrl)
.into(holder.image)
holder.image.clipToOutline = true
} else {
holder.profile.setImageResource(R.drawable.broken_image)
}
holder.profile.setOnClickListener {
profileClickListener.onClick(it, position)
}
holder.image.setOnClickListener {
imageClickListener.onClick(it, position)
}
}
}
}
override fun getItemCount(): Int {
return items.size
}
override fun getItemViewType(position: Int): Int {
return items[position].viewType
}
interface OnItemClickListener {
fun onClick(view : View, position : Int)
}
fun setProfileClickListener(onItemClickListener: OnItemClickListener) {
this.profileClickListener = onItemClickListener
}
fun setImageClickListener(onItemClickListener: OnItemClickListener) {
this.imageClickListener = onItemClickListener
}
private lateinit var profileClickListener : OnItemClickListener
private lateinit var imageClickListener : OnItemClickListener
}
② ChatRoomActivity를 아래와 같이 수정한다.
messageAdapter.setProfileClickListener(object : MessageAdapter.OnItemClickListener {
override fun onClick(view: View, position: Int) {
val uid = messageList!![position].senderUid
val imgUrl = messageList!![position].senderProfileUrl
val nickName = messageList!![position].senderNickName
var intent : Intent? = null
if(uid == FirebaseAuthUtils.getUid()) {
intent = Intent(this@ChatRoomActivity, MyProfileActivity::class.java)
intent.putExtra("imgUrl", imgUrl)
intent.putExtra("nickName", nickName)
}
else {
intent = Intent(this@ChatRoomActivity, UserDetailActivity::class.java)
intent.putExtra("uid", uid)
intent.putExtra("imgUrl", imgUrl)
intent.putExtra("nickName", nickName)
}
startActivity(intent)
}
})
messageAdapter.setImageClickListener(object : MessageAdapter.OnItemClickListener {
override fun onClick(view: View, position: Int) {
val imgUrl = messageList!![position].imageUrl
val intent = Intent(this@ChatRoomActivity, ProfileImageActivity::class.java)
intent.putExtra("imgUrl", imgUrl)
startActivity(intent)
}
})
코드를 실행해보면, 메시지의 프로필을 클릭하면 유저의 상세 정보 화면으로 이동되고, 이미지를 클릭하면 이미지를 크게 보는 화면으로 이동될 것이다. 아래는 이미지를 클릭했을 때 나타나는 화면이다.
① settings.gradle의 dependencyResolutionManagement > repositories에 아래의 코드를 추가한다.
maven { url = uri("https://jitpack.io") }
② Module 수준의 build.gradle에 아래의 의존성을 추가한다.
implementation("com.github.chrisbanes:PhotoView:2.3.0")
③ activity_profile_image.xml의 ImageView를 PhotoView로 바꿔주자.
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/profileImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/profile"/>
코드를 실행시켜보면, 두 손가락으로 화면을 줌인 줌아웃할 수 있게 된다.
현재는 S3에 올라간 이미지를 DB에 저장하고 있지 않다. 조금 더 완성도를 높이고 싶다면, 채팅방에 업로드한 이미지를 저장하는 Entity를 만들어 채팅방과 일대다 관계를 매핑해주어야 할 것이다. 그래서 채팅방에 아무도 남아있지 않게 될 때, 채팅방에서 주고 받은 사진을 모두 S3와 DB에서 삭제하고, (당연히 메시지도 삭제하고) 채팅방이 삭제되도록 처리해야 할 것이다.
하지만, 이 부분은 굳이 코드를 보여주지 않더라도, 지금까지의 내용을 잘 이해하였으면 충분히 혼자 해볼 수 있을 것이다. 따라서 이 내용은 생략하기로 한다.