diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index 314a7a1a5e..b0fe63ee3a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -343,10 +343,20 @@ abstract class AbsMessageItem : AbsBaseMessageItem } } + /** + * Whether to show the footer in front of the viewStub + */ open fun allowFooterOverlay(holder: H): Boolean { return false } + /** + * Whether to show the footer aligned below the viewStub - requires enough width! + */ + open fun allowFooterBelow(holder: H): Boolean { + return true + } + open fun needsFooterReservation(holder: H): Boolean { return false } @@ -363,6 +373,45 @@ abstract class AbsMessageItem : AbsBaseMessageItem (attributes.informationData.isDirect && attributes.informationData.senderId == attributes.informationData.dmChatPartnerId) } + protected fun getFooterMeasures(holder: H): Array { + val anonymousReadReceipt = BubbleThemeUtils.getVisibleAnonymousReadReceipts(holder.bubbleFootView.context, + attributes.informationData.readReceiptAnonymous, attributes.informationData.sentByMe) + return getFooterMeasures(holder, anonymousReadReceipt) + } + + private fun getFooterMeasures(holder: H, anonymousReadReceipt: AnonymousReadReceipt): Array { + val timeWidth: Int + val timeHeight: Int + if (BubbleThemeUtils.getBubbleTimeLocation(holder.bubbleTimeView.context) == BubbleThemeUtils.BUBBLE_TIME_BOTTOM) { + // Guess text width for name and time + timeWidth = ceil(BubbleThemeUtils.guessTextWidth(holder.bubbleFooterTimeView, attributes.informationData.time.toString())).toInt() + holder.bubbleFooterTimeView.paddingLeft + holder.bubbleFooterTimeView.paddingRight + timeHeight = ceil(holder.bubbleFooterTimeView.textSize).toInt() + holder.bubbleFooterTimeView.paddingTop + holder.bubbleFooterTimeView.paddingBottom + } else { + timeWidth = 0 + timeHeight = 0 + } + val readReceiptWidth: Int + val readReceiptHeight: Int + if (anonymousReadReceipt == AnonymousReadReceipt.NONE) { + readReceiptWidth = 0 + readReceiptHeight = 0 + } else { + readReceiptWidth = holder.bubbleFooterReadReceipt.maxWidth + holder.bubbleFooterReadReceipt.paddingLeft + holder.bubbleFooterReadReceipt.paddingRight + readReceiptHeight = holder.bubbleFooterReadReceipt.maxHeight + holder.bubbleFooterReadReceipt.paddingTop + holder.bubbleFooterReadReceipt.paddingBottom + } + + var footerWidth = timeWidth + readReceiptWidth + var footerHeight = max(timeHeight, readReceiptHeight) + // Reserve extra padding, if we do have actual content + if (footerWidth > 0) { + footerWidth += holder.bubbleFootView.paddingLeft + holder.bubbleFootView.paddingRight + } + if (footerHeight > 0) { + footerHeight += holder.bubbleFootView.paddingTop + holder.bubbleFootView.paddingBottom + } + return arrayOf(footerWidth, footerHeight) + } + override fun setBubbleLayout(holder: H, bubbleStyle: String, bubbleStyleSetting: String, reverseBubble: Boolean) { super.setBubbleLayout(holder, bubbleStyle, bubbleStyleSetting, reverseBubble) @@ -442,53 +491,60 @@ abstract class AbsMessageItem : AbsBaseMessageItem } } + val footerLayoutParams = holder.bubbleFootView.layoutParams as RelativeLayout.LayoutParams + var footerMarginStartDp = 4 + var footerMarginEndDp = 1 if (allowFooterOverlay(holder)) { - (holder.bubbleFootView.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.ALIGN_BOTTOM, R.id.viewStubContainer) - (holder.bubbleFootView.layoutParams as RelativeLayout.LayoutParams).removeRule(RelativeLayout.BELOW) + footerLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, R.id.viewStubContainer) + footerLayoutParams.addRule(RelativeLayout.ALIGN_END, R.id.viewStubContainer) + footerLayoutParams.removeRule(RelativeLayout.BELOW) + footerLayoutParams.removeRule(RelativeLayout.END_OF) if (needsFooterReservation(holder)) { // Remove style used when not having reserved space removeFooterOverlayStyle(holder, density) // Calculate required footer space - val timeWidth: Int - val timeHeight: Int - if (BubbleThemeUtils.getBubbleTimeLocation(holder.bubbleTimeView.context) == BubbleThemeUtils.BUBBLE_TIME_BOTTOM) { - // Guess text width for name and time - timeWidth = ceil(BubbleThemeUtils.guessTextWidth(holder.bubbleFooterTimeView, attributes.informationData.time.toString())).toInt() + holder.bubbleFooterTimeView.paddingLeft + holder.bubbleFooterTimeView.paddingRight - timeHeight = ceil(holder.bubbleFooterTimeView.textSize).toInt() + holder.bubbleFooterTimeView.paddingTop + holder.bubbleFooterTimeView.paddingBottom - } else { - timeWidth = 0 - timeHeight = 0 - } - val readReceiptWidth: Int - val readReceiptHeight: Int - if (anonymousReadReceipt == AnonymousReadReceipt.NONE) { - readReceiptWidth = 0 - readReceiptHeight = 0 - } else { - readReceiptWidth = holder.bubbleFooterReadReceipt.maxWidth + holder.bubbleFooterReadReceipt.paddingLeft + holder.bubbleFooterReadReceipt.paddingRight - readReceiptHeight = holder.bubbleFooterReadReceipt.maxHeight + holder.bubbleFooterReadReceipt.paddingTop + holder.bubbleFooterReadReceipt.paddingBottom - } + val footerMeasures = getFooterMeasures(holder, anonymousReadReceipt) + val footerWidth = footerMeasures[0] + val footerHeight = footerMeasures[1] - var footerWidth = timeWidth + readReceiptWidth - var footerHeight = max(timeHeight, readReceiptHeight) - // Reserve extra padding, if we do have actual content - if (footerWidth > 0) { - footerWidth += holder.bubbleFootView.paddingLeft + holder.bubbleFootView.paddingRight - } - if (footerHeight > 0) { - footerHeight += holder.bubbleFootView.paddingTop + holder.bubbleFootView.paddingBottom - } reserveFooterSpace(holder, footerWidth, footerHeight) } else { // We have no reserved space -> style it to ensure readability on arbitrary backgrounds styleFooterOverlay(holder, density) } } else { - (holder.bubbleFootView.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.BELOW, R.id.viewStubContainer) - (holder.bubbleFootView.layoutParams as RelativeLayout.LayoutParams).removeRule(RelativeLayout.ALIGN_BOTTOM) + when { + allowFooterBelow(holder) -> { + footerLayoutParams.addRule(RelativeLayout.BELOW, R.id.viewStubContainer) + footerLayoutParams.addRule(RelativeLayout.ALIGN_END, R.id.viewStubContainer) + footerLayoutParams.removeRule(RelativeLayout.ALIGN_BOTTOM) + footerLayoutParams.removeRule(RelativeLayout.END_OF) + footerLayoutParams.removeRule(RelativeLayout.START_OF) + } + reverseBubble -> /* force footer on the left / at the start */ { + footerLayoutParams.addRule(RelativeLayout.START_OF, R.id.viewStubContainer) + footerLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, R.id.viewStubContainer) + footerLayoutParams.removeRule(RelativeLayout.ALIGN_END) + footerLayoutParams.removeRule(RelativeLayout.END_OF) + footerLayoutParams.removeRule(RelativeLayout.BELOW) + // Reverse margins + footerMarginStartDp = 1 + // 4 as previously the start margin, +4 to compensate the missing inner padding for the textView which we have on the other side + footerMarginEndDp = 8 + } + else -> /* footer on the right / at the end */ { + footerLayoutParams.addRule(RelativeLayout.END_OF, R.id.viewStubContainer) + footerLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, R.id.viewStubContainer) + footerLayoutParams.removeRule(RelativeLayout.ALIGN_END) + footerLayoutParams.removeRule(RelativeLayout.BELOW) + footerLayoutParams.removeRule(RelativeLayout.START_OF) + } + } removeFooterOverlayStyle(holder, density) } + footerLayoutParams.marginStart = round(footerMarginStartDp*density).toInt() + footerLayoutParams.marginEnd = round(footerMarginEndDp*density).toInt() } if (bubbleStyle == BubbleThemeUtils.BUBBLE_STYLE_BOTH_HIDDEN) { // We need to align the non-bubble member name view to pseudo bubbles diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index a871e73690..d8ed46686b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -54,9 +54,28 @@ abstract class MessageImageVideoItem : AbsMessageItem different footer space situation possible + val footerMeasures = getFooterMeasures(holder) + forceAllowFooterOverlay = shouldAllowFooterOverlay(footerMeasures, width, height) + val newShowFooterBellow = shouldShowFooterBellow(footerMeasures, width) + if (lastAllowedFooterOverlay != forceAllowFooterOverlay || newShowFooterBellow != lastShowFooterBellow) { + showFooterBellow = newShowFooterBellow + updateMessageBubble(holder.imageView.context, holder) + } + } + } + imageContentRenderer.render(mediaData, mode, holder.imageView, onImageSizeListener) if (!attributes.informationData.sendState.hasFailed()) { contentUploadStateTrackerBinder.bind( attributes.informationData.eventId, @@ -122,8 +141,42 @@ abstract class MessageImageVideoItem : AbsMessageItem, imageWidth: Int, imageHeight: Int): Boolean { + val footerWidth = footerMeasures[0] + val footerHeight = footerMeasures[1] + return imageWidth > 1.5*footerWidth && imageHeight > 1.5*footerHeight + } + + private fun shouldShowFooterBellow(footerMeasures: Array, imageWidth: Int): Boolean { + // Only show footer bellow if the width is not the limiting factor (or it will get cut). + // Otherwise, we can not be sure in this place that we'll have enough space on the side + val footerWidth = footerMeasures[0] + return imageWidth > 1.5*footerWidth + } + override fun allowFooterOverlay(holder: Holder): Boolean { - return true + val rememberedAllowFooterOverlay = forceAllowFooterOverlay + if (rememberedAllowFooterOverlay != null) { + lastAllowedFooterOverlay = rememberedAllowFooterOverlay + return rememberedAllowFooterOverlay + } + val imageWidth = holder.imageView.width + val imageHeight = holder.imageView.height + if (imageWidth == 0 && imageHeight == 0) { + // Not initialised yet, assume true + lastAllowedFooterOverlay = true + return true + } + // If the footer covers most of the image, or is even larger than the image, move it outside + val footerMeasures = getFooterMeasures(holder) + lastAllowedFooterOverlay = shouldAllowFooterOverlay(footerMeasures, imageWidth, imageHeight) + return lastAllowedFooterOverlay + } + + override fun allowFooterBelow(holder: Holder): Boolean { + val showBellow = showFooterBellow + lastShowFooterBellow = showBellow + return showBellow } diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 7ee8253481..f6785a78ef 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -110,12 +110,13 @@ 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, onImageSizeListener: OnImageSizeListener? = null) { val size = processSize(data, mode) imageView.updateLayoutParams { width = size.width height = size.height } + onImageSizeListener?.onImageSizeUpdated(size.width, size.height) // a11y imageView.contentDescription = data.filename @@ -134,6 +135,7 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc width = newSize.width height = newSize.height } + onImageSizeListener?.onImageSizeUpdated(newSize.width, newSize.height) } } return false @@ -350,4 +352,8 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc } return Size(finalWidth, finalHeight) } + + interface OnImageSizeListener { + fun onImageSizeUpdated(width: Int, height: Int) + } } 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 0626efbbd8..e2277a4a7b 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -208,11 +208,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end|bottom" - android:layout_marginStart="4dp" - android:layout_alignEnd="@id/viewStubContainer" - android:layout_marginEnd="1dp" android:layout_marginBottom="1dp" + tools:layout_marginStart="4dp" + tools:layout_marginEnd="1dp" tools:layout_alignBottom="@id/viewStubContainer" + tools:layout_alignEnd="@id/viewStubContainer" tools:paddingTop="4dp" android:id="@+id/bubbleFootView">