Search code examples
androidlistviewscrolltextviewongloballayoutlistener

Android TextView inside ListView does not measure the correct height until manually scrolling


I have a listView filled with multi-line TextViews. Each TextView has a different amount of text. After pressing a button, the user is taken to another Activity where they can change the font and the font size. Upon reEntry into the Fragment, if these settings have changed, the listView is reset and the measurements of the TextViews are changed.

I need to know the measured height of the first TextView in view after these settings have changed. For some reason, the measured height is different at first after it is measured. Once I manually scroll the list, the real height measurement is recorded.

Log output:

After measured: tv height = 2036

After measured: tv height = 2036

After scroll: tv height = 7950

Minimal Code:

class FragmentRead : Fragment() {
    private var firstVisiblePos = 0
    lateinit var adapterRead: AdapterRead
    lateinit var lvTextList: ListView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        lvTextList = view.findViewById(R.id.read_listview)
        setListView(lvTextList)


        lvTextList.setOnScrollListener(object : AbsListView.OnScrollListener {
            var offset = 0
            override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
                if(scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
                    offset = if(lvTextList.getChildAt(0) == null) 0 else lvTextList.getChildAt(0).top - lvTextList.paddingTop
                    println("After scroll: tv height = ${lvTextList[0].height}")
                }
            }

            override fun onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {
                firstVisiblePos = firstVisibleItem
            }
        })
    }
    /*=======================================================================================================*/

    fun setListView(lv: ListView) {
        adapterRead = AdapterRead(Data.getTextList(), context!!)
        lv.apply {this.adapter = adapterRead}
    }
    /*=======================================================================================================*/

    inline fun <T : View> T.afterMeasured(crossinline f: T.() -> Unit) {
        viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                if(measuredWidth > 0 && measuredHeight > 0) {
                    println("After measured: tv height = ${lvTextList[0].height}")
                    viewTreeObserver.removeOnGlobalLayoutListener(this)
                    f()
                }
            }
        })
    }
    /*=======================================================================================================*/
    override fun onStart() {
    if(Settings.settingsChanged) {
        setListView(lvTextList)
        lvTextList.afterMeasured {
                println("After measured: tv height = ${lvTextList[0].height}")
            }
        }
    }
}

What I have tried:

I have tried setting a TextView with the text and layoutParams and reading the height as explained here (Getting height of text view before rendering to layout) but the results are the same. The measured height is much less than after I scroll the list.

I have also tried to programatically scroll the list using lvTextList.scrollBy(0,1) in order to trigger the scroll listener or whatever else is triggered when the correct height is read.

EDIT: I put a delay in after coming back to the Fragment:

Handler().postDelayed({
println("tv height after delay = ${lvScriptureList[0].height}")}, 1000)

And this reports the correct height. So my guess is that the OnGlobalLayoutListener is being called to early. Any way to fix this?


Solution

  • Here is my solution. The reason I need to know the height of the TextView is because after the user changes settings (e.g. font, font size, line spacing) the size of the TextView changes. In order to return to the same spot the TextView was in previously, I need to know the height of the newly measured TextView. Then I can go to the same spot (or very close) based on the position previously and recalculating it based on the new height.

    So after the settings are changed and the Fragment is loaded back up:

    override fun onStart(){
        if(Settings.settingsChanged) {
            setListView(lvTextList)
            lvTextList.afterMeasured {
                lvTextList.post { lvTextList.setSelectionFromTop(readPos, 0) }
                Handler().postDelayed({
                    val newOffset = getNewOffset() // Recalculates the new offset based on the last offset and the new TextView height
                    lvTextList.post { lvTextList.setSelectionFromTop(readPos, newOffset) }
                }, 500)
            }
        }
    }
    

    For some reason I had to scroll to a position first before scheduling the delay so I simply just scrolled to the beginning of the TextView.

    The 500ms is goofy and is just an estimate but it works. It actually works with a value of 100ms on my phone but I want to ensure a better chance of success across devices.