Bubbles: handle images and make small refactoring

This commit is contained in:
ganfra 2022-01-18 19:27:12 +01:00
parent a9e7c45074
commit 5ee4984ec8
12 changed files with 131 additions and 113 deletions

View File

@ -54,6 +54,7 @@
<dimen name="chat_bubble_margin_start">28dp</dimen>
<dimen name="chat_bubble_margin_end">62dp</dimen>
<dimen name="chat_bubble_fixed_size">300dp</dimen>
<dimen name="chat_bubble_corner_radius">12dp</dimen>
<!-- Onboarding -->
<item name="ftue_auth_gutter_start_percent" format="float" type="dimen">0.05</item>

View File

@ -4,12 +4,6 @@
<style name="TimelineContentStubBaseParams">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginStart">8dp</item>
<item name="android:layout_marginLeft">8dp</item>
<item name="android:layout_marginEnd">8dp</item>
<item name="android:layout_marginRight">8dp</item>
<item name="android:layout_marginBottom">4dp</item>
<item name="android:layout_marginTop">4dp</item>
</style>
<style name="TimelineContentMediaPillStyle">

View File

@ -29,7 +29,7 @@ import im.vector.app.core.ui.views.ShieldImageView
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.view.MessageViewConfiguration
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
import im.vector.app.features.reactions.widget.ReactionButton
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.room.send.SendState
@ -99,10 +99,7 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
holder.view.onClick(baseAttributes.itemClickListener)
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
(holder.view as? MessageViewConfiguration)?.apply {
isFirstFromSender = baseAttributes.informationData.isFirstFromThisSender
isLastFromSender = baseAttributes.informationData.isLastFromThisSender
}
(holder.view as? TimelineMessageLayoutRenderer)?.render(baseAttributes.informationData.messageLayout)
}
override fun unbind(holder: H) {

View File

@ -23,14 +23,17 @@ import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.files.LocalFilesHelper
import im.vector.app.core.glide.GlideApp
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.view.MessageViewConfiguration
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
import im.vector.app.features.media.ImageContentRenderer
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
@ -56,7 +59,21 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
override fun bind(holder: Holder) {
super.bind(holder)
imageContentRenderer.render(mediaData, mode, holder.imageView)
val messageLayout = baseAttributes.informationData.messageLayout
val dimensionConverter = DimensionConverter(holder.view.resources)
val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) {
val cornerRadius = holder.view.resources.getDimensionPixelSize(R.dimen.chat_bubble_corner_radius).toFloat()
val topRadius = if (messageLayout.isFirstFromThisSender) cornerRadius else 0f
val bottomRadius = if (messageLayout.isLastFromThisSender) cornerRadius else 0f
if (messageLayout.isIncoming) {
GranularRoundedCorners(topRadius, cornerRadius, cornerRadius, bottomRadius)
} else {
GranularRoundedCorners(cornerRadius, topRadius, bottomRadius, cornerRadius)
}
} else {
RoundedCorners(dimensionConverter.dpToPx(8))
}
imageContentRenderer.render(mediaData, mode, holder.imageView, imageCornerTransformation)
if (!attributes.informationData.sendState.hasFailed()) {
contentUploadStateTrackerBinder.bind(
attributes.informationData.eventId,
@ -72,8 +89,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
holder.mediaContentView.onClick(attributes.itemClickListener)
holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener)
holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
(holder.view as? MessageViewConfiguration)?.showTimeAsOverlay = true
holder.overlayView.isVisible = baseAttributes.informationData.messageLayout is TimelineMessageLayout.Bubble
holder.overlayView.isVisible = messageLayout is TimelineMessageLayout.Bubble
}
override fun unbind(holder: Holder) {

View File

@ -41,10 +41,11 @@ sealed interface TimelineMessageLayout : Parcelable {
val isIncoming: Boolean,
val isFirstFromThisSender: Boolean,
val isLastFromThisSender: Boolean,
val isPseudoBubble: Boolean,
override val layoutRes: Int = if (isIncoming) {
R.layout.item_timeline_event_bubble_incoming_base
} else {
R.layout.item_timeline_event_bubble_outgoing_base
},
}
) : TimelineMessageLayout
}

