1

I am trying to create a custom button with progress bar inside of it in Android.

The button should have 2 states: Normal and Loading.

In Normal state it should show a text while in Loading state it should show a centerred circular progress indicator instead of the text! When the button state returns to "Normal" state it should show the text again.

To achieve this, I've thought about create a custom view which build from a RelativeLayout and inside of it there is a TextView and a Circular progress indicator and change their visibility in code according to the state.

This idea and logic works pretty good.

Please refer to images of my buttons with the progress indicators: enter image description here

However, the problem comes when I want to apply a selector to this view, I've created a style and a selector for each button but it just not setting the right background to the view when its disabled.

A RelativeLayout doesn't has an enabled attribute available in its xml so I had to add a styleable attr and change its state in code with isEnabled = false or something like that. This makes it disabled in did, but the background stays as it is enabled (The selector not working).

This is my "Button" source code:

class ProgressButton @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
    ) : RelativeLayout(context, attrs, defStyleAttr) {

private val progressBar: LottieAnimationView
private val buttonTextView: TextView

init {
    val root = LayoutInflater.from(context).inflate(R.layout.progress_button, this, true)
    buttonTextView = root.findViewById(R.id.button_text)
    progressBar = root.findViewById(R.id.progress_indicator)
    loadAttr(attrs, defStyleAttr)
}

private fun loadAttr(attrs: AttributeSet?, defStyleAttr: Int) {
    val arr = context.obtainStyledAttributes(
        attrs,
        R.styleable.ProgressButton,
        defStyleAttr,
       0
    )

    val buttonText = arr.getString(R.styleable.ProgressButton_text)
    val loading = arr.getBoolean(R.styleable.ProgressButton_loading, false)
    val enabled = arr.getBoolean(R.styleable.ProgressButton_enabled, true)
    isEnabled = enabled
    arr.recycle()

    buttonTextView.text = buttonText
    setLoading(loading)
}

fun setLoading(loading: Boolean){
    if(loading){
        buttonTextView.visibility = View.GONE
        progressBar.visibility = View.VISIBLE
    } else {
        buttonTextView.visibility = View.VISIBLE
        progressBar.visibility = View.GONE
    }
}

}

This its layout:

<RelativeLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="50dp">

<TextView
    android:id="@+id/button_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:textAppearance="?android:attr/textAppearanceButton"
    android:text="OK" />


<com.airbnb.lottie.LottieAnimationView
    android:id="@+id/progress_indicator"
    android:layout_width="@dimen/progressbar_width"
    android:layout_height="@dimen/progressbar_width"
    android:layout_centerInParent="true"
    android:visibility="gone"
    app:lottie_autoPlay="true"
    app:lottie_loop="true"
    app:lottie_rawRes="@raw/lottile_button_loader" />

</RelativeLayout>

This a the background with selector for it:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
  android:shape="rectangle">
    <corners android:radius="@dimen/components_corner_radius" />
    <solid android:color="@color/button_black_bg_selector" />
</shape>

<selector xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:color="@color/black" android:state_enabled="true" />
   <item android:color="@color/buttons_black_pressed" android:state_pressed="true" />
   <item android:color="@color/buttons_black_disabled" android:state_enabled="false" />
   <item android:color="@color/black" />

This is the styling and theme:

 <style name="Theme.Widget.ProgressButton" parent="">
    <item name="android:textAppearanceButton">@style/TextAppearance.Body.White</item>
</style>

<style name="Widget.ProgressButton.Black" parent="@style/Theme.Widget.ProgressButton">
    <item name="android:colorControlHighlight">@color/buttons_black_pressed</item>
    <item name="android:background">@drawable/progress_button_black</item>
</style>

And finally, this how i use it in a fragment layout xml:

 <com.example.widgets.ProgressButton
        android:id="@+id/button_black_loading"
        android:theme="@style/Widget.ProgressButton.Black". //This is where it gets its style and theme
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginHorizontal="24dp"
        android:layout_marginTop="16dp"
        app:loading="true"/>

Any help will be appreciated.

Moti Bartov
  • 3,454
  • 33
  • 42

2 Answers2

1

