Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 565
Wirling Avatar asked Oct 14 '22 20:10

Wirling


2 Answers

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)
        }
    }
}
like image 124
Cheticamp Avatar answered Oct 19 '22 09:10

Cheticamp


Disclaimer: it seems a bug of the material components library and this answer is not the solution for the code reported in the answer.

If you can use Compose as workaround just apply something like:

Button (
    modifier = Modifier.width(150.dp),
    onClick = { /* Do something! */ }
){
    CircularProgressIndicator(color = White,
        modifier = Modifier
            .size(24.dp)
            .align(Alignment.CenterVertically))
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Delete")
}

enter image description here

To incorporate the Compose UI content in an existing layout, just add in the xml layout:

<androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="150dp"
        android:layout_height="wrap_content" />

and in your Fragment or Activity:

    val view: ComposeView = findViewById(R.id.compose_view)
    view.apply {
        setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
        setContent {
            MaterialTheme {
                /* The code above */
                Button (
                    onClick = { /* Do something! */ }){
                    CircularProgressIndicator(color = White, modifier = Modifier.size(24.dp).align(
                        Alignment.CenterVertically))
                    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
                    Text("Delete")
                }
            }
        }
    }

Here you can find more details about the Interoperability.

like image 43
Gabriele Mariotti Avatar answered Oct 19 '22 08:10

Gabriele Mariotti