Bubbles: still R&D. Not sure how to handle every event types.

This commit is contained in:
ganfra 2022-01-06 19:07:28 +01:00
parent bde1df0322
commit ad63d3de1c
11 changed files with 121 additions and 25 deletions

View file

@ -3,6 +3,7 @@
<declare-styleable name="MessageBubble">
<attr name="incoming_style" format="boolean" />
<attr name="show_background" format="boolean" />
<attr name="is_first" format="boolean" />
<attr name="is_last" format="boolean" />
</declare-styleable>

View file

@ -355,12 +355,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
(0 until modelCache.size).forEach { position ->
val event = currentSnapshot[position]
val nextEvent = currentSnapshot.nextOrNull(position)
val prevEvent = currentSnapshot.prevOrNull(position)
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
}
// Should be build if not cached or if model should be refreshed
if (modelCache[position] == null || modelCache[position]?.isCacheable(partialState) == false) {
val prevEvent = currentSnapshot.prevOrNull(position)
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
}
val timelineEventsGroup = timelineEventsGroups.getOrNull(event)
val params = TimelineItemFactoryParams(
event = event,

View file

@ -389,6 +389,13 @@ class MessageItemFactory @Inject constructor(
allowNonMxcUrls = informationData.sendState.isSending()
)
return MessageImageVideoItem_()
.layout(
if (informationData.sentByMe) {
R.layout.item_timeline_event_bubble_outgoing_base
} else {
R.layout.item_timeline_event_bubble_incoming_base
}
)
.attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline)
.imageContentRenderer(imageContentRenderer)

View file

@ -57,8 +57,11 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
fun create(params: TimelineItemFactoryParams): MessageInformationData {
val event = params.event
val nextDisplayableEvent = params.nextDisplayableEvent
val prevEvent = params.prevEvent
val eventId = event.eventId
val isSentByMe = event.root.senderId == session.myUserId
val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId
val isLastFromThisSender = prevEvent?.root?.senderId != event.root.senderId
val roomSummary = params.partialState.roomSummary
val date = event.root.localDateTime()
@ -128,6 +131,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
ReferencesInfoData(verificationState)
},
sentByMe = isSentByMe,
isFirstFromThisSender = isFirstFromThisSender,
isLastFromThisSender = isLastFromThisSender,
e2eDecoration = e2eDecoration,
sendStateDecoration = sendStateDecoration
)

View file