Finally I've came to a pattern that seems to work with a single style line in the custom view usage xml (e.g. Fragment or Activity layout).

For each button I've defined similar styling blocks that looks like this:

<style name="Theme.Widget.Button.Black" parent="">
    <item name="android:textAppearanceButton">@style/TextAppearance.BlackButton</item>  //This will set the theme for the button internal TextView
    <item name="android:colorControlHighlight">@color/buttons_black_pressed</item> //This will set the highlight color for ripple effect
</style>

<style name="Widget.Button.Black" parent="">
    **<item name="android:theme">@style/Theme.Widget.Button.Black</item>** //Note this theme attribute which takes that above styling and applying it as a theme!! 
    <item name="android:background">@drawable/button_black</item> //A shape with selector drawable
</style>

The background drawable:

<ripple xmlns:android="http://schemas.android.com/apk/res/android"
   android:color="?android:attr/colorControlHighlight"> //This comes from the theme for the ripple effect
   <item android:drawable="@drawable/button_black_shape"/> //The selector 
</ripple>

The background shape drawable:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
   <corners android:radius="@dimen/components_corner_radius" />
   <solid android:color="@color/button_black_bg_selector" />
</shape>

The selector:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:color="@color/black" android:state_enabled="true" />
   <item android:color="@color/buttons_black_pressed" android:state_pressed="true" />
   <item android:color="@color/buttons_black_disabled" android:state_enabled="false" />
   <item android:color="@color/black" />
</selector>

And finally using the button in fragment XML like this:

 <com.sample.widgets.ProgressButton
        android:id="@+id/button_black_enabled"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginHorizontal="24dp"
        android:layout_marginTop="16dp"
        app:enabled="true"
        app:text="OK"
        style="@style/Widget.Button.Black"/> //The style brings a theme also!
Moti Bartov
  • 3,454
  • 33
  • 42
0

After adding the selector you need to change the state of ProgressButtonI am adding essential code below which should work.

Selector should be like :-

<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/black" android:state_enabled="true" />
<item android:drawable="@color/buttons_black_pressed" android:state_pressed="true" />
<item android:drawable="@color/buttons_black_disabled" android:state_enabled="false" />

and ProgressButton.kt

class ProgressButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {

private val progressBar: LottieAnimationView
private val buttonTextView: TextView

init {
    val root = LayoutInflater.from(context).inflate(R.layout.progress_button, this, true)
    buttonTextView = root.findViewById(R.id.button_text)
    progressBar = root.findViewById(R.id.progress_indicator)
    loadAttr(attrs, defStyleAttr)
}

private fun loadAttr(attrs: AttributeSet?, defStyleAttr: Int) {
    // this line can be removed if you are setting selector in xml   
    setBackgroundResource(R.drawable.button_selector)
    setLoading(true)
}

 fun setLoading(loading: Boolean) {
    if (loading) {
        buttonTextView.visibility = View.GONE
        progressBar.visibility = View.VISIBLE
    } else {
        buttonTextView.visibility = View.VISIBLE
        progressBar.visibility = View.GONE
    }
    isEnabled = !loading
}
}
ADM
  • 20,406
  • 11
  • 52
  • 83
  • Thanks I did set it in XML, in the style i've set the background with a drawable which include the selector. Then in the layout where i am using the button i set its theme with that style. – Moti Bartov Feb 27 '21 at 08:12
  • Yeah i saw it later . Use the selector in answer above . i do not see the point of using a Style here which derived from `Theme.Widget.ProgressButton` . You can use the selector directly and change the state with `isEnabled = !loading`. – ADM Feb 27 '21 at 08:16
  • So How can I set different selectors (with different colors), as you can see I want to have blue, green, black etc' buttons. – Moti Bartov Feb 27 '21 at 09:33
  • Just create different selectors and set them with `setBackgroundResource` at runtime if you want to change it at runtime or just set it in xml . I do not see a problem here. whats holding you ? – ADM Feb 27 '21 at 09:35
  • 1
    Thanks!! It looks like that putting the background drawable (shape with selector) directly the xml worked!! But I don't understand why putting the same drawable background through the style didn't worked?? What is the difference? – Moti Bartov Feb 27 '21 at 11:26