Add several performance improvements to ConversationItemV2.
This commit is contained in:
parent
32ae4393e2
commit
4494d8652d
10 changed files with 342 additions and 128 deletions
|
@ -95,9 +95,11 @@ class BadgeImageView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearDrawable() {
|
private fun clearDrawable() {
|
||||||
|
if (drawable != null) {
|
||||||
setImageDrawable(null)
|
setImageDrawable(null)
|
||||||
isClickable = false
|
isClickable = false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getGlideRequests(): GlideRequests? {
|
private fun getGlideRequests(): GlideRequests? {
|
||||||
return try {
|
return try {
|
||||||
|
|
|
@ -59,9 +59,11 @@ class BadgeSpriteTransformation(
|
||||||
return outBitmap
|
return outBitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Size(val code: String, val frameMap: Map<Density, FrameSet>) {
|
enum class Size(val code: String) {
|
||||||
SMALL(
|
SMALL(
|
||||||
"small",
|
"small"
|
||||||
|
) {
|
||||||
|
override val frameMap: Map<Density, FrameSet> by lazy {
|
||||||
mapOf(
|
mapOf(
|
||||||
Density.LDPI to FrameSet(Frame(124, 1, 12, 12), Frame(145, 31, 12, 12)),
|
Density.LDPI to FrameSet(Frame(124, 1, 12, 12), Frame(145, 31, 12, 12)),
|
||||||
Density.MDPI to FrameSet(Frame(163, 1, 16, 16), Frame(189, 39, 16, 16)),
|
Density.MDPI to FrameSet(Frame(163, 1, 16, 16), Frame(189, 39, 16, 16)),
|
||||||
|
@ -70,9 +72,12 @@ class BadgeSpriteTransformation(
|
||||||
Density.XXHDPI to FrameSet(Frame(483, 1, 48, 48), Frame(557, 111, 48, 48)),
|
Density.XXHDPI to FrameSet(Frame(483, 1, 48, 48), Frame(557, 111, 48, 48)),
|
||||||
Density.XXXHDPI to FrameSet(Frame(643, 1, 64, 64), Frame(741, 147, 64, 64))
|
Density.XXXHDPI to FrameSet(Frame(643, 1, 64, 64), Frame(741, 147, 64, 64))
|
||||||
)
|
)
|
||||||
),
|
}
|
||||||
|
},
|
||||||
MEDIUM(
|
MEDIUM(
|
||||||
"medium",
|
"medium"
|
||||||
|
) {
|
||||||
|
override val frameMap: Map<Density, FrameSet> by lazy {
|
||||||
mapOf(
|
mapOf(
|
||||||
Density.LDPI to FrameSet(Frame(124, 16, 18, 18), Frame(160, 31, 18, 18)),
|
Density.LDPI to FrameSet(Frame(124, 16, 18, 18), Frame(160, 31, 18, 18)),
|
||||||
Density.MDPI to FrameSet(Frame(163, 19, 24, 24), Frame(207, 39, 24, 24)),
|
Density.MDPI to FrameSet(Frame(163, 19, 24, 24), Frame(207, 39, 24, 24)),
|
||||||
|
@ -81,9 +86,12 @@ class BadgeSpriteTransformation(
|
||||||
Density.XXHDPI to FrameSet(Frame(483, 51, 72, 72), Frame(607, 111, 72, 72)),
|
Density.XXHDPI to FrameSet(Frame(483, 51, 72, 72), Frame(607, 111, 72, 72)),
|
||||||
Density.XXXHDPI to FrameSet(Frame(643, 67, 96, 96), Frame(807, 147, 96, 96))
|
Density.XXXHDPI to FrameSet(Frame(643, 67, 96, 96), Frame(807, 147, 96, 96))
|
||||||
)
|
)
|
||||||
),
|
}
|
||||||
|
},
|
||||||
LARGE(
|
LARGE(
|
||||||
"large",
|
"large"
|
||||||
|
) {
|
||||||
|
override val frameMap: Map<Density, FrameSet> by lazy {
|
||||||
mapOf(
|
mapOf(
|
||||||
Density.LDPI to FrameSet(Frame(145, 1, 27, 27), Frame(124, 46, 27, 27)),
|
Density.LDPI to FrameSet(Frame(145, 1, 27, 27), Frame(124, 46, 27, 27)),
|
||||||
Density.MDPI to FrameSet(Frame(189, 1, 36, 36), Frame(163, 57, 36, 36)),
|
Density.MDPI to FrameSet(Frame(189, 1, 36, 36), Frame(163, 57, 36, 36)),
|
||||||
|
@ -92,9 +100,12 @@ class BadgeSpriteTransformation(
|
||||||
Density.XXHDPI to FrameSet(Frame(557, 1, 108, 108), Frame(483, 161, 108, 108)),
|
Density.XXHDPI to FrameSet(Frame(557, 1, 108, 108), Frame(483, 161, 108, 108)),
|
||||||
Density.XXXHDPI to FrameSet(Frame(741, 1, 144, 144), Frame(643, 213, 144, 144))
|
Density.XXXHDPI to FrameSet(Frame(741, 1, 144, 144), Frame(643, 213, 144, 144))
|
||||||
)
|
)
|
||||||
),
|
}
|
||||||
|
},
|
||||||
BADGE_64(
|
BADGE_64(
|
||||||
"badge_64",
|
"badge_64"
|
||||||
|
) {
|
||||||
|
override val frameMap: Map<Density, FrameSet> by lazy {
|
||||||
mapOf(
|
mapOf(
|
||||||
Density.LDPI to FrameSet(Frame(124, 73, 48, 48), Frame(124, 73, 48, 48)),
|
Density.LDPI to FrameSet(Frame(124, 73, 48, 48), Frame(124, 73, 48, 48)),
|
||||||
Density.MDPI to FrameSet(Frame(163, 97, 64, 64), Frame(163, 97, 64, 64)),
|
Density.MDPI to FrameSet(Frame(163, 97, 64, 64), Frame(163, 97, 64, 64)),
|
||||||
|
@ -103,9 +114,12 @@ class BadgeSpriteTransformation(
|
||||||
Density.XXHDPI to FrameSet(Frame(483, 289, 192, 192), Frame(483, 289, 192, 192)),
|
Density.XXHDPI to FrameSet(Frame(483, 289, 192, 192), Frame(483, 289, 192, 192)),
|
||||||
Density.XXXHDPI to FrameSet(Frame(643, 385, 256, 256), Frame(643, 385, 256, 256))
|
Density.XXXHDPI to FrameSet(Frame(643, 385, 256, 256), Frame(643, 385, 256, 256))
|
||||||
)
|
)
|
||||||
),
|
}
|
||||||
|
},
|
||||||
BADGE_112(
|
BADGE_112(
|
||||||
"badge_112",
|
"badge_112"
|
||||||
|
) {
|
||||||
|
override val frameMap: Map<Density, FrameSet> by lazy {
|
||||||
mapOf(
|
mapOf(
|
||||||
Density.LDPI to FrameSet(Frame(181, 1, 84, 84), Frame(181, 1, 84, 84)),
|
Density.LDPI to FrameSet(Frame(181, 1, 84, 84), Frame(181, 1, 84, 84)),
|
||||||
Density.MDPI to FrameSet(Frame(233, 1, 112, 112), Frame(233, 1, 112, 112)),
|
Density.MDPI to FrameSet(Frame(233, 1, 112, 112), Frame(233, 1, 112, 112)),
|
||||||
|
@ -114,9 +128,12 @@ class BadgeSpriteTransformation(
|
||||||
Density.XXHDPI to FrameSet(Frame(681, 1, 336, 336), Frame(681, 1, 336, 336)),
|
Density.XXHDPI to FrameSet(Frame(681, 1, 336, 336), Frame(681, 1, 336, 336)),
|
||||||
Density.XXXHDPI to FrameSet(Frame(905, 1, 448, 448), Frame(905, 1, 448, 448))
|
Density.XXXHDPI to FrameSet(Frame(905, 1, 448, 448), Frame(905, 1, 448, 448))
|
||||||
)
|
)
|
||||||
),
|
}
|
||||||
|
},
|
||||||
XLARGE(
|
XLARGE(
|
||||||
"xlarge",
|
"xlarge"
|
||||||
|
) {
|
||||||
|
override val frameMap: Map<Density, FrameSet> by lazy {
|
||||||
mapOf(
|
mapOf(
|
||||||
Density.LDPI to FrameSet(Frame(1, 1, 120, 120), Frame(1, 1, 120, 120)),
|
Density.LDPI to FrameSet(Frame(1, 1, 120, 120), Frame(1, 1, 120, 120)),
|
||||||
Density.MDPI to FrameSet(Frame(1, 1, 160, 160), Frame(1, 1, 160, 160)),
|
Density.MDPI to FrameSet(Frame(1, 1, 160, 160), Frame(1, 1, 160, 160)),
|
||||||
|
@ -125,7 +142,10 @@ class BadgeSpriteTransformation(
|
||||||
Density.XXHDPI to FrameSet(Frame(1, 1, 480, 480), Frame(1, 1, 480, 480)),
|
Density.XXHDPI to FrameSet(Frame(1, 1, 480, 480), Frame(1, 1, 480, 480)),
|
||||||
Density.XXXHDPI to FrameSet(Frame(1, 1, 640, 640), Frame(1, 1, 640, 640))
|
Density.XXXHDPI to FrameSet(Frame(1, 1, 640, 640), Frame(1, 1, 640, 640))
|
||||||
)
|
)
|
||||||
);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
abstract val frameMap: Map<Density, FrameSet>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInteger(integer: Int): Size {
|
fun fromInteger(integer: Int): Size {
|
||||||
|
|
|
@ -2303,6 +2303,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||||
return bodyBubble;
|
return bodyBubble;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidateChatColorsDrawable(@NonNull ViewGroup coordinateRoot) {
|
||||||
|
// Intentionally left blank.
|
||||||
|
}
|
||||||
|
|
||||||
private class SharedContactEventListener implements SharedContactView.EventListener {
|
private class SharedContactEventListener implements SharedContactView.EventListener {
|
||||||
@Override
|
@Override
|
||||||
public void onAddToContactsClicked(@NonNull Contact contact) {
|
public void onAddToContactsClicked(@NonNull Contact contact) {
|
||||||
|
|
|
@ -185,6 +185,7 @@ import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResults
|
||||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModelV2
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModelV2
|
||||||
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupCallViewModel
|
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupCallViewModel
|
||||||
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupViewModel
|
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupViewModel
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.items.ChatColorsDrawable
|
||||||
import org.thoughtcrime.securesms.conversation.v2.items.InteractiveConversationElement
|
import org.thoughtcrime.securesms.conversation.v2.items.InteractiveConversationElement
|
||||||
import org.thoughtcrime.securesms.conversation.v2.keyboard.AttachmentKeyboardFragment
|
import org.thoughtcrime.securesms.conversation.v2.keyboard.AttachmentKeyboardFragment
|
||||||
import org.thoughtcrime.securesms.database.DraftTable
|
import org.thoughtcrime.securesms.database.DraftTable
|
||||||
|
@ -579,6 +580,8 @@ class ConversationFragment :
|
||||||
registerForResults()
|
registerForResults()
|
||||||
|
|
||||||
inputPanel.setMediaListener(InputPanelMediaListener())
|
inputPanel.setMediaListener(InputPanelMediaListener())
|
||||||
|
|
||||||
|
ChatColorsDrawable.attach(binding.conversationItemRecycler)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.conversation.v2.items
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.ColorFilter
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.PixelFormat
|
||||||
|
import android.graphics.PointF
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.graphics.withClip
|
||||||
|
import androidx.core.graphics.withTranslation
|
||||||
|
import androidx.core.view.children
|
||||||
|
import androidx.core.view.doOnDetach
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||||
|
import org.thoughtcrime.securesms.util.Projection
|
||||||
|
import org.thoughtcrime.securesms.util.Projection.Corners
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawable that renders the given chat colors at a specified coordinate offset.
|
||||||
|
* This is meant to be used in conjunction with [ChatColorsItemDecoration]
|
||||||
|
*/
|
||||||
|
class ChatColorsDrawable : Drawable() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var maskDrawable: Drawable? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds the ChatColorsDrawable static cache to the lifecycle of the given recycler-view
|
||||||
|
*/
|
||||||
|
fun attach(recyclerView: RecyclerView) {
|
||||||
|
recyclerView.addOnLayoutChangeListener { _, left, top, right, bottom, _, _, _, _ ->
|
||||||
|
applyBounds(Rect(left, top, right, bottom))
|
||||||
|
}
|
||||||
|
|
||||||
|
recyclerView.addItemDecoration(ChatColorsItemDecoration)
|
||||||
|
recyclerView.doOnDetach {
|
||||||
|
maskDrawable = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyBounds(bounds: Rect) {
|
||||||
|
maskDrawable?.bounds = bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translation coordinates so that the mask is drawn at the right location
|
||||||
|
* on the screen.
|
||||||
|
*/
|
||||||
|
private val maskOffset = PointF()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clipping path that includes the dimensions and corners for this view.
|
||||||
|
*/
|
||||||
|
private val path = Path()
|
||||||
|
|
||||||
|
private val rect = RectF()
|
||||||
|
|
||||||
|
private var gradientColors: ChatColors? = null
|
||||||
|
private var corners: FloatArray = floatArrayOf()
|
||||||
|
private var fillColor: Int = 0
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
if (gradientColors == null && fillColor == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val mask = maskDrawable
|
||||||
|
if (gradientColors != null && mask != null) {
|
||||||
|
canvas.withTranslation(-maskOffset.x, -maskOffset.y) {
|
||||||
|
canvas.withClip(path) {
|
||||||
|
mask.draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path.reset()
|
||||||
|
rect.set(bounds)
|
||||||
|
path.addRoundRect(rect, corners, Path.Direction.CW)
|
||||||
|
canvas.withClip(path) {
|
||||||
|
canvas.drawColor(fillColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAlpha(alpha: Int) = Unit
|
||||||
|
|
||||||
|
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||||
|
|
||||||
|
override fun getOpacity(): Int {
|
||||||
|
return PixelFormat.TRANSLUCENT
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the given [Projection] as the clipping path for the canvas on subsequent draws.
|
||||||
|
* Also applies the given [Projection]'s (x,y) (Top, Left) coordinates as the mask offset,
|
||||||
|
* which is used as a canvas translation before drawing.
|
||||||
|
*
|
||||||
|
* This is done separately from setting the corners and color, because it needs to happen
|
||||||
|
* on every frame we would normally perform a decorator onDraw, whereas setting the corners
|
||||||
|
* and color only needs to happen on bind.
|
||||||
|
*/
|
||||||
|
fun applyMaskProjection(projection: Projection) {
|
||||||
|
path.reset()
|
||||||
|
projection.applyToPath(path)
|
||||||
|
|
||||||
|
maskOffset.set(
|
||||||
|
projection.x,
|
||||||
|
projection.y
|
||||||
|
)
|
||||||
|
|
||||||
|
invalidateSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isSolidColor(): Boolean {
|
||||||
|
return gradientColors == null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the chat color and shape as specified. If the colors are a gradient,
|
||||||
|
* we will use masking to draw, and we will draw every time we're told to by
|
||||||
|
* the decorator.
|
||||||
|
*
|
||||||
|
* If a solid color is set, we can skip drawing as we move, since we haven't changed.
|
||||||
|
*/
|
||||||
|
fun setChatColors(
|
||||||
|
chatColors: ChatColors,
|
||||||
|
corners: Corners
|
||||||
|
) {
|
||||||
|
this.gradientColors = chatColors
|
||||||
|
this.corners = corners.toRadii()
|
||||||
|
|
||||||
|
if (chatColors.isGradient()) {
|
||||||
|
if (maskDrawable == null) {
|
||||||
|
maskDrawable = chatColors.chatBubbleMask
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fillColor = 0
|
||||||
|
} else {
|
||||||
|
this.fillColor = chatColors.asSingleColor()
|
||||||
|
this.gradientColors = null
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private object ChatColorsItemDecoration : RecyclerView.ItemDecoration() {
|
||||||
|
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
parent.children.map { parent.getChildViewHolder(it) }.filterIsInstance<ChatColorsDrawableInvalidator>().forEach { element ->
|
||||||
|
element.invalidateChatColorsDrawable(parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatColorsDrawableInvalidator {
|
||||||
|
fun invalidateChatColorsDrawable(coordinateRoot: ViewGroup)
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.util.ProjectionList
|
||||||
/**
|
/**
|
||||||
* A conversation element that a user can either swipe or snapshot
|
* A conversation element that a user can either swipe or snapshot
|
||||||
*/
|
*/
|
||||||
interface InteractiveConversationElement {
|
interface InteractiveConversationElement : ChatColorsDrawable.ChatColorsDrawableInvalidator {
|
||||||
val conversationMessage: ConversationMessage
|
val conversationMessage: ConversationMessage
|
||||||
|
|
||||||
val root: ViewGroup
|
val root: ViewGroup
|
||||||
|
|
|
@ -5,8 +5,6 @@
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.conversation.v2.items
|
package org.thoughtcrime.securesms.conversation.v2.items
|
||||||
|
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
|
||||||
import com.google.android.material.shape.ShapeAppearanceModel
|
|
||||||
import org.signal.core.util.dp
|
import org.signal.core.util.dp
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
|
@ -36,11 +34,6 @@ class V2ConversationItemShape(
|
||||||
var corners: Projection.Corners = Projection.Corners(bigRadius)
|
var corners: Projection.Corners = Projection.Corners(bigRadius)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var bodyBubble: MaterialShapeDrawable = MaterialShapeDrawable(
|
|
||||||
ShapeAppearanceModel.Builder().setAllCornerSizes(bigRadius).build()
|
|
||||||
)
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the message spacing and corners based off the given information. This
|
* Sets the message spacing and corners based off the given information. This
|
||||||
* updates the class state.
|
* updates the class state.
|
||||||
|
@ -93,12 +86,6 @@ class V2ConversationItemShape(
|
||||||
}
|
}
|
||||||
|
|
||||||
corners = newCorners
|
corners = newCorners
|
||||||
bodyBubble.shapeAppearanceModel = ShapeAppearanceModel.builder()
|
|
||||||
.setTopLeftCornerSize(corners.topLeft)
|
|
||||||
.setTopRightCornerSize(corners.topRight)
|
|
||||||
.setBottomLeftCornerSize(corners.bottomLeft)
|
|
||||||
.setBottomRightCornerSize(corners.bottomRight)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isSingularMessage(
|
private fun isSingularMessage(
|
||||||
|
|
|
@ -63,7 +63,7 @@ fun V2ConversationItemTextOnlyIncomingBinding.bridge(): V2ConversationItemTextOn
|
||||||
conversationItemFooterBackground = conversationItemFooterBackground,
|
conversationItemFooterBackground = conversationItemFooterBackground,
|
||||||
conversationItemAlert = null,
|
conversationItemAlert = null,
|
||||||
conversationItemFooterSpace = null,
|
conversationItemFooterSpace = null,
|
||||||
isIncoming = false
|
isIncoming = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
package org.thoughtcrime.securesms.conversation.v2.items
|
package org.thoughtcrime.securesms.conversation.v2.items
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
@ -64,21 +63,22 @@ class V2ConversationItemTheme(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
fun getBodyBubbleColor(
|
fun getBodyBubbleColor(
|
||||||
conversationMessage: ConversationMessage
|
conversationMessage: ConversationMessage
|
||||||
): ColorStateList {
|
): Int {
|
||||||
if (conversationMessage.messageRecord.hasNoBubble(context)) {
|
if (conversationMessage.messageRecord.hasNoBubble(context)) {
|
||||||
return ColorStateList.valueOf(Color.TRANSPARENT)
|
return Color.TRANSPARENT
|
||||||
}
|
}
|
||||||
|
|
||||||
return getFooterBubbleColor(conversationMessage)
|
return getFooterBubbleColor(conversationMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
fun getFooterBubbleColor(
|
fun getFooterBubbleColor(
|
||||||
conversationMessage: ConversationMessage
|
conversationMessage: ConversationMessage
|
||||||
): ColorStateList {
|
): Int {
|
||||||
return ColorStateList.valueOf(
|
return if (conversationMessage.messageRecord.isOutgoing) {
|
||||||
if (conversationMessage.messageRecord.isOutgoing) {
|
|
||||||
Color.TRANSPARENT
|
Color.TRANSPARENT
|
||||||
} else {
|
} else {
|
||||||
if (conversationContext.hasWallpaper()) {
|
if (conversationContext.hasWallpaper()) {
|
||||||
|
@ -87,7 +87,6 @@ class V2ConversationItemTheme(
|
||||||
ContextCompat.getColor(context, R.color.signal_colorSurface2)
|
ContextCompat.getColor(context, R.color.signal_colorSurface2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
|
|
|
@ -23,8 +23,6 @@ import android.view.ViewGroup.MarginLayoutParams
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
|
||||||
import com.google.android.material.shape.ShapeAppearanceModel
|
|
||||||
import org.signal.core.util.StringUtil
|
import org.signal.core.util.StringUtil
|
||||||
import org.signal.core.util.dp
|
import org.signal.core.util.dp
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
@ -32,6 +30,7 @@ import org.thoughtcrime.securesms.components.mention.MentionAnnotation
|
||||||
import org.thoughtcrime.securesms.conversation.BodyBubbleLayoutTransition
|
import org.thoughtcrime.securesms.conversation.BodyBubbleLayoutTransition
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||||
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect
|
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect
|
||||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||||
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselectable
|
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselectable
|
||||||
|
@ -55,7 +54,6 @@ import org.thoughtcrime.securesms.util.VibrateUtil
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||||
import org.thoughtcrime.securesms.util.hasExtraText
|
import org.thoughtcrime.securesms.util.hasExtraText
|
||||||
import org.thoughtcrime.securesms.util.hasNoBubble
|
|
||||||
import org.thoughtcrime.securesms.util.isScheduled
|
import org.thoughtcrime.securesms.util.isScheduled
|
||||||
import org.thoughtcrime.securesms.util.visible
|
import org.thoughtcrime.securesms.util.visible
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
@ -82,6 +80,9 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||||
companion object {
|
companion object {
|
||||||
private val STYLE_FACTORY = StyleFactory { arrayOf<CharacterStyle>(BackgroundColorSpan(Color.YELLOW), ForegroundColorSpan(Color.BLACK)) }
|
private val STYLE_FACTORY = StyleFactory { arrayOf<CharacterStyle>(BackgroundColorSpan(Color.YELLOW), ForegroundColorSpan(Color.BLACK)) }
|
||||||
private const val CONDENSED_MODE_MAX_LINES = 3
|
private const val CONDENSED_MODE_MAX_LINES = 3
|
||||||
|
|
||||||
|
private val footerCorners = Projection.Corners(18f.dp)
|
||||||
|
private val transparentChatColors = ChatColors.forColor(ChatColors.Id.NotSet, Color.TRANSPARENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var messageId: Long = Long.MAX_VALUE
|
private var messageId: Long = Long.MAX_VALUE
|
||||||
|
@ -89,13 +90,6 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||||
private val projections = ProjectionList()
|
private val projections = ProjectionList()
|
||||||
private val footerDelegate = V2FooterPositionDelegate(binding)
|
private val footerDelegate = V2FooterPositionDelegate(binding)
|
||||||
|
|
||||||
private val conversationItemFooterBackgroundCorners = Projection.Corners(18f.dp)
|
|
||||||
private val conversationItemFooterBackground = MaterialShapeDrawable(
|
|
||||||
ShapeAppearanceModel.Builder()
|
|
||||||
.setAllCornerSizes(18f.dp)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
override lateinit var conversationMessage: ConversationMessage
|
override lateinit var conversationMessage: ConversationMessage
|
||||||
|
|
||||||
override val root: ViewGroup = binding.root
|
override val root: ViewGroup = binding.root
|
||||||
|
@ -117,6 +111,9 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||||
|
|
||||||
private var reactionMeasureListener: ReactionMeasureListener = ReactionMeasureListener()
|
private var reactionMeasureListener: ReactionMeasureListener = ReactionMeasureListener()
|
||||||
|
|
||||||
|
private val bodyBubbleDrawable = ChatColorsDrawable()
|
||||||
|
private val footerDrawable = ChatColorsDrawable()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
binding.root.addOnMeasureListener(footerDelegate)
|
binding.root.addOnMeasureListener(footerDelegate)
|
||||||
|
|
||||||
|
@ -150,7 +147,15 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||||
binding.conversationItemBody.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_25))
|
binding.conversationItemBody.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_25))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.conversationItemBodyWrapper.background = bodyBubbleDrawable
|
||||||
binding.conversationItemBodyWrapper.layoutTransition = BodyBubbleLayoutTransition()
|
binding.conversationItemBodyWrapper.layoutTransition = BodyBubbleLayoutTransition()
|
||||||
|
|
||||||
|
binding.conversationItemFooterBackground.background = footerDrawable
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidateChatColorsDrawable(coordinateRoot: ViewGroup) {
|
||||||
|
invalidateBodyBubbleDrawable(coordinateRoot)
|
||||||
|
invalidateFooterDrawable(coordinateRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(model: Model) {
|
override fun bind(model: Model) {
|
||||||
|
@ -164,11 +169,6 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||||
adapterPosition = bindingAdapterPosition
|
adapterPosition = bindingAdapterPosition
|
||||||
)
|
)
|
||||||
|
|
||||||
shapeDelegate.bodyBubble.fillColor = themeDelegate.getBodyBubbleColor(conversationMessage)
|
|
||||||
|
|
||||||
binding.conversationItemBodyWrapper.background = shapeDelegate.bodyBubble
|
|
||||||
binding.conversationItemReply.setBackgroundColor(themeDelegate.getReplyIconBackgroundColor())
|
|
||||||
|
|
||||||
presentBody()
|
presentBody()
|
||||||
presentDate(shape)
|
presentDate(shape)
|
||||||
presentDeliveryStatus(shape)
|
presentDeliveryStatus(shape)
|
||||||
|
@ -178,6 +178,19 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||||
presentSender()
|
presentSender()
|
||||||
presentReactions()
|
presentReactions()
|
||||||
|
|
||||||
|
bodyBubbleDrawable.setChatColors(
|
||||||
|
if (binding.conversationItemBody.isJumbomoji) {
|
||||||
|
transparentChatColors
|
||||||
|
} else if (binding.isIncoming) {
|
||||||
|
ChatColors.forColor(ChatColors.Id.NotSet, themeDelegate.getBodyBubbleColor(conversationMessage))
|
||||||
|
} else {
|
||||||
|
conversationMessage.threadRecipient.chatColors
|
||||||
|
},
|
||||||
|
shapeDelegate.corners
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.conversationItemReply.setBackgroundColor(themeDelegate.getReplyIconBackgroundColor())
|
||||||
|
|
||||||
itemView.updateLayoutParams<MarginLayoutParams> {
|
itemView.updateLayoutParams<MarginLayoutParams> {
|
||||||
topMargin = shape.topPadding.toInt()
|
topMargin = shape.topPadding.toInt()
|
||||||
bottomMargin = shape.bottomPadding.toInt()
|
bottomMargin = shape.bottomPadding.toInt()
|
||||||
|
@ -208,29 +221,13 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||||
return projections
|
return projections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: This is not necessary for CFV2 Text-Only items because the background is rendered by
|
||||||
|
* [ChatColorsDrawable]
|
||||||
|
*/
|
||||||
override fun getColorizerProjections(coordinateRoot: ViewGroup): ProjectionList {
|
override fun getColorizerProjections(coordinateRoot: ViewGroup): ProjectionList {
|
||||||
projections.clear()
|
projections.clear()
|
||||||
|
|
||||||
if (conversationMessage.messageRecord.isOutgoing) {
|
|
||||||
if (!conversationMessage.messageRecord.hasNoBubble(context)) {
|
|
||||||
projections.add(
|
|
||||||
Projection.relativeToParent(
|
|
||||||
coordinateRoot,
|
|
||||||
binding.conversationItemBodyWrapper,
|
|
||||||
shapeDelegate.corners
|
|
||||||
).translateX(binding.conversationItemBodyWrapper.translationX).translateY(root.translationY)
|
|
||||||
)
|
|
||||||
} else if (conversationContext.hasWallpaper()) {
|
|
||||||
projections.add(
|
|
||||||
Projection.relativeToParent(
|
|
||||||
coordinateRoot,
|
|
||||||
binding.conversationItemFooterBackground,
|
|
||||||
conversationItemFooterBackgroundCorners
|
|
||||||
).translateX(binding.conversationItemFooterBackground.translationX).translateY(root.translationY)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return projections
|
return projections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,6 +274,36 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||||
|
|
||||||
override fun shouldProjectContent(): Boolean = false
|
override fun shouldProjectContent(): Boolean = false
|
||||||
|
|
||||||
|
private fun invalidateFooterDrawable(coordinateRoot: ViewGroup) {
|
||||||
|
if (footerDrawable.isSolidColor()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val projection = Projection.relativeToParent(
|
||||||
|
coordinateRoot,
|
||||||
|
binding.conversationItemFooterBackground,
|
||||||
|
shapeDelegate.corners
|
||||||
|
)
|
||||||
|
|
||||||
|
footerDrawable.applyMaskProjection(projection)
|
||||||
|
projection.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invalidateBodyBubbleDrawable(coordinateRoot: ViewGroup) {
|
||||||
|
if (bodyBubbleDrawable.isSolidColor()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val projection = Projection.relativeToParent(
|
||||||
|
coordinateRoot,
|
||||||
|
binding.conversationItemBodyWrapper,
|
||||||
|
shapeDelegate.corners
|
||||||
|
)
|
||||||
|
|
||||||
|
bodyBubbleDrawable.applyMaskProjection(projection)
|
||||||
|
projection.release()
|
||||||
|
}
|
||||||
|
|
||||||
private fun MessageRecord.buildMessageId(): Long {
|
private fun MessageRecord.buildMessageId(): Long {
|
||||||
return if (isMms) -id else id
|
return if (isMms) -id else id
|
||||||
}
|
}
|
||||||
|
@ -472,8 +499,14 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.conversationItemFooterBackground.visible = true
|
binding.conversationItemFooterBackground.visible = true
|
||||||
binding.conversationItemFooterBackground.background = conversationItemFooterBackground
|
footerDrawable.setChatColors(
|
||||||
conversationItemFooterBackground.fillColor = themeDelegate.getFooterBubbleColor(conversationMessage)
|
if (binding.isIncoming) {
|
||||||
|
ChatColors.forColor(ChatColors.Id.NotSet, themeDelegate.getFooterBubbleColor(conversationMessage))
|
||||||
|
} else {
|
||||||
|
conversationMessage.threadRecipient.chatColors
|
||||||
|
},
|
||||||
|
footerCorners
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun presentDate(shape: V2ConversationItemShape.MessageShape) {
|
private fun presentDate(shape: V2ConversationItemShape.MessageShape) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue