3

I'm inserting ImageSpans inside TextViews, which are part of a RecyclerView. Most of the time it works fine, but as you scroll up and down sometimes the TextView height is wrong for no obvious reason - there's too much vertical whitespace.

In this layout on a specific device, the correct height for a TextView with 1 line of text is 48, and the wrong height is 80 (I got these values from HierarchyViewer). The interesting thing is that the correct height for a TextView with 1 line of text including an ImageSpan is 80. So what seems to be happening is as the TextView gets recycled by RecyclerView, it sometimes keeps the height representing its old content (which included an ImageSpan).

This screenshot shows the "Anore" and "Halal blahs" TextViews as being 80 pixels high, which is wrong. The "Hello" TextView is correct at 48 pixels.

Wrong height (80)

While investigating this I opened DDMS and ran "Dump UI hierarchy" and something interesting happened: the TextView heights corrected themselves on the fly:

Correct height (48)

That's pretty compelling evidence that the problem is TextView isn't being laid out properly after updating the text, so I tried various ways of calling forceLayout() and invalidate() on the TextView and its parent views, but it didn't help.

I tried replacing RecyclerView with ListView, no luck. I tried having all ImageSpans use the same drawable, no luck.

I ran HierarchyViewer but it didn't "fix" the layouts like UI Automator did. Not even when I pressed the "invalidate layout" and "request layout" buttons.

I'm not doing anything fancy to set the ImageSpans:

SpannableStringBuilder stringBuilder = new SpannableStringBuilder( rawText );
for( int i = inlineImages.size() - 1; i >= 0; i-- ) {
    InlineImage img = inlineImages.get( i );
    stringBuilder.insert( img.idx, "x" );
    ImageSpan span = new ImageSpan( context, img.drawable );
    stringBuilder.setSpan( span, img.idx, img.idx + 1, 0 );
}

holder.mText.setText( stringBuilder );

// none of this helps
holder.mText.forceLayout();
holder.mText.requestLayout();
holder.mText.invalidate();

Here's the relevant part of the layout for each list item:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

    <LinearLayout
        android:id="@+id/speech_bubble"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:background="@drawable/speech_bubble_right"
        android:layout_marginLeft="40dp"
        >

        <TextView
            android:id="@+id/text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:layout_toRightOf="@id/timestamp"
            />

    </LinearLayout>

</RelativeLayout>

I tried invalidating and forcing a layout on the TextView parent (LinearLayout) but it did nothing.

I tried editing the layout down to literally just this:

<?xml version="1.0" encoding="utf-8"?>
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceMedium"
    />

.. and the problem still happens. It's definitely the fault of TextView itself.

foo64
  • 2,075
  • 3
  • 18
  • 25
  • What is the TextView nested in? Do you have a reference to this ViewGroup? – Gil Moshayof Mar 31 '15 at 11:12
  • Edited question to include the list item layout. I tried invalidating and forcing a layout on the parent, but it didn't do anything. – foo64 Mar 31 '15 at 19:27
  • You might temporarily remove the `LinearLayout` and see what your results are. You might subclass `TextView`, override some interesting methods (e.g., `onMeasure()`), and try to figure out what's not getting called when you think it should. You could try a `setText()` variant that takes a `BufferType`, though that shouldn't be needed. BTW, `android:layout_toRightOf` is for immediate children of a `RelativeLayout`, not for children of a `LinearLayout`. – CommonsWare Apr 05 '15 at 22:50
  • You have a `LinearLayout` inside a `RelativeLayout`. This is virtually never needed. – rds Apr 06 '15 at 10:15
  • For brevity, the layout I posted is a stripped down version of the actual layout. I was posting only the immediate parents of the TextView. There are additional views in the actual layout that make this hierarchy necessary. – foo64 Apr 06 '15 at 16:54
  • @CommonsWare Good catch. At some point during re-arranging this layout, the `layout_toRightOf` got moved to the wrong view. It belongs in TextView's parent. Unfortunately this wasn't the source of my problem.. – foo64 Apr 06 '15 at 17:00
  • We have this issue on small set of devices only, running 4.0.3. On other versions all is ok. – ernazm Jul 24 '15 at 15:51
  • @foo64 What's the device and OS version where you encountered this issue? – ernazm Jul 24 '15 at 16:23
  • i have the same problem,i solve it by create a textview in very item,but i don't think it is a good solution – Egos Zhang Jul 27 '16 at 12:34

2 Answers2

1

I'm guessing that the issue here isn't with the measurement of the TextView, but rather, the measurement of the TextView's container (some ViewGroup - perhaps a LinearLayout?).

If this ViewGroup's layout params is set to WRAP_CONTENT for the height, then for some reason, the height isn't being calculated again on time. To solve this, try the following:

int widthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
holder.mContainer.measure(widthMeasureSpec, heightMeasureSpec); // perhaps a simple call to requestLayout or forceLayout will be enough?

If you know the exact height the cell should be, you can use that as well - simple replace the line where I calculate the heightMeasureSpec with the following:

int heightMeasureSpec = MeasureSpec.makeMeasureSpec(calculatedHeight + topPadding + bottomPadding, MeasureSpec.EXACTLY);

Hope this helps.

Gil Moshayof
  • 16,633
  • 4
  • 47
  • 58
  • Actually, the problem is with the measurement of TextView itself, because its height is wrap_content and at runtime the height is 80 when it should be 48. This is according to HierarchyViewer. – foo64 Mar 31 '15 at 19:25
  • Probably that's because TextViev didn't measure itself if it's content not being changed and measurement is doing before onBindViewHolder. So when TextView is being rebound, it's contents not changed for that moment and measurement is broken. – s.maks Apr 13 '15 at 16:09
0

Got a little bit tricky

setTextSize(someDifferentSize);
setTextSize(normalSize);

Seems like it is some kind of line height problem, happened when image span is taller than it. I will dig into that if I have time.

Neil
  • 664
  • 6
  • 16