@ -29,6 +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.reactions.widget.ReactionButton
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.room.send.SendState
@ -98,6 +99,10 @@ 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
}
}
override fun unbind(holder: H) {

View file

@ -29,6 +29,8 @@ 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.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.view.MessageBubbleView
import im.vector.app.features.home.room.detail.timeline.view.MessageViewConfiguration
import im.vector.app.features.media.ImageContentRenderer
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
@ -70,6 +72,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)?.displayBorder = false
}
override fun unbind(holder: Holder) {

View file

@ -42,7 +42,9 @@ data class MessageInformationData(
val referencesInfoData: ReferencesInfoData? = null,
val sentByMe: Boolean,
val e2eDecoration: E2EDecoration = E2EDecoration.NONE,
val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE
val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE,
val isFirstFromThisSender: Boolean = false,
val isLastFromThisSender: Boolean = false
) : Parcelable {
val matrixItem: MatrixItem

View file

@ -20,9 +20,10 @@ import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.widget.RelativeLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.core.content.withStyledAttributes
import androidx.core.view.updateLayoutParams
@ -33,34 +34,61 @@ import im.vector.app.R
import im.vector.app.core.utils.DimensionConverter
class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : RelativeLayout(context, attrs, defStyleAttr) {
defStyleAttr: Int = 0)
: RelativeLayout(context, attrs, defStyleAttr), MessageViewConfiguration {
var incoming: Boolean = false
var isFirst: Boolean = false
var isLast: Boolean = false
var cornerRadius = DimensionConverter(resources).dpToPx(12).toFloat()
override var isIncoming: Boolean = false
set(value) {
field = value
render()
}
override var isFirstFromSender: Boolean = false
set(value) {
field = value
render()
}
override var isLastFromSender: Boolean = false
set(value) {
field = value
render()
}
override var displayBorder: Boolean = true
set(value) {
field = value
render()
}
private val cornerRadius = DimensionConverter(resources).dpToPx(12).toFloat()
init {
inflate(context, R.layout.view_message_bubble, this)
context.withStyledAttributes(attrs, R.styleable.MessageBubble) {
incoming = getBoolean(R.styleable.MessageBubble_incoming_style, false)
isFirst = getBoolean(R.styleable.MessageBubble_is_first, false)
isLast = getBoolean(R.styleable.MessageBubble_is_last, false)
isIncoming = getBoolean(R.styleable.MessageBubble_incoming_style, false)
displayBorder = getBoolean(R.styleable.MessageBubble_show_background, true)
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() {
val currentLayoutDirection = layoutDirection
findViewById<ViewGroup>(R.id.bubbleView).apply {
val bubbleView: ConstraintLayout = findViewById(R.id.bubbleView)
bubbleView.apply {
background = createBackgroundDrawable()
outlineProvider = ViewOutlineProvider.BACKGROUND
clipToOutline = true
}
if (incoming) {
if (isIncoming) {
findViewById<View>(R.id.informationBottom).layoutDirection = currentLayoutDirection
findViewById<View>(R.id.bubbleWrapper).layoutDirection = currentLayoutDirection
findViewById<View>(R.id.bubbleView).layoutDirection = currentLayoutDirection
bubbleView.layoutDirection = currentLayoutDirection
findViewById<View>(R.id.messageEndGuideline).updateLayoutParams<LayoutParams> {
marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end)
}
@ -73,21 +101,37 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri
findViewById<View>(R.id.informationBottom).layoutDirection = oppositeLayoutDirection
findViewById<View>(R.id.bubbleWrapper).layoutDirection = oppositeLayoutDirection
findViewById<View>(R.id.bubbleView).layoutDirection = currentLayoutDirection
bubbleView.layoutDirection = currentLayoutDirection
findViewById<View>(R.id.messageEndGuideline).updateLayoutParams<LayoutParams> {
marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start)
}
}
ConstraintSet().apply {
clone(bubbleView)
clear(R.id.viewStubContainer, ConstraintSet.END)
if (displayBorder) {
connect(R.id.viewStubContainer, ConstraintSet.END, R.id.messageTimeView, ConstraintSet.START, 0)
} else {
connect(R.id.viewStubContainer, ConstraintSet.END, R.id.parent, ConstraintSet.END, 0)
}
applyTo(bubbleView)
}
}
private fun createBackgroundDrawable(): Drawable {
val topCornerFamily = if (isFirst) CornerFamily.ROUNDED else CornerFamily.CUT
val bottomCornerFamily = if (isLast) CornerFamily.ROUNDED else CornerFamily.CUT
val topRadius = if (isFirst) cornerRadius else 0f
val bottomRadius = if (isLast) cornerRadius else 0f
val (topCornerFamily, topRadius) = if (isFirstFromSender) {
Pair(CornerFamily.ROUNDED, cornerRadius)
} else {
Pair(CornerFamily.CUT, 0f)
}
val (bottomCornerFamily, bottomRadius) = if (isLastFromSender) {
Pair(CornerFamily.ROUNDED, cornerRadius)
} else {
Pair(CornerFamily.CUT, 0f)
}
val shapeAppearanceModelBuilder = ShapeAppearanceModel().toBuilder()
val backgroundColor: Int
if (incoming) {
if (isIncoming) {
backgroundColor = R.color.bubble_background_incoming
shapeAppearanceModelBuilder
.setTopRightCorner(CornerFamily.ROUNDED, cornerRadius)
@ -104,7 +148,11 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri
}
val shapeAppearanceModel = shapeAppearanceModelBuilder.build()
val shapeDrawable = MaterialShapeDrawable(shapeAppearanceModel)
shapeDrawable.fillColor = ContextCompat.getColorStateList(context, backgroundColor)
if (displayBorder) {
shapeDrawable.fillColor = ContextCompat.getColorStateList(context, backgroundColor)
} else {
shapeDrawable.fillColor = ContextCompat.getColorStateList(context, android.R.color.transparent)
}
return shapeDrawable
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.timeline.view
interface MessageViewConfiguration {
var isIncoming: Boolean
var isFirstFromSender: Boolean
var isLastFromSender: Boolean
var displayBorder: Boolean
}

View file

@ -4,6 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/palette_element_green"
tools:viewBindingIgnore="true">
<ImageView

View file

@ -103,7 +103,7 @@
android:layout_height="wrap_content"
android:addStatesFromChildren="true"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/messageTimeView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="300dp" />