View File

@ -33,15 +33,22 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
private val vectorPreferences: VectorPreferences) {
companion object {
// Can't be rendered in bubbles, so get back to default layout
private val EVENT_TYPES_WITH_BUBBLE_LAYOUT = setOf(
EventType.MESSAGE,
EventType.POLL_START,
EventType.ENCRYPTED,
EventType.STICKER
)
// Can't be rendered in bubbles, so get back to default layout
private val MSG_TYPES_WITHOUT_BUBBLE_LAYOUT = setOf(
MessageType.MSGTYPE_VERIFICATION_REQUEST
)
// Use the bubble layout but without borders
private val MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT = setOf(
MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_VIDEO,
)
}
fun create(params: TimelineItemFactoryParams): TimelineMessageLayout {
@ -86,6 +93,7 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
isIncoming = !isSentByMe,
isFirstFromThisSender = isFirstFromThisSender,
isLastFromThisSender = isLastFromThisSender,
isPseudoBubble = messageContent?.msgType in MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT
)
}
} else {

View File

@ -18,13 +18,12 @@ package im.vector.app.features.home.room.detail.timeline.view
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import android.view.ViewOutlineProvider
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.core.content.withStyledAttributes
@ -35,70 +34,38 @@ import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
import im.vector.app.R
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.databinding.ViewMessageBubbleBinding
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.themes.ThemeUtils
import timber.log.Timber
class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) :
RelativeLayout(context, attrs, defStyleAttr), MessageViewConfiguration {
RelativeLayout(context, attrs, defStyleAttr), TimelineMessageLayoutRenderer {
override var isIncoming: Boolean = false
set(value) {
field = value
render()
}
private var isIncoming: Boolean = false
override var isFirstFromSender: Boolean = false
set(value) {
field = value
render()
}
override var isLastFromSender: Boolean = false
set(value) {
field = value
render()
}
private val cornerRadius = resources.getDimensionPixelSize(R.dimen.chat_bubble_corner_radius).toFloat()
private val horizontalStubPadding = DimensionConverter(resources).dpToPx(12)
private val verticalStubPadding = DimensionConverter(resources).dpToPx(4)
override var showTimeAsOverlay: Boolean = false
set(value) {
field = value
render()
}
private val cornerRadius = DimensionConverter(resources).dpToPx(12).toFloat()
private lateinit var views: ViewMessageBubbleBinding
init {
inflate(context, R.layout.view_message_bubble, this)
context.withStyledAttributes(attrs, R.styleable.MessageBubble) {
isIncoming = getBoolean(R.styleable.MessageBubble_incoming_style, false)
showTimeAsOverlay = getBoolean(R.styleable.MessageBubble_show_time_overlay, false)
isFirstFromSender = getBoolean(R.styleable.MessageBubble_is_first, false)
isLastFromSender = getBoolean(R.styleable.MessageBubble_is_last, false)
}
}
override fun onFinishInflate() {
super.onFinishInflate()
render()
}
private fun render() {
views = ViewMessageBubbleBinding.bind(this)
val currentLayoutDirection = layoutDirection
val bubbleView: ConstraintLayout = findViewById(R.id.bubbleView)
bubbleView.apply {
background = createBackgroundDrawable()
outlineProvider = ViewOutlineProvider.BACKGROUND
clipToOutline = true
}
if (isIncoming) {
findViewById<View>(R.id.informationBottom).layoutDirection = currentLayoutDirection
findViewById<View>(R.id.bubbleWrapper).layoutDirection = currentLayoutDirection
bubbleView.layoutDirection = currentLayoutDirection
findViewById<View>(R.id.messageEndGuideline).updateLayoutParams<LayoutParams> {
marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end)
}
findViewById<View>(R.id.messageStartGuideline).updateLayoutParams<LayoutParams> {
marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start)
}
views.informationBottom.layoutDirection = currentLayoutDirection
views.bubbleWrapper.layoutDirection = currentLayoutDirection
views.bubbleView.layoutDirection = currentLayoutDirection
} else {
val oppositeLayoutDirection = if (currentLayoutDirection == View.LAYOUT_DIRECTION_LTR) {
View.LAYOUT_DIRECTION_RTL
@ -106,41 +73,66 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri
View.LAYOUT_DIRECTION_LTR
}
findViewById<View>(R.id.informationBottom).layoutDirection = oppositeLayoutDirection
findViewById<View>(R.id.bubbleWrapper).layoutDirection = oppositeLayoutDirection
bubbleView.layoutDirection = currentLayoutDirection
findViewById<View>(R.id.messageEndGuideline).updateLayoutParams<LayoutParams> {
marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start)
}
findViewById<View>(R.id.messageStartGuideline).updateLayoutParams<LayoutParams> {
marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end)
}
}
ConstraintSet().apply {
clone(bubbleView)
clear(R.id.viewStubContainer, ConstraintSet.END)
if (showTimeAsOverlay) {
val timeColor = ContextCompat.getColor(context, R.color.palette_white)
findViewById<TextView>(R.id.messageTimeView).setTextColor(timeColor)
connect(R.id.viewStubContainer, ConstraintSet.END, R.id.parent, ConstraintSet.END, 0)
val margin = resources.getDimensionPixelSize(R.dimen.layout_horizontal_margin)
setMargin(R.id.messageTimeView, ConstraintSet.END, margin)
} else {
val timeColor = ThemeUtils.getColor(context, R.attr.vctr_content_tertiary)
findViewById<TextView>(R.id.messageTimeView).setTextColor(timeColor)
connect(R.id.viewStubContainer, ConstraintSet.END, R.id.messageTimeView, ConstraintSet.START, 0)
}
applyTo(bubbleView)
views.informationBottom.layoutDirection = oppositeLayoutDirection
views.bubbleWrapper.layoutDirection = oppositeLayoutDirection
views.bubbleView.layoutDirection = currentLayoutDirection
}
}
private fun createBackgroundDrawable(): Drawable {
val (topCornerFamily, topRadius) = if (isFirstFromSender) {
override fun render(messageLayout: TimelineMessageLayout) {
if (messageLayout !is TimelineMessageLayout.Bubble) {
Timber.v("Can't render messageLayout $messageLayout")
return
}
views.bubbleView.apply {
background = createBackgroundDrawable(messageLayout)
outlineProvider = ViewOutlineProvider.BACKGROUND
clipToOutline = true
}
ConstraintSet().apply {
clone(views.bubbleView)
clear(R.id.viewStubContainer, ConstraintSet.END)
val showTimeAsOverlay = messageLayout.isPseudoBubble
if (showTimeAsOverlay) {
val timeColor = ContextCompat.getColor(context, R.color.palette_white)
views.messageTimeView.setTextColor(timeColor)
connect(R.id.viewStubContainer, ConstraintSet.END, R.id.parent, ConstraintSet.END, 0)
} else {
val timeColor = ThemeUtils.getColor(context, R.attr.vctr_content_tertiary)
views.messageTimeView.setTextColor(timeColor)
connect(R.id.viewStubContainer, ConstraintSet.END, R.id.messageTimeView, ConstraintSet.START, 0)
}
applyTo(views.bubbleView)
}
if (messageLayout.isPseudoBubble) {
views.viewStubContainer.root.setPadding(0, 0, 0, 0)
} else {
views.viewStubContainer.root.setPadding(horizontalStubPadding, verticalStubPadding, horizontalStubPadding, verticalStubPadding)
}
if (messageLayout.isIncoming) {
views.messageEndGuideline.updateLayoutParams<LayoutParams> {
marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end)
}
views.messageStartGuideline.updateLayoutParams<LayoutParams> {
marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start)
}
} else {
views.messageEndGuideline.updateLayoutParams<LayoutParams> {
marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start)
}
views.messageStartGuideline.updateLayoutParams<LayoutParams> {
marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end)
}
}
}
private fun createBackgroundDrawable(messageLayout: TimelineMessageLayout.Bubble): Drawable {
val (topCornerFamily, topRadius) = if (messageLayout.isFirstFromThisSender) {
Pair(CornerFamily.ROUNDED, cornerRadius)
} else {
Pair(CornerFamily.CUT, 0f)
}
val (bottomCornerFamily, bottomRadius) = if (isLastFromSender) {
val (bottomCornerFamily, bottomRadius) = if (messageLayout.isLastFromThisSender) {
Pair(CornerFamily.ROUNDED, cornerRadius)
} else {
Pair(CornerFamily.CUT, 0f)
@ -166,7 +158,11 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri
}
val shapeAppearanceModel = shapeAppearanceModelBuilder.build()
return MaterialShapeDrawable(shapeAppearanceModel).apply {
fillColor = ColorStateList.valueOf(backgroundColor)
fillColor = if (messageLayout.isPseudoBubble) {
ColorStateList.valueOf(Color.TRANSPARENT)
} else {
ColorStateList.valueOf(backgroundColor)
}
}
}
}

