Tricky Android


Android tips, tricks and everything I found interesting


Protip. Inflating layout for your custom view

Today's protip will be about one of the problems people usually face when they create custom compound views.

Let's take a custom compound view as an example and try to figure out what does it take to create one.

As you can see, I have a quite decent view here - simple card-like widget. Since in theory I will have some logic inside this card, I decided to create a custom view for this situation.

The very common approach I am seeing throughout the internet - is to extend one of the built-in layouts and inflate custom layout during initialization:

Card.java

package com.trickyandroid.customview.app.view;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.trickyandroid.customview.app.R;

public class Card extends RelativeLayout {
    private TextView header;
    private TextView description;
    private ImageView thumbnail;
    private ImageView icon;

    public Card(Context context) {
        super(context);
        init();
    }

    public Card(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public Card(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        inflate(getContext(), R.layout.card, this);
        this.header = (TextView)findViewById(R.id.header);
        this.description = (TextView)findViewById(R.id.description);
        this.thumbnail = (ImageView)findViewById(R.id.thumbnail);
        this.icon = (ImageView)findViewById(R.id.icon);
    }
}

card.xml

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="@dimen/card_padding"
    android:background="@color/card_background">

    <ImageView
        android:id="@+id/thumbnail"
        android:src="@drawable/thumbnail"
        android:layout_width="72dip"
        android:layout_height="72dip"
        android:scaleType="centerCrop"/>

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Card title"
        android:layout_toRightOf="@+id/thumbnail"
        android:layout_toLeftOf="@+id/icon"
        android:textAppearance="@android:style/TextAppearance.Holo.Medium"
        android:layout_marginLeft="?android:attr/listPreferredItemPaddingLeft"/>

    <TextView
        android:id="@+id/description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Card description"
        android:layout_toRightOf="@+id/thumbnail"
        android:layout_below="@+id/title"
        android:layout_toLeftOf="@+id/icon"
        android:layout_marginLeft="?android:attr/listPreferredItemPaddingLeft"
        android:textAppearance="@android:style/TextAppearance.Holo.Small"/>

    <ImageView
        android:id="@+id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/icon"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"/>

</RelativeLayout>

So now, whenever we need to use our newly created custom view, the only thing we need to do is to add our view to the main layout like any other default views:

activity_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin">

    <com.trickyandroid.customview.app.view.Card
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</FrameLayout>

Looks very easy and straight forward, isn't it? But! Let's take a look at the view hierarchy:

As we can see, there is one extra RelativeLayout before our actual card's content. This is bacause our Card class is RelativeLayout and when we inflate actual content - we just add this content to this RelativeLayout.

Of course, this is not big of a deal for this particular situation - we don't do anything in our parent RelativeLayout. But when we have more complex layouts and the number of custom views is getting bigger - you actually can notice some performance hit. This is because UI engine having really hard time traversing all these layouts, measuring them and laying them out.

So the general rule would be - the deeper your layout is - the harder to traverse it. So always try to flatten your layout

Now let's see what we can do to flatten out layout.

Merge

One of the possible ways to reduce number of layouts in our case is to merge our actual card's content into parent view (Card class):

card.xml:

<?xml version="1.0" encoding="utf-8"?>

<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="@dimen/card_padding"
    android:background="@color/card_background">

    <ImageView
        android:id="@+id/thumbnail"
        android:src="@drawable/thumbnail"
        android:layout_width="72dip"
        android:layout_height="72dip"
        android:scaleType="centerCrop"/>

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Card title"
        android:layout_toRightOf="@+id/thumbnail"
        android:layout_toLeftOf="@+id/icon"
        android:textAppearance="@android:style/TextAppearance.Holo.Medium"
        android:layout_marginLeft="?android:attr/listPreferredItemPaddingLeft"/>

    <TextView
        android:id="@+id/description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Card description"
        android:layout_toRightOf="@+id/thumbnail"
        android:layout_below="@+id/title"
        android:layout_toLeftOf="@+id/icon"
        android:layout_marginLeft="?android:attr/listPreferredItemPaddingLeft"
        android:textAppearance="@android:style/TextAppearance.Holo.Small"/>

    <ImageView
        android:id="@+id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/icon"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"/>

</merge>

Here is what we got:

Great! We eliminated extra RelativeLayout, but lost our top-level card layout attributes - white background and padding. That's because <marge> tag merges its content, but not itself. So your top-level attributes are lost.
There are 3 simple ways to add them back:

1) Add them through the code:

