Search code examples
android-layoutandroid-custom-viewandroid-stylesandroid-textinputlayoutandroid-textinputedittext

Apply Style in custom view where layout root tag is merge


I have a TextInputLayout with a custom Style that needs to be heavily reused so I am trying to turn it into a custom view.

Here is xml to be reused :

<com.google.android.material.textfield.TextInputLayout
        style="@style/TextInputLayoutAppearance"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.textfield.TextInputEditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </com.google.android.material.textfield.TextInputLayout>

TextInputLayoutAppearance is the custom style I have created in the styles.xml

Here is the Class for my custom view :

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

    init {
        LayoutInflater.from(context).inflate(R.layout.view_outlined_textinput, this, true)
    }
}

Here is the view_outlined_textinput I have adapted from the original xml above, for the custom view :

<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:parentTag="com.google.android.material.textfield.TextInputLayout">

    <com.google.android.material.textfield.TextInputEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</merge>

There are 2 things to notice here :

  1. The layout uses a merge tag to avoid redundant view in the view hierarchy.

  2. It is crucial to apply the custom style. The style is applied in the original xml using style= syntax. However, since the merge tag is being used for the custom view it cannot be done that way.

I tried setting the style as follows but it didn't work :

class OutlinedTextInput @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = R.style.TextInputLayoutAppearance
) : TextInputLayout(context, attrs, defStyleAttr) {

I assume the above solution should work as the 3rd parameter in the constructor is to pass in the style.

Another option is to set all the properties programatically in the init {} of my custom view but that defeats the purpose of having declared a style in the Styles file.

What are my options ?


Solution

  • Currently there are 4 constructor for a view

    1. public View (Context context) -> Simple constructor to use when creating a view from code.

    2. public View (Context context, AttributeSet attrs) -> Constructor that is called when inflating a view from XML

    3. public View (Context context, AttributeSet attrs, int defStyleAttr) -> Perform inflation from XML and apply a class-specific base style from a theme attribute.

    4. public View (Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) Added in API level 21

    The 3rd & 4th constructors are called by subclasses if they want to specify an attribute containing a default style, or a default style directly (in the case of the four-argument constructor)

    For example, a Button class's constructor would call this version of the super class constructor and supply R.attr.buttonStyle for defStyleAttr; this allows the theme's button style to modify all of the base view attributes (in particular its background) as well as the Button class's attributes. from the docs

    So, when you are creating your custom view and adding that view into an XML android will always call the second constructor which look like this under the hood

    public TextInputLayout(Context context, AttributeSet attrs) {
        this(context, attrs, attr.textInputStyle);
    }
    

    The third argument attr.textInputStyle is getting that specific style directly from the application theme.

    So, To achieve the result you are looking for you can do the following.

    1. In your attrs.xml add <attr name="attribute_name" format="reference" />

    eg. attrs.xml

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    <attr name="attribute_name" format="reference" />
    </resources>
    
    1. Add in your AppTheme in the file style.xml <item name="attribute_name">@style/your_style</item>

    eg. style.xml

    <resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        ...
        <item name="attribute_name">@style/your_style</item>
         ...
    </style>
    

    1. Finally, in your custom view second constructor you pass that attribute as a parameter

      constructor(context: Context, attrs: AttributeSet) : 
        super(
              context,
              attrs,
              R.attr.attribute_name
             )
      

    I hope it helps!