koresuniku koresuniku -3 years ago 135
Android Question

Flow textview around image

I've spent hours looking for answer and have really no idea how to solve it. So let's get down to business:

There is an image and a

TextView
and I need to flow the
TextView
around the
ImageView
like this:

enter image description here

First possible solution woult be to use https://github.com/deano2390/FlowTextView but it's not extending
TextView
so this library is not suitable for me for number of reasons.

Second solution would be to use
LeadingMarginSpan.LeadingMarginSpan2
span but it affects on each paragraph for each n lines inside the text (like in this answer -> How to layout text to flow around an image), so I get smth like this:

enter image description here

But I wanted to set margin only for first n lines! Then I decided to implement
LeadingMarginSpan.Standart
and create a counter and increment it in
getLeadingMargin(first: Boolean): Int
function invocation. When the counter reach the desirable value, the function returns 0 as a margin width. And there is a fail again! Instead of filling the
TextView
lines, the text just moved left and didn't spread to the end of the view!

UPD: Yes, I've used
onGlobalLayoutListener
in here

enter image description here

Well, googling for another solution I found this answer https://stackoverflow.com/a/27064368/7218592
Ok, I've done everything as described and implemented the code:

//set left margin of desirable width
val params: RelativeLayout.LayoutParams = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
params.leftMargin = holder.imageContainerHeight!!
params.addRule(RelativeLayout.BELOW, holder.mNumberAndTimeInfo!!.id)
holder.mCommentTextView!!.layoutParams = params
if (holder.commentTextViewOnGlobalLayoutListener != null)
holder.mCommentTextView!!.viewTreeObserver.removeOnGlobalLayoutListener(
holder.commentTextViewOnGlobalLayoutListener)

//add onGlobalLayoutListener
holder.mCommentTextView!!.viewTreeObserver.addOnGlobalLayoutListener(
if (holder.commentTextViewOnGlobalLayoutListener != null)
holder.commentTextViewOnGlobalLayoutListener
else CommentTextViewOnGlobalLayoutListener(holder,
SpannableString(HtmlCompat.fromHtml(
mView.getActivity(), commentDocument.html(), 0,
null, SpanTagHandlerCompat(mView.getActivity())))))`


My
OnGlobalLayoutListener
looks like this: `

class CommentTextViewOnGlobalLayoutListener(
val holder: CommentAndFilesListViewViewHolder, val commentSpannable: Spannable) :
ViewTreeObserver.OnGlobalLayoutListener {
val LOG_TAG: String = CommentTextViewOnGlobalLayoutListener::class.java.simpleName

override fun onGlobalLayout() {
holder.mCommentTextView!!.viewTreeObserver.removeGlobalOnLayoutListener(this)

//when textview layout is drawn, get the line end to spanify only the needed text
val charCount = holder.mCommentTextView!!.layout.getLineEnd(Math.min(
holder.mCommentTextView!!.layout.lineCount - 1,
CommentLeadingMarginSpan.computeLinesToBeSpanned(holder)))
if (charCount <= commentSpannable.length) {
commentSpannable.setSpan(CommentLeadingMarginSpan(holder),
0, charCount, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}

//set the left margin back to zero
(holder.mCommentTextView!!.layoutParams as RelativeLayout.LayoutParams).leftMargin = 0
holder.mCommentTextView!!.text = commentSpannable
}
}


`

Well, it works. But how terrible it works! As I'm using view holder pattern I have to hold a variable to the listener and remove if it is not been called and successfully removed because
onGlobalLayout
function wasn't called in time! And it is called too late, so you need to wait about 300 ms and then watch all the "reconstruction" of the
TextView
and it looks disgustingly!

So, my question is:
How to make margins for first n lines in
TextView
, before it's been drawn on UI?

Answer Source

Finally, I've managed to find the best solution. It is based on creating a mock-up of the TextView of the desired width, using StaticLayout

  1. Firstly, let's calculate the width of our TextView (just substract all the side paddings and image container width from the display width) in pixels.
  2. Secondly, we'll need the height of the image container (in pixels).
  3. Thirdly, as practice shows, StaticLayout ignores line breaks ("\n" or "\r"), so we need to split our string into single, non-breaking lines in order to successfully mock-up the TextView layout: val commentParts = spannable.toString().split("\r")
  4. Then, the following algorithm creates StaticLayout instances for each line, and checks if the lines exceed the image container height. In case they do, we finally can get the position of the last char of the last line, adjacent to the right of the image container. And then we need to create a line break by ourselves, in order to let LeadingMarginSpanLayout know to stop making margins starting with [end + 1] char position:

     var endReached: Boolean = false;
                commentParts.forEach {
                    if (endReached) return@forEach
    
                    val layout: StaticLayout = StaticLayout(it, holder.mCommentTextView!!.paint,
                            textViewWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f, false)
                    if (layout.lineCount > 0) {
                        var localHeight: Int
    
                        for (lineIndex in 0..layout.lineCount - 1) {
                            localHeight = layout.getLineBottom(lineIndex)
                            if (localHeight + totalHeightOfLines > imageContainerHeight) {
                                endReached = true
                                end = layout.getLineEnd(lineIndex)
                                val spannableStringBuilder = SpannableStringBuilder(spannable)
                                if (spannable.substring(end - 1, end) != "\n" &&
                                        spannable.substring(end - 1, end) != "\r") {
                                    if (spannable.substring(end - 1, end) == " ") {
                                        spannableStringBuilder.replace(end - 1, end, "\n")
                                    } else {
                                        spannableStringBuilder.insert(end, "\n")
                                    }
                                }
                                spannable = SpannableString(spannableStringBuilder)
                                break
                            }
                        }
                        totalHeightOfLines += layout.lineCount * holder.mCommentTextView!!.lineHeight
                    }
                }
    
  5. And the last thing, set LeadingMarginSpan2 on the spannable string:

    spannable.setSpan(CommentLeadingMarginSpan2(
                        CommentLeadingMarginSpan2.calculateLeadingMarginWidthInPx(holder)),
                        0, if (end == 0) spannable.length else end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    holder.mCommentTextView!!.text = spannable
    holder.mCommentTextView!!.requestLayout()
    
  6. Eventually, the span is set correctly!

    !

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download