Include layout with custom attributes

I'm building a complex layout and I want to use include tag for my custom component, like this:

<include layout="@layout/topbar"/>

The topbar layout has custom root component and in layout xml file it's defined like this:

<my.package.TopBarLayout
 ... a lot of code

Now, I wanna pass my custom defined attributes to "topbar" like this:

<include layout="@layout/topbar" txt:trName="@string/contacts"/>

And then get the value of those custom attributes in custom component code or ideally in xml.

Sadly, I cannot get value of txt:trName attribute to make it to the topbar layout, I just don't receive anything in code. If I understand correctly from that documentation page, I can set no attributes for layouts used via include, but id, height and width.

So my question is how can I pass my custom defined attributes to layout which is added via include?


Solution 1:

I know this is an old question but I came across it and found that it is now possible thanks to Data Binding.

First you need to enable Data Binding in your project. Use DataBindingUtil.inflate (instead of setContentView, if it's Activity) to make it work.

Then add data binding to the layout you want to include:

<?xml version="1.0" encoding="utf-8"?>    
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="title" type="java.lang.String"/>
    </data>
    <RelativeLayout xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/screen_header"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="top"
        android:gravity="center">

    ...

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:textSize="20sp"
            android:textStyle="bold"
            android:text="@{title}"/>

    ...

    </RelativeLayout>
</layout>

Finally, pass the variable from the main layout to the included layout like this:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        ...
    </data>
    ...
    <include layout="@layout/included_layout"
        android:id="@+id/title"
        app:title="@{@string/title}"/>
    ...
</layout>

Solution 2:

It's not possible to attributes other than layout params, visibility or ID on an include tag. This includes custom attributes.

You can verify this by looking at the source of the LayoutInflater.parseInclude method, around line 705: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.2_r1.1/android/view/LayoutInflater.java#640

The inflater only applies the ID and visibility attributes to the included layout.

Solution 3:

I ran into this issue today. For whatever it is worth, I think there is a straight-forward work around. Instead of adding attributes to the include tag, create a custom wrapper view for the include and add attributes to that. Then, do the include from the wrapper. Have the wrapper class implementation extract the attributes and pass along to its single child, which is the root view of the include layout.

So, say we declare some custom attributes for a wrapper called SingleSettingWrapper like this -

<declare-styleable name="SingleSettingWrapper">
    <attr name="labelText" format="string"/>
</declare-styleable>

Then, we create two custom view classes - one for the wrapper (SingleSettingWrapper) and one for the child (SingleSettingChild) that will be included -

<!-- You will never end up including this wrapper - it will be pasted where ever you wanted to include. But since the bulk of the XML is in the child, that's ok -->
<com.something.SingleSettingWrapper
    android:id="@+id/wrapper"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    custom:labelText="@string/my_label_string">

    <!-- Include the child layout -->
    <include layout="@layout/setting_single_item"/>

</com.something.SingleSettingWrapper>

For the child, we can put whatever complex layout in there that we want. I'll just put something basic, but really you can include whatever -

<com.something.SingleSettingItem
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <RelativeLayout >
        <!-- add whatever custom stuff here -->
        <!-- in this example there would be a text view for the label and maybe a bunch of other stuff -->
        <!-- blah blah blah -->
    </RelativeLayout>
</com.something.SingleSettingItem>

For the wrapper (this is the key), we read all of our custom attributes in the constructor. Then, we override onViewAdded() and pass those custom attributes to our child.

public class SingleSettingWrapper extends FrameLayout 
{
    private String mLabel;

    public SingleSettingWrapper(Context context, AttributeSet attrs)
    {
        super(context, attrs);

        TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
                                                             R.styleable.SingleSettingWrapper,
                                                             0, 0);

        mLabel = a.getString(R.styleable.SingleSettingWrapper_labelText);
        a.recycle();
    }

    public void onViewAdded(View child)
    {
        super.onViewAdded(child);
        if (!(child instanceof SingleSettingItem))
            return;

       ((TextView)child.findViewById(R.id.setting_single_label)).setText(mLabel);
        /*
        Or, alternatively, call a custom method on the child implementation -
        ((SingleSettingItem)child)setLabel(mLabel);
        */
    }
}

Optionally, you can implement the child too and have it receive messages from the wrapper and modify itself (instead of having the wrapper modify the child as I did above).

public class SingleSettingItem extends LinearLayout
{
    public SingleSettingItem(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    public void setLabel(String l)
    {
        // set the string into the resource here if desired, for example
    }
}

At the end of the day, each of the XML files where you wanted to <include> your layout will contain about 7 lines of XML for the wrapper+include instead of the single include that you wanted, but if the included view contains hundreds of lines you're still way better off. For example -

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical" >

    <!-- this is the beginning of your custom attribute include -->
    <com.something.SingleSettingWrapper
        android:id="@+id/my_wrapper"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        custom:labelText="@string/auto_lock_heading">
        <include layout="@layout/setting_single_item"/>
    </com.something.SingleSettingWrapper>
    <!-- this is the end of your custom attribute include -->
</LinearLayout>

In practice, this seems to work pretty well and is relatively simple to set up. I hope it helps someone.

Solution 4:

Unfortunately, the only thing I can contribute is that I was also unable to set custom attributes on an include tag, and have them pass through to the included layout.

It may well not be possible at this point.