Search code examples
androidxmlandroid-layoutandroid-drawable

Android XML: Custom button drawable without duplicate xml for each button state?


I'm trying to make a custom_button, and what I have so far is this, two states (pressed and enabled) have their own shape with a border:

<?xml version='1.0' encoding='utf-8'?>
<selector xmlns:android='http://schemas.android.com/apk/res/android'>

    <!-- Pressed -->
    <item android:state_pressed='true'>
        <layer-list>
            <item> <!-- Border -->
                <shape
                    android:shape='rectangle'>

                    <solid android:color='@color/Foreground'/>
                </shape>
            </item>

            <item android:bottom='2dp' android:top='2dp' android:left='2dp' android:right='2dp'> <!-- Color -->
                <shape
                    android:shape='rectangle'>

                    <solid android:color='@color/WidgetSoft'/>
                </shape>
            </item>
        </layer-list>
    </item>

    <!-- Enabled -->
    <item>
        <layer-list>
            <item> <!-- Border -->
                <shape
                    android:shape='rectangle'>

                    <solid android:color='@color/Foreground'/>
                </shape>
            </item> <!-- Color -->

            <item android:bottom='2dp' android:top='2dp' android:left='2dp' android:right='2dp'> <!-- Base Color -->
                <shape
                    android:shape='rectangle'>

                    <solid android:color='@color/Widget'/>
                </shape>
            </item>
        </layer-list>
    </item>

</selector>

This works, but it specifies two identical buttons where only the colors are different. When I add the other button states and more look 'n feel this is gonna be more and more duplicating. Is there a way of having a base_button.xml drawable where the basic same-for-every-button attributes can be set and a custom_button.xml where attributes specific to the button state can be set in their corresponding tag?

Below is a pseudo-example of what I mean, but I don't know how to make that a reality. custom_button.xml handles the button states and base_button.xml specifies the basic characteristics of the button.

custom_button.xml:

<?xml version='1.0' encoding='utf-8'?>
<selector xmlns:android='http://schemas.android.com/apk/res/android'>

    <!-- Pressed -->
    <item android:state_pressed='true' android:drawable='@drawable/base_button'>

        <!-- SET GLOBAL_BORDER_COLOR TO '@color/btnPressedBorder' -->
        <!-- SET GLOBAL_BASE_COLOR TO '@color/btnPressedBase' -->

    </item>

    <!-- Enabled -->
    <item  android:drawable='@drawable/base_button'>

        <!-- SET GLOBAL_BORDER_COLOR TO '@color/btnEnabledBorder' -->
        <!-- SET GLOBAL_BASE_COLOR TO '@color/btnEnabledBase' -->

    </item>
    
</selector>

base_button.xml:

<?xml version='1.0' encoding='utf-8'?>
<layer-list xmlns:android='http://schemas.android.com/apk/res/android'>
    <item>
        <shape
            android:shape='rectangle'>

            <solid android:color= GLOBAL_BORDER_COLOR />
        </shape>
    </item>

    <item android:bottom='2dp' android:top='2dp' android:left='2dp' android:right='2dp'>
        <shape
            android:shape='rectangle'>

            <solid android:color= GLOBAL_BASE_COLOR />
        </shape>
    </item>
</layer-list>

Is this even possible in XML? I came across xsl:variable online, but couldn't get it to work. Any suggestions on how to this or alternative ways of achieving the same result are much appreciated!


Solution

  • You can use a ColorStateList for the colors. Define a ColorStateList for the widget's border and one for its fill. The state of the widget will be pushed down to the ColorStateList where the color will be selected. You can then define a drawable as follows:

    base_button.xml

    <layer-list>
        <item>
            <shape android:shape='rectangle'>
                <solid android:color="@color/widget_border" />
            </shape>
        </item>
    
        <item
            android:bottom='2dp'
            android:left='2dp'
            android:right='2dp'
            android:top='2dp'>
            <shape android:shape='rectangle'>
                <solid android:color="@color/widget_fill" />
            </shape>
        </item>
    </layer-list>
    

    The ColorStateLists will look like this. (Forgive the color selection.)

    widget_fill.xml

    <selector>
        <item android:color="@android:color/holo_red_light" android:state_pressed="true" />
        <item android:color="@android:color/holo_blue_light" android:state_enabled="true" />
    </selector>
    

    widget_border.xml

    <selector>
        <item android:color="@android:color/black" android:state_pressed="true" />
        <item android:color="@android:color/holo_red_light" android:state_enabled="true" />
    </selector>
    

    And finally, the sample layout file:

    activity_main.xml

    <androidx.constraintlayout.widget.ConstraintLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
    
        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:background="@drawable/base_button"
            android:text="Button"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    Putting it all together, it will look like this:

    enter image description here