0
votes

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.

2

2 Answers

1
votes

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!
0
votes

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
}
}