Card.java:

    .....
    private void init() {
        inflate(getContext(), R.layout.card, this);
 setBackgroundColor(getResources().getColor(R.color.card_background));

        //Add missing top level attributes    
        int padding = (int)getResources().getDimension(R.dimen.card_padding);
        setPadding(padding, padding, padding, padding);

        this.header = (TextView)findViewById(R.id.header);
        this.description = (TextView)findViewById(R.id.description);
        this.thumbnail = (ImageView)findViewById(R.id.thumbnail);
        this.icon = (ImageView)findViewById(R.id.icon);
    }

2) Specify them when you add your card to the main layout:

activity_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin">

    <com.trickyandroid.customview.app.view.Card
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/card_background"
        android:padding="@dimen/card_padding"/>

</FrameLayout>

3) Define a stylable attribute and provide these values through the style. Thanks to @vovkab for pointing this out:

attr.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="Card">
        <attr name="cardStyle" format="reference"/>
    </declare-styleable>
</resources>

style.xml

    <!-- Base application theme. -->
    <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
        <item name="android:windowBackground">@color/main_background</item>
        <item name="cardStyle">@style/CardStyle</item>
    </style>

    <style name="CardStyle" parent="android:Widget.Holo.Light">
        <item name="android:padding">@dimen/card_padding</item>
        <item name="android:background">@color/card_background</item>
    </style>

</resources>

Card.java

public class Card extends RelativeLayout {
    private TextView header;
    private TextView description;
    private ImageView thumbnail;
    private ImageView icon;

    public Card(Context context) {
        super(context, null, R.attr.cardStyle);
        init();
    }

    public Card(Context context, AttributeSet attrs) {
        super(context, attrs, R.attr.cardStyle);
        init();
    }
    ..........

Note that in view's constructor I need to specify my stylable, not the style directly

Include

Another way to reduce number of layouts is to use <include> tag. Since my Card.java is RelativeLayout, I can make it a root view for my content. After that I need to include it into my main activity layout:

card.xml:

<com.trickyandroid.views.Card xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="@dimen/card_padding"
    android:background="@color/card_background">

    <ImageView
        android:id="@+id/thumbnail"
        android:src="@drawable/thumbnail"
        android:layout_width="72dip"
        android:layout_height="72dip"
        android:scaleType="centerCrop"/>

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Card title"
        android:layout_toRightOf="@+id/thumbnail"
        android:layout_toLeftOf="@+id/icon"
        android:textAppearance="@android:style/TextAppearance.Holo.Medium"
        android:layout_marginLeft="?android:attr/listPreferredItemPaddingLeft"/>

    <TextView
        android:id="@+id/description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Card description"
        android:layout_toRightOf="@+id/thumbnail"
        android:layout_below="@+id/title"
        android:layout_toLeftOf="@+id/icon"
        android:layout_marginLeft="?android:attr/listPreferredItemPaddingLeft"
        android:textAppearance="@android:style/TextAppearance.Holo.Small"/>

    <ImageView
        android:id="@+id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/icon"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"/>

</com.trickyandroid.views.Card>

Card.java:

public class Card extends RelativeLayout {
    private TextView header;
    private TextView description;
    private ImageView thumbnail;
    private ImageView icon;

    public Card(Context context) {
        super(context);
    }

    public Card(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public Card(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        this.header = (TextView)findViewById(R.id.title);
        this.description = (TextView)findViewById(R.id.description);
        this.thumbnail = (ImageView)findViewById(R.id.thumbnail);
        this.icon = (ImageView)findViewById(R.id.icon);
    }
}

In this case I don't need to manually inflate my content - it is already there. Specified via xml. So I don't need my init() anymore and all views initialisation stage goes to onFinishInflate() callback.

Now the question is how do add this custom view into my main layout. I need to to <include> it:

activity_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <include
        layout="@layout/card"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</FrameLayout>

My personal preference is to use merge + stylable approach since with <include> you loose control over the underlying layout for your view.

Some of you might ask why didn't I just <include> my card's layout into main activity layout instead of creating custom view and doing all of this. This works if you have UI parts only in your custom view. In my case I assume I will have some tricky logic (image downloading/db access, etc), I really need to go with custom view class.

Official Android docs
Layout optimization tricks by Romain Guy

comments powered by Disqus