Conditional message bubble corners

This commit is contained in:
Matéo Duparc 2021-06-22 15:45:31 +02:00
parent 5a2a9ccc4f
commit 05a9f5881d
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
3 changed files with 62 additions and 35 deletions

View File

@ -2,8 +2,9 @@ package sushi.hardcore.aira.adapters
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.PorterDuff import android.graphics.drawable.GradientDrawable
import android.graphics.PorterDuffColorFilter import android.os.Handler
import android.os.Looper
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -29,6 +30,8 @@ class ChatAdapter(
const val BUBBLE_MARGIN = 150 const val BUBBLE_MARGIN = 150
const val CONTAINER_PADDING = 40 const val CONTAINER_PADDING = 40
const val BUBBLE_VERTICAL_MARGIN = 40 const val BUBBLE_VERTICAL_MARGIN = 40
const val BUBBLE_CORNER_NORMAL = 50f
const val BUBBLE_CORNER_ARROW = 20f
} }
private val inflater: LayoutInflater = LayoutInflater.from(context) private val inflater: LayoutInflater = LayoutInflater.from(context)
@ -36,6 +39,9 @@ class ChatAdapter(
fun newMessage(chatItem: ChatItem) { fun newMessage(chatItem: ChatItem) {
chatItems.add(chatItem) chatItems.add(chatItem)
Handler(Looper.getMainLooper()).postDelayed({
notifyItemChanged(chatItems.size-2)
}, 100)
notifyItemInserted(chatItems.size-1) notifyItemInserted(chatItems.size-1)
} }
@ -50,11 +56,17 @@ class ChatAdapter(
} }
internal open class BubbleViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { internal open class BubbleViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
protected fun configureContainer(outgoing: Boolean, previousOutgoing: Boolean?) { private fun generateCorners(topLeft: Float, topRight: Float, bottomRight: Float, bottomLeft: Float): FloatArray {
return floatArrayOf(topLeft, topLeft, topRight, topRight, bottomRight, bottomRight, bottomLeft, bottomLeft)
}
protected fun configureContainer(outgoing: Boolean, previousOutgoing: Boolean?, isLast: Boolean) {
val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
if (previousOutgoing != null && previousOutgoing != outgoing) { if (previousOutgoing != null && previousOutgoing != outgoing) {
layoutParams.updateMargins(top = BUBBLE_VERTICAL_MARGIN) layoutParams.updateMargins(top = BUBBLE_VERTICAL_MARGIN)
} }
if (isLast) {
layoutParams.updateMargins(bottom = BUBBLE_VERTICAL_MARGIN)
}
itemView.layoutParams = layoutParams //set layoutParams anyway to reset margins if the view was recycled itemView.layoutParams = layoutParams //set layoutParams anyway to reset margins if the view was recycled
if (outgoing) { if (outgoing) {
itemView.updatePadding(right = CONTAINER_PADDING) itemView.updatePadding(right = CONTAINER_PADDING)
@ -62,7 +74,7 @@ class ChatAdapter(
itemView.updatePadding(left = CONTAINER_PADDING) itemView.updatePadding(left = CONTAINER_PADDING)
} }
} }
protected fun configureBubble(context: Context, outgoing: Boolean) { protected fun configureBubble(context: Context, outgoing: Boolean, previousOutgoing: Boolean?, nextOutgoing: Boolean?) {
val bubble = itemView.findViewById<LinearLayout>(R.id.bubble_content) val bubble = itemView.findViewById<LinearLayout>(R.id.bubble_content)
bubble.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { bubble.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
gravity = if (outgoing) { gravity = if (outgoing) {
@ -73,12 +85,32 @@ class ChatAdapter(
Gravity.START Gravity.START
} }
} }
if (!outgoing) { val backgroundDrawable = GradientDrawable()
bubble.background.colorFilter = PorterDuffColorFilter( backgroundDrawable.setColor(ContextCompat.getColor(context, if (outgoing) {
ContextCompat.getColor(context, R.color.incomingBubbleBackground), R.color.bubbleBackground
PorterDuff.Mode.SRC } else {
) R.color.incomingBubbleBackground
}))
var topLeft = BUBBLE_CORNER_NORMAL
var topRight = BUBBLE_CORNER_NORMAL
var bottomRight = BUBBLE_CORNER_NORMAL
var bottomLeft = BUBBLE_CORNER_NORMAL
if (nextOutgoing == outgoing) {
if (outgoing) {
bottomRight = BUBBLE_CORNER_ARROW
} else {
bottomLeft = BUBBLE_CORNER_ARROW
}
} }
if (previousOutgoing == outgoing) {
if (outgoing) {
topRight = BUBBLE_CORNER_ARROW
} else {
topLeft = BUBBLE_CORNER_ARROW
}
}
backgroundDrawable.cornerRadii = generateCorners(topLeft, topRight, bottomRight, bottomLeft)
bubble.background = backgroundDrawable
} }
protected fun setTimestamp(chatItem: ChatItem): TextView { protected fun setTimestamp(chatItem: ChatItem): TextView {
val calendar = Calendar.getInstance().apply { val calendar = Calendar.getInstance().apply {
@ -104,28 +136,28 @@ class ChatAdapter(
} }
internal class OutgoingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) { internal class OutgoingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) {
fun bind(chatItem: ChatItem, previousOutgoing: Boolean?) { fun bind(chatItem: ChatItem, previousOutgoing: Boolean?, nextOutgoing: Boolean?) {
setTimestamp(chatItem).apply { setTimestamp(chatItem).apply {
setTextColor(ContextCompat.getColor(context, R.color.outgoingTimestamp)) setTextColor(ContextCompat.getColor(context, R.color.outgoingTimestamp))
} }
configureBubble(context, true) configureBubble(context, true, previousOutgoing, nextOutgoing)
bindMessage(chatItem, true).apply { bindMessage(chatItem, true).apply {
setLinkTextColor(ContextCompat.getColor(context, R.color.outgoingTextLink)) setLinkTextColor(ContextCompat.getColor(context, R.color.outgoingTextLink))
} }
configureContainer(true, previousOutgoing) configureContainer(true, previousOutgoing, nextOutgoing == null)
} }
} }
internal class IncomingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) { internal class IncomingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) {
fun bind(chatItem: ChatItem, previousOutgoing: Boolean?) { fun bind(chatItem: ChatItem, previousOutgoing: Boolean?, nextOutgoing: Boolean?) {
setTimestamp(chatItem).apply { setTimestamp(chatItem).apply {
setTextColor(ContextCompat.getColor(context, R.color.incomingTimestamp)) setTextColor(ContextCompat.getColor(context, R.color.incomingTimestamp))
} }
configureBubble(context, false) configureBubble(context, false, previousOutgoing, nextOutgoing)
bindMessage(chatItem, false).apply { bindMessage(chatItem, false).apply {
setLinkTextColor(ContextCompat.getColor(context, R.color.incomingTextLink)) setLinkTextColor(ContextCompat.getColor(context, R.color.incomingTextLink))
} }
configureContainer(false, previousOutgoing) configureContainer(false, previousOutgoing, nextOutgoing == null)
} }
} }
@ -145,24 +177,24 @@ class ChatAdapter(
} }
internal class OutgoingFileViewHolder(private val context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(itemView, onSavingFile) { internal class OutgoingFileViewHolder(private val context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(itemView, onSavingFile) {
fun bind(chatItem: ChatItem, previousOutgoing: Boolean?) { fun bind(chatItem: ChatItem, previousOutgoing: Boolean?, nextOutgoing: Boolean?) {
setTimestamp(chatItem).apply { setTimestamp(chatItem).apply {
setTextColor(ContextCompat.getColor(context, R.color.outgoingTimestamp)) setTextColor(ContextCompat.getColor(context, R.color.outgoingTimestamp))
} }
bindFile(chatItem, true) bindFile(chatItem, true)
configureBubble(context, true) configureBubble(context, true, previousOutgoing, nextOutgoing)
configureContainer(true, previousOutgoing) configureContainer(true, previousOutgoing, nextOutgoing == null)
} }
} }
internal class IncomingFileViewHolder(private val context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(itemView, onSavingFile) { internal class IncomingFileViewHolder(private val context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(itemView, onSavingFile) {
fun bind(chatItem: ChatItem, previousOutgoing: Boolean?) { fun bind(chatItem: ChatItem, previousOutgoing: Boolean?, nextOutgoing: Boolean?) {
setTimestamp(chatItem).apply { setTimestamp(chatItem).apply {
setTextColor(ContextCompat.getColor(context, R.color.incomingTimestamp)) setTextColor(ContextCompat.getColor(context, R.color.incomingTimestamp))
} }
bindFile(chatItem, false) bindFile(chatItem, false)
configureBubble(context, false) configureBubble(context, false, previousOutgoing, nextOutgoing)
configureContainer(false, previousOutgoing) configureContainer(false, previousOutgoing, nextOutgoing == null)
} }
} }
@ -191,11 +223,16 @@ class ChatAdapter(
} else { } else {
chatItems[position-1].outgoing chatItems[position-1].outgoing
} }
val nextOutgoing = if (position == chatItems.size - 1) {
null
} else {
chatItems[position+1].outgoing
}
when (chatItem.itemType) { when (chatItem.itemType) {
ChatItem.OUTGOING_MESSAGE -> (holder as OutgoingMessageViewHolder).bind(chatItem, previousOutgoing) ChatItem.OUTGOING_MESSAGE -> (holder as OutgoingMessageViewHolder).bind(chatItem, previousOutgoing, nextOutgoing)
ChatItem.INCOMING_MESSAGE -> (holder as IncomingMessageViewHolder).bind(chatItem, previousOutgoing) ChatItem.INCOMING_MESSAGE -> (holder as IncomingMessageViewHolder).bind(chatItem, previousOutgoing, nextOutgoing)
ChatItem.OUTGOING_FILE -> (holder as OutgoingFileViewHolder).bind(chatItem, previousOutgoing) ChatItem.OUTGOING_FILE -> (holder as OutgoingFileViewHolder).bind(chatItem, previousOutgoing, nextOutgoing)
ChatItem.INCOMING_FILE -> (holder as IncomingFileViewHolder).bind(chatItem, previousOutgoing) ChatItem.INCOMING_FILE -> (holder as IncomingFileViewHolder).bind(chatItem, previousOutgoing, nextOutgoing)
} }
} }

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/bubbleBackground"/>
<corners android:radius="20dp"/>
</shape>
</item>
</selector>

View File

@ -7,7 +7,6 @@
<item name="android:paddingBottom">8dp</item> <item name="android:paddingBottom">8dp</item>
</style> </style>
<style name="Bubble"> <style name="Bubble">
<item name="android:background">@drawable/background_adapter_bubble</item>
<item name="android:paddingTop">10dp</item> <item name="android:paddingTop">10dp</item>
<item name="android:paddingBottom">10dp</item> <item name="android:paddingBottom">10dp</item>
<item name="android:paddingStart">15dp</item> <item name="android:paddingStart">15dp</item>