From 8c2b9ec6f4df2b063ac861b6d94c2479e919cbec Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Wed, 8 Feb 2023 12:44:59 +0100 Subject: [PATCH] [merge,WIP] interface'd FooteredTextView Change-Id: I62f09fff7d094ebb3bf6690b17c951e4e48e80c7 --- .../core/ui/views/AbstractFooteredTextView.kt | 149 ++++++++++++++++++ .../ui/views/FooteredEditorStyledTextView.kt | 32 ++++ .../app/core/ui/views/FooteredTextView.kt | 133 +--------------- .../detail/timeline/item/MessageAudioItem.kt | 4 +- .../detail/timeline/item/MessageFileItem.kt | 4 +- .../timeline/item/MessageImageVideoItem.kt | 6 +- .../detail/timeline/item/MessageTextItem.kt | 13 +- ...timeline_event_text_message_plain_stub.xml | 3 +- ..._timeline_event_text_message_rich_stub.xml | 3 +- 9 files changed, 202 insertions(+), 145 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/ui/views/AbstractFooteredTextView.kt create mode 100644 vector/src/main/java/im/vector/app/core/ui/views/FooteredEditorStyledTextView.kt diff --git a/vector/src/main/java/im/vector/app/core/ui/views/AbstractFooteredTextView.kt b/vector/src/main/java/im/vector/app/core/ui/views/AbstractFooteredTextView.kt new file mode 100644 index 0000000000..85be24fcd8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/views/AbstractFooteredTextView.kt @@ -0,0 +1,149 @@ +package im.vector.app.core.ui.views + +import android.graphics.Canvas +import android.graphics.Rect +import android.text.Layout +import android.text.Spannable +import android.text.Spanned +import android.view.View +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.text.getSpans +import androidx.core.text.toSpanned +import androidx.core.view.ViewCompat.LAYOUT_DIRECTION_RTL +import im.vector.app.R +import im.vector.app.features.html.HtmlCodeSpan +import io.noties.markwon.core.spans.EmphasisSpan +import kotlin.math.ceil +import kotlin.math.max + +/** + * TextView that reserves space at the bottom for overlaying it with a footer, e.g. in a FrameLayout or RelativeLayout + */ +interface AbstractFooteredTextView { + + fun getAppCompatTextView(): AppCompatTextView + fun setMeasuredDimensionExposed(measuredWidth: Int, measuredHeight: Int) + + val footerState: FooterState + + class FooterState { + var footerHeight: Int = 0 + var footerWidth: Int = 0 + //var widthLimit: Float = 0f + + // Some Rect to use during draw, since we should not alloc it during draw + val testBounds = Rect() + + // Workaround to RTL languages with non-RTL content messages aligning left instead of start + var requiredHorizontalCanvasMove = 0f + } + + fun updateDimensionsWithFooter(widthMeasureSpec: Int, heightMeasureSpec: Int) = with(getAppCompatTextView()) { + // Default case + footerState.requiredHorizontalCanvasMove = 0f + + // Get max available width + //val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val widthSize = View.MeasureSpec.getSize(widthMeasureSpec) + //val widthLimit = if (widthMode == MeasureSpec.AT_MOST) { widthSize.toFloat() } else { Float.MAX_VALUE } + val widthLimit = widthSize.toFloat() + /* + // Sometimes, widthLimit is not the actual limit, so remember it... ? + if (this.widthLimit > widthLimit) { + widthLimit = this.widthLimit + } else { + this.widthLimit = widthLimit + } + */ + + val lastLine = layout.lineCount - 1 + + // Let's check if the last line's text has the same RTL behaviour as the layout direction. + val viewIsRtl = layoutDirection == LAYOUT_DIRECTION_RTL + val looksLikeRtl = layout.getParagraphDirection(lastLine) == Layout.DIR_RIGHT_TO_LEFT + /* + val lastVisibleCharacter = layout.getLineVisibleEnd(lastLine) - 1 + val looksLikeRtl = layout.isRtlCharAt(lastVisibleCharacter) + */ + + // Get required width for all lines + var maxLineWidth = 0f + for (i in 0 until layout.lineCount) { + // For some reasons, the getLineWidth is not working too well with RTL lines when rendering replies. + // -> https://github.com/SchildiChat/SchildiChat-android/issues/74 + // However, the bounds method is a little generous sometimes (reserving too much space), + // so we don't want to use it over getLineWidth() unless required. + maxLineWidth = if (layout.getParagraphDirection(i) == Layout.DIR_RIGHT_TO_LEFT) { + layout.getLineBounds(i, footerState.testBounds) + max((footerState.testBounds.right - footerState.testBounds.left).toFloat(), maxLineWidth) + } else { + max(layout.getLineWidth(i), maxLineWidth) + } + } + + // Fix wrap_content in multi-line texts by using maxLineWidth instead of measuredWidth here + // (compare WrapWidthTextView.kt) + var newWidth = ceil(maxLineWidth).toInt() + var newHeight = measuredHeight + + val widthLastLine = layout.getLineWidth(lastLine) + + // Required width if putting footer in the same line as the last line + val widthWithHorizontalFooter = ( + if (looksLikeRtl == viewIsRtl) + widthLastLine + else + (maxLineWidth + resources.getDimensionPixelSize(R.dimen.sc_footer_rtl_mismatch_extra_padding)) + ) + footerState.footerWidth + + // If the last line is a multi-line code block, we have never space in the last line (as the black background always uses full width) + val forceNewlineFooter: Boolean + // For italic text, we need some extra space due to a wrap_content bug: https://stackoverflow.com/q/4353836 + val addItalicPadding: Boolean + + if (text is Spannable || text is Spanned) { + val span = text.toSpanned() + // If not found, -1+1 = 0 + val lastLineStart = span.lastIndexOf("\n") + 1 + val lastLineCodeSpans = span.getSpans(lastLineStart) + forceNewlineFooter = lastLineCodeSpans.any { it.isBlock } + addItalicPadding = span.getSpans().isNotEmpty() + } else { + forceNewlineFooter = false + addItalicPadding = false + } + + // Is there space for a horizontal footer? + if (widthWithHorizontalFooter <= widthLimit && !forceNewlineFooter) { + // Reserve extra horizontal footer space if necessary + if (widthWithHorizontalFooter > newWidth) { + newWidth = ceil(widthWithHorizontalFooter).toInt() + + if (viewIsRtl) { + footerState.requiredHorizontalCanvasMove = widthWithHorizontalFooter - measuredWidth + } + } + } else { + // Reserve vertical footer space + newHeight += footerState.footerHeight + // Ensure enough width for footer bellow + newWidth = max(newWidth, footerState.footerWidth + + resources.getDimensionPixelSize(R.dimen.sc_footer_padding_compensation) + + 2 * resources.getDimensionPixelSize(R.dimen.sc_footer_overlay_padding)) + } + + if (addItalicPadding) { + newWidth += resources.getDimensionPixelSize(R.dimen.italic_text_view_extra_padding) + } + + //setMeasuredDimension(newWidth, newHeight) + Pair(newWidth, newHeight) + } + + fun updateFooterOnPreDraw(canvas: Canvas?) { + // Workaround to RTL languages with non-RTL content messages aligning left instead of start + if (footerState.requiredHorizontalCanvasMove > 0f) { + canvas?.translate(footerState.requiredHorizontalCanvasMove, 0f) + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/views/FooteredEditorStyledTextView.kt b/vector/src/main/java/im/vector/app/core/ui/views/FooteredEditorStyledTextView.kt new file mode 100644 index 0000000000..39692cdd03 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/views/FooteredEditorStyledTextView.kt @@ -0,0 +1,32 @@ +package im.vector.app.core.ui.views + +import android.content.Context +import android.graphics.Canvas +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import io.element.android.wysiwyg.EditorStyledTextView + +class FooteredEditorStyledTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +): EditorStyledTextView(context, attrs, defStyleAttr), AbstractFooteredTextView { + + override val footerState: AbstractFooteredTextView.FooterState = AbstractFooteredTextView.FooterState() + override fun getAppCompatTextView(): AppCompatTextView = this + override fun setMeasuredDimensionExposed(measuredWidth: Int, measuredHeight: Int) = setMeasuredDimension(measuredWidth, measuredHeight) + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + // First, let super measure the content for our normal TextView use + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + val updatedMeasures = updateDimensionsWithFooter(widthMeasureSpec, heightMeasureSpec) + setMeasuredDimension(updatedMeasures.first, updatedMeasures.second) + } + + override fun onDraw(canvas: Canvas?) { + updateFooterOnPreDraw(canvas) + + super.onDraw(canvas) + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/views/FooteredTextView.kt b/vector/src/main/java/im/vector/app/core/ui/views/FooteredTextView.kt index 712dfc4db3..50e6314a55 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/FooteredTextView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/FooteredTextView.kt @@ -2,148 +2,29 @@ package im.vector.app.core.ui.views import android.content.Context import android.graphics.Canvas -import android.graphics.Rect -import android.text.Layout -import android.text.Spannable -import android.text.Spanned import android.util.AttributeSet import androidx.appcompat.widget.AppCompatTextView -import androidx.core.text.getSpans -import androidx.core.text.toSpanned -import im.vector.app.R -import im.vector.app.features.html.HtmlCodeSpan -import io.noties.markwon.core.spans.EmphasisSpan -import kotlin.math.ceil -import kotlin.math.max -/** - * TextView that reserves space at the bottom for overlaying it with a footer, e.g. in a FrameLayout or RelativeLayout - */ class FooteredTextView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -): AppCompatTextView(context, attrs, defStyleAttr) { +): AppCompatTextView(context, attrs, defStyleAttr), AbstractFooteredTextView { - var footerHeight: Int = 0 - var footerWidth: Int = 0 - //var widthLimit: Float = 0f - - // Some Rect to use during draw, since we should not alloc it during draw - private val testBounds = Rect() - - // Workaround to RTL languages with non-RTL content messages aligning left instead of start - private var requiredHorizontalCanvasMove = 0f + override val footerState: AbstractFooteredTextView.FooterState = AbstractFooteredTextView.FooterState() + override fun getAppCompatTextView(): AppCompatTextView = this + override fun setMeasuredDimensionExposed(measuredWidth: Int, measuredHeight: Int) = setMeasuredDimension(measuredWidth, measuredHeight) override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { // First, let super measure the content for our normal TextView use super.onMeasure(widthMeasureSpec, heightMeasureSpec) - // Default case - requiredHorizontalCanvasMove = 0f - - // Get max available width - //val widthMode = MeasureSpec.getMode(widthMeasureSpec) - val widthSize = MeasureSpec.getSize(widthMeasureSpec) - //val widthLimit = if (widthMode == MeasureSpec.AT_MOST) { widthSize.toFloat() } else { Float.MAX_VALUE } - val widthLimit = widthSize.toFloat() - /* - // Sometimes, widthLimit is not the actual limit, so remember it... ? - if (this.widthLimit > widthLimit) { - widthLimit = this.widthLimit - } else { - this.widthLimit = widthLimit - } - */ - - val lastLine = layout.lineCount - 1 - - // Let's check if the last line's text has the same RTL behaviour as the layout direction. - val viewIsRtl = layoutDirection == LAYOUT_DIRECTION_RTL - val looksLikeRtl = layout.getParagraphDirection(lastLine) == Layout.DIR_RIGHT_TO_LEFT - /* - val lastVisibleCharacter = layout.getLineVisibleEnd(lastLine) - 1 - val looksLikeRtl = layout.isRtlCharAt(lastVisibleCharacter) - */ - - // Get required width for all lines - var maxLineWidth = 0f - for (i in 0 until layout.lineCount) { - // For some reasons, the getLineWidth is not working too well with RTL lines when rendering replies. - // -> https://github.com/SchildiChat/SchildiChat-android/issues/74 - // However, the bounds method is a little generous sometimes (reserving too much space), - // so we don't want to use it over getLineWidth() unless required. - maxLineWidth = if (layout.getParagraphDirection(i) == Layout.DIR_RIGHT_TO_LEFT) { - layout.getLineBounds(i, testBounds) - max((testBounds.right - testBounds.left).toFloat(), maxLineWidth) - } else { - max(layout.getLineWidth(i), maxLineWidth) - } - } - - // Fix wrap_content in multi-line texts by using maxLineWidth instead of measuredWidth here - // (compare WrapWidthTextView.kt) - var newWidth = ceil(maxLineWidth).toInt() - var newHeight = measuredHeight - - val widthLastLine = layout.getLineWidth(lastLine) - - // Required width if putting footer in the same line as the last line - val widthWithHorizontalFooter = ( - if (looksLikeRtl == viewIsRtl) - widthLastLine - else - (maxLineWidth + resources.getDimensionPixelSize(R.dimen.sc_footer_rtl_mismatch_extra_padding)) - ) + footerWidth - - // If the last line is a multi-line code block, we have never space in the last line (as the black background always uses full width) - val forceNewlineFooter: Boolean - // For italic text, we need some extra space due to a wrap_content bug: https://stackoverflow.com/q/4353836 - val addItalicPadding: Boolean - - if (text is Spannable || text is Spanned) { - val span = text.toSpanned() - // If not found, -1+1 = 0 - val lastLineStart = span.lastIndexOf("\n") + 1 - val lastLineCodeSpans = span.getSpans(lastLineStart) - forceNewlineFooter = lastLineCodeSpans.any { it.isBlock } - addItalicPadding = span.getSpans().isNotEmpty() - } else { - forceNewlineFooter = false - addItalicPadding = false - } - - // Is there space for a horizontal footer? - if (widthWithHorizontalFooter <= widthLimit && !forceNewlineFooter) { - // Reserve extra horizontal footer space if necessary - if (widthWithHorizontalFooter > newWidth) { - newWidth = ceil(widthWithHorizontalFooter).toInt() - - if (viewIsRtl) { - requiredHorizontalCanvasMove = widthWithHorizontalFooter - measuredWidth - } - } - } else { - // Reserve vertical footer space - newHeight += footerHeight - // Ensure enough width for footer bellow - newWidth = max(newWidth, footerWidth + - resources.getDimensionPixelSize(R.dimen.sc_footer_padding_compensation) + - 2 * resources.getDimensionPixelSize(R.dimen.sc_footer_overlay_padding)) - } - - if (addItalicPadding) { - newWidth += resources.getDimensionPixelSize(R.dimen.italic_text_view_extra_padding) - } - - setMeasuredDimension(newWidth, newHeight) + val updatedMeasures = updateDimensionsWithFooter(widthMeasureSpec, heightMeasureSpec) + setMeasuredDimension(updatedMeasures.first, updatedMeasures.second) } override fun onDraw(canvas: Canvas?) { - // Workaround to RTL languages with non-RTL content messages aligning left instead of start - if (requiredHorizontalCanvasMove > 0f) { - canvas?.translate(requiredHorizontalCanvasMove, 0f) - } + updateFooterOnPreDraw(canvas) super.onDraw(canvas) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt index 1a611ea677..c6e09319bf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt @@ -31,7 +31,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide -import im.vector.app.core.ui.views.FooteredTextView +import im.vector.app.core.ui.views.AbstractFooteredTextView import im.vector.app.core.utils.TextUtils import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder @@ -224,7 +224,7 @@ abstract class MessageAudioItem : AbsMessageItem() { val audioPlaybackTime by bind(R.id.audioPlaybackTime) val progressLayout by bind(R.id.messageFileUploadProgressLayout) val fileSize by bind(R.id.fileSize) - val captionView by bind(R.id.messageCaptionView) + val captionView by bind(R.id.messageCaptionView) val audioPlaybackDuration by bind(R.id.audioPlaybackDuration) val audioSeekBar by bind(R.id.audioSeekBar) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt index a00d69b72d..828854f129 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -30,7 +30,7 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide -import im.vector.app.core.ui.views.FooteredTextView +import im.vector.app.core.ui.views.AbstractFooteredTextView import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout @@ -154,7 +154,7 @@ abstract class MessageFileItem : AbsMessageItem() { val fileImageWrapper by bind(R.id.messageFileImageView) val fileDownloadProgress by bind(R.id.messageFileProgressbar) val filenameView by bind(R.id.messageFilenameView) - val captionView by bind(R.id.messageCaptionView) + val captionView by bind(R.id.messageCaptionView) } companion object { 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 bb9c67f3db..479f5d60d5 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 @@ -34,17 +34,15 @@ import im.vector.app.core.epoxy.onClick import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.files.LocalFilesHelper import im.vector.app.core.glide.GlideApp -import im.vector.app.core.ui.views.FooteredTextView +import im.vector.app.core.ui.views.AbstractFooteredTextView 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.style.granularRoundedCorners import im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView import im.vector.app.features.media.ImageContentRenderer -import im.vector.app.features.themes.defaultScBubbleAppearance import kotlin.math.round import org.matrix.android.sdk.api.session.room.model.message.MessageType -import org.matrix.android.sdk.api.util.MimeTypes @EpoxyModelClass abstract class MessageImageVideoItem : AbsMessageItem() { @@ -255,7 +253,7 @@ abstract class MessageImageVideoItem : AbsMessageItem(R.id.messageMediaUploadProgressLayout) val imageView by bind(R.id.messageThumbnailView) - val captionView by bind(R.id.messageCaptionView) + val captionView by bind(R.id.messageCaptionView) val playContentView by bind(R.id.messageMediaPlayView) val mediaContentView by bind(R.id.messageContentMedia) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index b047af0805..12f42780bc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -17,23 +17,18 @@ package im.vector.app.features.home.room.detail.timeline.item import android.text.Spanned -import android.text.method.MovementMethod import android.view.ViewStub import androidx.appcompat.widget.AppCompatTextView import androidx.core.text.PrecomputedTextCompat -import androidx.core.view.isVisible import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onLongClickIgnoringLinks -import im.vector.app.core.ui.views.FooteredTextView +import im.vector.app.core.ui.views.AbstractFooteredTextView import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import im.vector.app.features.home.room.detail.timeline.reply.InReplyToView import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout -import im.vector.app.features.home.room.detail.timeline.reply.PreviewReplyUiState -import im.vector.app.features.home.room.detail.timeline.reply.ReplyPreviewRetriever import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess import im.vector.app.features.home.room.detail.timeline.url.AbstractPreviewUrlView import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever @@ -105,7 +100,7 @@ abstract class MessageTextItem : AbsMessageItem() { holder.previewUrlView.delegate = previewUrlCallback holder.previewUrlView.renderMessageLayout(attributes.informationData.messageLayout) - val messageView: FooteredTextView = holder.messageView(useRichTextEditorStyle) //if (useRichTextEditorStyle) holder.richMessageView else holder.plainMessageView + val messageView: AbstractFooteredTextView = holder.messageView(useRichTextEditorStyle) //if (useRichTextEditorStyle) holder.richMessageView else holder.plainMessageView if (useBigFont) { messageView.textSize = 44F } else { @@ -155,10 +150,10 @@ abstract class MessageTextItem : AbsMessageItem() { lateinit var previewUrlView: AbstractPreviewUrlView // set to either previewUrlViewElement or previewUrlViewSc by layout private val richMessageStub by bind(R.id.richMessageTextViewStub) private val plainMessageStub by bind(R.id.plainMessageTextViewStub) - val richMessageView: FooteredTextView by lazy { + val richMessageView: AbstractFooteredTextView by lazy { richMessageStub.inflate().findViewById(R.id.messageTextView) } - val plainMessageView: FooteredTextView by lazy { + val plainMessageView: AbstractFooteredTextView by lazy { plainMessageStub.inflate().findViewById(R.id.messageTextView) } fun messageView(useRichTextEditorStyle: Boolean) = if (useRichTextEditorStyle) richMessageView else plainMessageView diff --git a/vector/src/main/res/layout/item_timeline_event_text_message_plain_stub.xml b/vector/src/main/res/layout/item_timeline_event_text_message_plain_stub.xml index 68d97bba43..5f506963a1 100644 --- a/vector/src/main/res/layout/item_timeline_event_text_message_plain_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_text_message_plain_stub.xml @@ -6,7 +6,8 @@ but it also makes it align better for RTL texts in RTL locales - at least until the user pill is touched... --> - -