androidmaterial-components-androidmaterial-componentsprogress-indicatorandroid-gravity

Android iconGravity not working for MaterialButton with CircularProgressIndicator


I've created a custom view that extends MaterialButton that replaces the drawable with a CircularProgressIndicator when it's loading.

However when I replace the drawable with the CircularProgressIndicator the iconGravity doesn't work anymore. I don't see what I'm doing wrong.

I've been able to boil the class down to this:

class LoadingButton constructor(context: Context) : MaterialButton(context) {

    private val progressIndicatorDrawable by lazy {
        val progressIndicatorSpec = CircularProgressIndicatorSpec(context, null, 0, R.style.Widget_MaterialComponents_CircularProgressIndicator_ExtraSmall)
        progressIndicatorSpec.indicatorColors = intArrayOf(getColor(context, R.color.white))
        IndeterminateDrawable.createCircularDrawable(context, progressIndicatorSpec).apply {
            setVisible(true, true)
        }
    }

    init {
        // the following drawable is aligned to the start of the view (incorrect).
        icon = progressIndicatorDrawable
        // the following drawable is aligned to the start of the text (correct).
        // icon = DrawableUtil.getDrawable(context, R.drawable.icon_delete)
        text = "Delete"
        iconGravity = ICON_GRAVITY_TEXT_START
    }
}

I'm using this dependency

// I've also tried 1.5.0-alpha02
implementation "com.google.android.material:material:1.3.0"

Correct position The drawable is positioned at the start of the text. drawable correct position

Incorrect position The progress indicator is not positioned at the start of the text anymore. progress indicator incorrect position


Solution

  • It is curious that one drawable works but another doesn't. It may be worth reporting as a bug. In the meantime, I can offer a workaround that doesn't involve a lot of work (IMO).

    If we take your custom view and remove the iconGravity and set the gravity for the text to the start and vertical center:

    gravity = Gravity.START or Gravity.CENTER_VERTICAL
    

    we can get the indeterminate and the text together on the left side of the button:

    enter image description here

    The issue now is how to get the pair centered in the button. We can take a hint from the code for MaterialButton on an approach on how much space is needed on the left to center the drawable and text.

    int newIconLeft =
        (buttonWidth
            - getTextWidth()
            - ViewCompat.getPaddingEnd(this)
            - localIconSize
            - iconPadding
            - ViewCompat.getPaddingStart(this))
            / 2;
    

    Putting it all together:

    enter image description here

    Here is the demo code. It may require some tweaking if your actual button is more complex than is pictured in your question.

    class LoadingButton @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null
    ) : MaterialButton(context, attrs) {
    
        // true - use indeterminate; false = use delete icon
        private val useIndeterminate = true
    
        private val progressIndicatorDrawable by lazy {
            val progressIndicatorSpec = CircularProgressIndicatorSpec(
                context,
                null,
                0,
                R.style.Widget_MaterialComponents_CircularProgressIndicator_ExtraSmall
            )
            progressIndicatorSpec.indicatorColors = intArrayOf(getColor(context, R.color.white))
            IndeterminateDrawable.createCircularDrawable(context, progressIndicatorSpec).apply {
                setVisible(true, true)
            }
        }
    
        init {
            if (useIndeterminate) {
                icon = progressIndicatorDrawable
                gravity = Gravity.START or Gravity.CENTER_VERTICAL
            } else {
                icon = ContextCompat.getDrawable(context, R.drawable.icon_delete)
                iconGravity = ICON_GRAVITY_TEXT_START
                gravity = Gravity.CENTER
            }
            text = "Delete"
        }
    
        // This is homw much we need to shift the contents of the button to the right in order to
        // center it.
        private var xShift = 0f
        override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
            super.onLayout(changed, left, top, right, bottom)
            if (useIndeterminate) {
                xShift = (width
                        - icon.intrinsicWidth
                        - paint.measureText(text.toString())
                        - iconPadding
                        - ViewCompat.getPaddingStart(this)
                        - ViewCompat.getPaddingEnd(this)) / 2f
            }
        }
    
        override fun onDraw(canvas: Canvas?) {
            // Shift right before drawing.
            canvas?.withTranslation(xShift) {
                super.onDraw(canvas)
            }
        }
    }