View File

@ -16,11 +16,8 @@
package im.vector.app.features.home.room.detail.timeline.view
interface MessageViewConfiguration {
var isIncoming: Boolean
var isFirstFromSender: Boolean
var isLastFromSender: Boolean
var showTimeAsOverlay: Boolean
var showNoBubble: Boolean
fun render()
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
interface TimelineMessageLayoutRenderer {
fun render(messageLayout: TimelineMessageLayout)
}

View File

@ -16,6 +16,7 @@
package im.vector.app.features.media
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Parcelable
@ -23,6 +24,7 @@ import android.view.View
import android.widget.ImageView
import androidx.core.view.updateLayoutParams
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.Transformation
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
@ -109,7 +111,7 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
.into(imageView)
}
fun render(data: Data, mode: Mode, imageView: ImageView) {
fun render(data: Data, mode: Mode, imageView: ImageView, cornerTransformation: Transformation<Bitmap> = RoundedCorners(dimensionConverter.dpToPx(8))) {
val size = processSize(data, mode)
imageView.updateLayoutParams {
width = size.width
@ -120,7 +122,7 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
createGlideRequest(data, mode, imageView, size)
.dontAnimate()
.transform(RoundedCorners(dimensionConverter.dpToPx(8)))
.transform(cornerTransformation)
// .thumbnail(0.3f)
.into(imageView)
}

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="@dimen/chat_bubble_corner_radius"/>
<gradient
android:type="linear"
android:angle="270"

View File

@ -3,18 +3,24 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:addStatesFromChildren="true">
android:addStatesFromChildren="true"
android:paddingStart="8dp"
android:paddingLeft="8dp"
android:paddingTop="4dp"
android:paddingEnd="8dp"
android:paddingRight="8dp"
android:paddingBottom="4dp">
<ViewStub
android:id="@+id/messageContentTextStub"
style="@style/TimelineContentStubBaseParams"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_text_message_stub"
tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentCodeBlockStub"
style="@style/TimelineContentStubBaseParams"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_code_block_stub"
tools:visibility="visible" />
@ -22,33 +28,34 @@
<ViewStub
android:id="@+id/messageContentMediaStub"
style="@style/TimelineContentStubBaseParams"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/messageContentMedia"
android:layout="@layout/item_timeline_event_media_message_stub" />
<ViewStub
android:id="@+id/messageContentFileStub"
style="@style/TimelineContentStubBaseParams"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:visibility="gone"
android:layout="@layout/item_timeline_event_file_stub" />
android:layout="@layout/item_timeline_event_file_stub"
tools:visibility="gone" />
<ViewStub
android:id="@+id/messageContentRedactedStub"
style="@style/TimelineContentStubBaseParams"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_redacted_stub" />
<ViewStub
android:id="@+id/messageContentVoiceStub"
style="@style/TimelineContentStubBaseParams"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_voice_stub"
tools:visibility="gone" />
<ViewStub
android:id="@+id/messageContentPollStub"
style="@style/TimelineContentStubBaseParams"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_poll" />

View File

@ -4,8 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="android.widget.RelativeLayout"
tools:viewBindingIgnore="true">
tools:parentTag="android.widget.RelativeLayout">
<im.vector.app.core.platform.CheckableView
android:id="@+id/messageSelectedBackground"
@ -91,7 +90,6 @@
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:addStatesFromChildren="true"
android:paddingStart="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">