Search code examples
androidandroid-viewclipping

Weighted buttons in vertical linear layout cutting off text if button text line counts vary


TL;DR: Here's the gist of everything that I can think of that's relevant to the issue I'm facing: [GIST LINK]

And here's a picture of the problem

Text cutting off problem: illustrated

I'm trying to set up a number of buttons that will all grow to the same size as each other by equal weighting in a vertically oriented LinearLayout container.

The problem I'm facing surfaces when the text on these buttons cause a different number of lines per button.

Let's say n is the lowest line count for the buttons and m is the highest line count; any descenders in the text of buttons with line count m are cut off. Refer to the words "qshowing my clipping problem" in the linked screengrab, where all descenders are cut off.

How can I go about fixing this? The clipping gets much worse if I introduce android:lineSpacingExtra to the button style.

If it's relevant, my minimum API is set to 21


Solution

  • I've fixed this using RxJava to set the height programmatically to the correct maximum so that no clipping occurs. If there is a better solution I'll be glad to see it, but this is what is working for me for now:

    class MyActivity {
    
        // ...
    
        private val compositeDisposable = CompositeDisposable()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            setContentView(R.layout.my_activity)
            // ...
    
            val container: LinearLayout = findViewById(R.id.container)
    
            val numBtns = getNumBtnsToAdd()
            val btnList: MutableList<Button> = mutableListOf()
            val margin10 = dpToPx(10f).toInt()
    
            val countDown = CountDownLatch(numBtns)
            val desiredLp = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0).apply {
                gravity = Gravity.CENTER
                setMargins(margin10, margin10, margin10, margin10)
            }
    
            // Completable will be run once per subscriber and emit a success or error
            val layoutCompletable = Completable.fromAction {
                countDown.await()
                for (btn in btnList) btn.layoutParams = desiredLp
            }.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread())
    
            compositeDisposable.add(
                layoutCompletable.subscribe({
                    Log.d("MyActivity", "Set LayoutParams on all buttons.")
                }, Throwable::printStackTrace)
            )
    
            for (i in 0 until numBtns) {
                val btn = Button(this, null, 0, R.style.button_style).apply {
                    layoutParams = LinearLayout.LayoutParams(desiredLp).apply { height = LinearLayout.LayoutParams.WRAP_CONTENTS }
                    text = if (i == 0) "Button${i+1} with short text"
                                  else "Button${i+1} with text that will span multiple lines showing my clipping problem"
                    setOnClickListener { doSomething() }
                }
    
                val listener = object : ViewTreeObserver.OnGlobalLayoutListener {
                    override fun onGlobalLayout() {
                        countDown.countDown()
                        val height = btn.height
                        if (height > desiredLp.height) desiredLp.height = height
                        btn.viewTreeObserver.removeOnGlobalLayoutListener(this)
                    }
                }
                btn.viewTreeObserver.addOnGlobalLayoutListener(listener)
                btnList.add(btn)
                container.addView(btn)
            }
    
            // ...
        }
    
        override fun finish() {
            compositeDisposable.clear()
            super.finish()
        }
    
        // ...
    
    }