From 96ca414d447e7aa4dea71c3fe0f20401bbea821b Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Mon, 25 May 2020 17:55:43 +0200 Subject: [PATCH] [WIP] Advanced both-side bubble layout TODO: - MessageFileItem minWidth might be calculatable - No-bubble alignment got broken --- .../timeline/factory/MessageItemFactory.kt | 2 - .../detail/timeline/item/AbsMessageItem.kt | 164 ++++++++++++++- .../detail/timeline/item/BaseEventItem.kt | 11 +- .../timeline/item/MessageBlockCodeItem.kt | 4 + .../detail/timeline/item/MessageFileItem.kt | 6 + .../timeline/item/MessageImageVideoItem.kt | 4 + .../detail/timeline/item/MessageTextItem.kt | 51 +---- .../res/layout/item_timeline_event_base.xml | 157 ++++++++++----- .../layout/item_timeline_event_base_bak.xml | 189 ++++++++++++++++++ ...item_timeline_event_media_message_stub.xml | 4 - vector/src/main/res/values/styles_riot.xml | 2 + 11 files changed, 491 insertions(+), 103 deletions(-) create mode 100644 vector/src/main/res/layout/item_timeline_event_base_bak.xml diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 96cfcaf7cd..ffb71a38c5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -397,8 +397,6 @@ class MessageItemFactory @Inject constructor( .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) - .outgoingMessage(informationData.sentByMe) - .incomingMessage(!informationData.sentByMe) .movementMethod(createLinkMovementMethod(callback)) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 2b01e915df..626047ddc4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -16,17 +16,30 @@ package im.vector.riotx.features.home.room.detail.timeline.item +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Paint import android.graphics.Typeface +import android.view.Gravity import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.RelativeLayout import android.widget.TextView import androidx.annotation.IdRes +import androidx.core.view.children import com.airbnb.epoxy.EpoxyAttribute import im.vector.riotx.R import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.themes.BubbleThemeUtils +import im.vector.riotx.features.themes.ThemeUtils +import kotlin.math.max +import kotlin.math.round /** * Base timeline item that adds an optional information bar with the sender avatar, name and time @@ -49,21 +62,37 @@ abstract class AbsMessageItem : AbsBaseMessageItem override fun bind(holder: H) { super.bind(holder) - if (attributes.informationData.showInformation) { + val contentInBubble = infoInBubbles(holder.memberNameView.context) + if (attributes.informationData.showInformation and (!contentInBubble || !attributes.informationData.sentByMe)) { holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply { height = attributes.avatarSize width = attributes.avatarSize } holder.avatarImageView.visibility = View.VISIBLE holder.avatarImageView.setOnClickListener(_avatarClickListener) - holder.memberNameView.visibility = View.VISIBLE + //holder.memberNameView.visibility = View.VISIBLE holder.memberNameView.setOnClickListener(_memberNameClickListener) - holder.timeView.visibility = View.VISIBLE + holder.bubbleMemberNameView.setOnClickListener(_memberNameClickListener) + //holder.timeView.visibility = View.VISIBLE holder.timeView.text = attributes.informationData.time + holder.bubbleTimeView.text = attributes.informationData.time holder.memberNameView.text = attributes.informationData.memberName + holder.bubbleMemberNameView.text = attributes.informationData.memberName attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView) holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener) holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener) + holder.bubbleMemberNameView.setOnLongClickListener(attributes.itemLongClickListener) + if (contentInBubble) { + holder.memberNameView.visibility = View.GONE + holder.timeView.visibility = View.GONE + holder.bubbleMemberNameView.visibility = View.VISIBLE + holder.bubbleTimeView.visibility = View.VISIBLE + } else { + holder.memberNameView.visibility = View.VISIBLE + holder.timeView.visibility = View.VISIBLE + holder.bubbleMemberNameView.visibility = View.GONE + holder.bubbleTimeView.visibility = View.GONE + } } else { holder.avatarImageView.setOnClickListener(null) holder.memberNameView.setOnClickListener(null) @@ -72,13 +101,41 @@ abstract class AbsMessageItem : AbsBaseMessageItem holder.timeView.visibility = View.GONE holder.avatarImageView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null) + if (attributes.informationData.showInformation /* && contentInBubble && attributes.informationData.sentByMe */) { + holder.bubbleTimeView.visibility = View.VISIBLE + holder.bubbleTimeView.text = attributes.informationData.time + holder.bubbleMemberNameView.setOnClickListener(_memberNameClickListener) + holder.bubbleMemberNameView.text = attributes.informationData.memberName + holder.bubbleMemberNameView.setOnLongClickListener(attributes.itemLongClickListener) + } else { + holder.bubbleTimeView.visibility = View.GONE + holder.bubbleMemberNameView.setOnClickListener(null) + holder.bubbleMemberNameView.visibility = View.GONE + holder.bubbleMemberNameView.setOnLongClickListener(null) + } } + if (contentInBubble && attributes.informationData.showInformation) { + // Guess text width for name and time + val text = holder.bubbleMemberNameView.text.toString() + " " + holder.bubbleTimeView.text.toString() + val paint = Paint() + paint.textSize = max(holder.bubbleMemberNameView.textSize, holder.bubbleTimeView.textSize) + holder.viewStubContainer.minimumWidth = round(paint.measureText(text)).toInt() + } else { + holder.viewStubContainer.minimumWidth = 0 + } + updateMessageBubble(holder) } abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) { val avatarImageView by bind(R.id.messageAvatarImageView) val memberNameView by bind(R.id.messageMemberNameView) val timeView by bind(R.id.messageTimeView) + val eventBaseView by bind(R.id.eventBaseView) + val bubbleView by bind(R.id.bubbleView) + val bubbleMemberNameView by bind(R.id.bubbleMessageMemberNameView) + val bubbleTimeView by bind(R.id.bubbleMessageTimeView) + val informationBottom by bind(R.id.informationBottom) + val viewStubContainer by bind(R.id.viewStubContainer) } /** @@ -97,4 +154,105 @@ abstract class AbsMessageItem : AbsBaseMessageItem override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, val emojiTypeFace: Typeface? = null ) : AbsBaseMessageItem.Attributes + + override fun ignoreMessageGuideline(context: Context): Boolean { + return infoInBubbles(context) && attributes.informationData.sentByMe + } + + open fun messageBubbleAllowed(): Boolean { + return false + } + + fun infoInBubbles(context: Context): Boolean { + return messageBubbleAllowed() && BubbleThemeUtils.getBubbleStyle(context) == BubbleThemeUtils.BUBBLE_STYLE_BOTH + } + + fun updateMessageBubble(holder: H) { + val bubbleStyle = if (messageBubbleAllowed()) BubbleThemeUtils.getBubbleStyle(holder.eventBaseView.context) else BubbleThemeUtils.BUBBLE_STYLE_NONE + val reverseBubble = attributes.informationData.sentByMe && bubbleStyle == BubbleThemeUtils.BUBBLE_STYLE_BOTH + + //val bubbleView = holder.eventBaseView + val bubbleView = holder.bubbleView + + when (bubbleStyle) { + BubbleThemeUtils.BUBBLE_STYLE_NONE -> { + bubbleView.background = null + (bubbleView.layoutParams as ViewGroup.MarginLayoutParams).marginEnd = 0 + /* + (bubbleView.layoutParams as RelativeLayout.LayoutParams).marginStart = 0 + (bubbleView.layoutParams as RelativeLayout.LayoutParams).topMargin = 0 + (bubbleView.layoutParams as RelativeLayout.LayoutParams).bottomMargin = 0 + */ + bubbleView.setPadding(0, 0, 0, 0) + } + BubbleThemeUtils.BUBBLE_STYLE_START, BubbleThemeUtils.BUBBLE_STYLE_BOTH -> { + bubbleView.setBackgroundResource(if (reverseBubble) R.drawable.msg_bubble_outgoing else R.drawable.msg_bubble_incoming) + var tintColor = ColorStateList( + arrayOf(intArrayOf(0)), + intArrayOf(ThemeUtils.getColor(bubbleView.context, + if (attributes.informationData.sentByMe) R.attr.sc_message_bg_outgoing else R.attr.sc_message_bg_incoming) + ) + ) + bubbleView.backgroundTintList = tintColor + val density = bubbleView.resources.displayMetrics.density + // TODO 96 = 2 * avatar size? + if (reverseBubble) { + (bubbleView.layoutParams as ViewGroup.MarginLayoutParams).marginStart = round(96 * density).toInt() + (bubbleView.layoutParams as ViewGroup.MarginLayoutParams).marginEnd = 0 + } else { + (bubbleView.layoutParams as ViewGroup.MarginLayoutParams).marginStart = 0 + (bubbleView.layoutParams as ViewGroup.MarginLayoutParams).marginEnd = round(96 * density).toInt() + } + /* + (bubbleView.layoutParams as RelativeLayout.LayoutParams).marginStart = round(20*density).toInt() + (bubbleView.layoutParams as RelativeLayout.LayoutParams).topMargin = round(8*density).toInt() + (bubbleView.layoutParams as RelativeLayout.LayoutParams).bottomMargin = round(8*density).toInt() + */ + // TODO padding? + if (reverseBubble) { + bubbleView.setPaddingRelative( + round(8 * density).toInt(), + round(8 * density).toInt(), + round(20 * density).toInt(), + round(8 * density).toInt() + ) + } else { + bubbleView.setPaddingRelative( + round(20 * density).toInt(), + round(8 * density).toInt(), + round(8 * density).toInt(), + round(8 * density).toInt() + ) + } + } + } + + val defaultRtl = holder.eventBaseView.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL; + val shouldRtl = reverseBubble != defaultRtl + /* + holder.eventBaseView.layoutDirection = if (shouldRtl) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR + setRtl(shouldRtl) + */ + (holder.bubbleView.layoutParams as FrameLayout.LayoutParams).gravity = if (reverseBubble) Gravity.END else Gravity.START + //holder.informationBottom.layoutDirection = if (shouldRtl) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR + setFlatRtl(holder.informationBottom, if (shouldRtl) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR, + holder.eventBaseView.resources.configuration.layoutDirection, 2) + } + + /* + open fun setRtl(rtl: Boolean) { + // TODO subclass overrides? + } + */ + + fun setFlatRtl(layout: ViewGroup, direction: Int, childDirection: Int, depth: Int = 1) { + layout.layoutDirection = direction + for (child in layout.children) { + if (depth > 1 && child is ViewGroup) { + setFlatRtl(child, direction, childDirection, depth-1) + } else { + child.layoutDirection = childDirection + } + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index f674cfa0f4..aa28dd7b02 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -15,6 +15,7 @@ */ package im.vector.riotx.features.home.room.detail.timeline.item +import android.content.Context import android.view.View import android.view.ViewStub import android.widget.RelativeLayout @@ -47,7 +48,11 @@ abstract class BaseEventItem : VectorEpoxyModel override fun bind(holder: H) { super.bind(holder) holder.leftGuideline.updateLayoutParams { - this.marginStart = leftGuideline + if (ignoreMessageGuideline(holder.leftGuideline.context)) { + this.marginStart = 0 + } else { + this.marginStart = leftGuideline + } } holder.checkableBackground.isChecked = highlighted } @@ -72,4 +77,8 @@ abstract class BaseEventItem : VectorEpoxyModel view.findViewById(stubId).inflate() } } + + open fun ignoreMessageGuideline(context: Context): Boolean { + return false + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt index 82a6a4db6f..67b597e459 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt @@ -51,4 +51,8 @@ abstract class MessageBlockCodeItem : AbsMessageItem() { holder.fileImageView.setImageResource(iconRes) holder.filenameView.setOnClickListener(clickListener) holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG) + // TODO calculate minimum width + holder.viewStubContainer.minimumWidth = 2000 } override fun unbind(holder: Holder) { @@ -64,6 +66,10 @@ abstract class MessageFileItem : AbsMessageItem() { override fun getViewType() = STUB_ID + override fun messageBubbleAllowed(): Boolean { + return true + } + class Holder : AbsMessageItem.Holder(STUB_ID) { val progressLayout by bind(R.id.messageFileUploadProgressLayout) val fileLayout by bind(R.id.messageFileLayout) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 2fd46ddf12..7488406aa5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -70,6 +70,10 @@ abstract class MessageImageVideoItem : AbsMessageItem(R.id.messageMediaUploadProgressLayout) val imageView by bind(R.id.messageThumbnailView) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt index bf75f7d8e7..e205a6b18c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -16,12 +16,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item -import android.content.res.ColorStateList import android.text.method.MovementMethod -import android.view.Gravity -import android.view.View -import android.widget.FrameLayout -import android.widget.RelativeLayout import androidx.appcompat.widget.AppCompatTextView import androidx.core.text.PrecomputedTextCompat import androidx.core.widget.TextViewCompat @@ -29,12 +24,6 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R import im.vector.riotx.features.home.room.detail.timeline.tools.findPillsAndProcess -import im.vector.riotx.features.themes.BubbleThemeUtils -import im.vector.riotx.features.themes.BubbleThemeUtils.BUBBLE_STYLE_BOTH -import im.vector.riotx.features.themes.BubbleThemeUtils.BUBBLE_STYLE_NONE -import im.vector.riotx.features.themes.BubbleThemeUtils.BUBBLE_STYLE_START -import im.vector.riotx.features.themes.ThemeUtils -import kotlin.math.round @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageTextItem : AbsMessageItem() { @@ -47,10 +36,6 @@ abstract class MessageTextItem : AbsMessageItem() { var useBigFont: Boolean = false @EpoxyAttribute var movementMethod: MovementMethod? = null - @EpoxyAttribute - var incomingMessage: Boolean = false - @EpoxyAttribute - var outgoingMessage: Boolean = false override fun bind(holder: Holder) { super.bind(holder) @@ -71,38 +56,6 @@ abstract class MessageTextItem : AbsMessageItem() { TextViewCompat.getTextMetricsParams(holder.messageView), null) holder.messageView.setTextFuture(textFuture) - - val bubbleStyle = if (incomingMessage || outgoingMessage) BubbleThemeUtils.getBubbleStyle(holder.messageView.context) else BUBBLE_STYLE_NONE - val reverseBubble = outgoingMessage && bubbleStyle == BUBBLE_STYLE_BOTH - when (bubbleStyle) { - BUBBLE_STYLE_NONE -> { - holder.messageView.background = null - holder.messageView.setPadding(0, 0, 0, 0) - } - BUBBLE_STYLE_START, BUBBLE_STYLE_BOTH -> { - holder.messageView.setBackgroundResource(if (reverseBubble) R.drawable.msg_bubble_outgoing else R.drawable.msg_bubble_incoming) - var tintColor = ColorStateList( - arrayOf(intArrayOf(0)), - intArrayOf(ThemeUtils.getColor(holder.messageView.context, - if (outgoingMessage) R.attr.sc_message_bg_outgoing else R.attr.sc_message_bg_incoming) - ) - ) - holder.messageView.backgroundTintList = tintColor - val density = holder.messageView.resources.displayMetrics.density - holder.messageView.setPaddingRelative( - round(20*density).toInt(), - round(8*density).toInt(), - round(8*density).toInt(), - round(8*density).toInt() - ) - } - } - if (holder.messageView.layoutParams is FrameLayout.LayoutParams) { - //(holder.messageView.layoutParams as FrameLayout.LayoutParams).gravity = - // if (outgoingMessage && bubbleStyle == BUBBLE_STYLE_BOTH) Gravity.END else Gravity.START - val defaultReverse = holder.messageView.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL; - (holder.messageView.parent.parent as RelativeLayout).layoutDirection = if (reverseBubble != defaultReverse) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR - } } override fun getViewType() = STUB_ID @@ -114,4 +67,8 @@ abstract class MessageTextItem : AbsMessageItem() { companion object { private const val STUB_ID = R.id.messageContentTextStub } + + override fun messageBubbleAllowed(): Boolean { + return true + } } diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 2ddf642d16..11cce87e86 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -2,6 +2,7 @@ + + android:layout_alignStart="@+id/messageMemberNameView"> - - - + android:layout_marginStart="0dp" + android:layout_marginEnd="8dp" + android:layout_marginVertical="4dp" + tools:background="@drawable/msg_bubble_incoming"> - + + - + - + - + - + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_timeline_event_base_bak.xml b/vector/src/main/res/layout/item_timeline_event_base_bak.xml new file mode 100644 index 0000000000..e45874e840 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_base_bak.xml @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml b/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml index 8fe373790d..5429a18e29 100644 --- a/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml @@ -9,8 +9,6 @@ android:id="@+id/messageThumbnailView" android:layout_width="375dp" android:layout_height="0dp" - android:layout_marginEnd="32dp" - android:layout_marginRight="32dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toStartOf="parent" @@ -48,8 +46,6 @@ android:layout_width="0dp" android:layout_height="46dp" android:layout_marginTop="8dp" - android:layout_marginEnd="32dp" - android:layout_marginRight="32dp" android:layout_marginBottom="8dp" android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index c4b42fe4fe..983e70fb25 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -323,12 +323,14 @@