Message footer: handle small width/height pictures

Change-Id: Ib8554c5057f09277cb9ef2a29a57d3a85cf35417
This commit is contained in:
SpiritCroc 2021-05-11 10:51:47 +02:00
parent 1481a3edd3
commit e9e522603d
4 changed files with 153 additions and 38 deletions

View File

@ -343,10 +343,20 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
}
}
/**
* 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<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
(attributes.informationData.isDirect && attributes.informationData.senderId == attributes.informationData.dmChatPartnerId)
}
protected fun getFooterMeasures(holder: H): Array<Int> {
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<Int> {
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<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
}
}
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

View File

@ -54,9 +54,28 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
@EpoxyAttribute
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
var lastAllowedFooterOverlay: Boolean = true
var lastShowFooterBellow: Boolean = true
var forceAllowFooterOverlay: Boolean? = null
var showFooterBellow: Boolean = true
override fun bind(holder: Holder) {
forceAllowFooterOverlay = null
super.bind(holder)
imageContentRenderer.render(mediaData, mode, holder.imageView)
val onImageSizeListener = object: ImageContentRenderer.OnImageSizeListener {
override fun onImageSizeUpdated(width: Int, height: Int) {
// Image size change -> 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<MessageImageVideoItem.Hold
}
}
private fun shouldAllowFooterOverlay(footerMeasures: Array<Int>, 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<Int>, 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
}

View File

@ -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)
}
}

View File

@ -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">
<TextView