如何更好的通过Inflate layout的方式来实现自定义view

英文原文 http://trickyandroid.com/protip-inflating-layout-for-your-custom-view/ 

今天要讲的是在通过组合的方式实现自定义view(custom compound view)的时候容易遇到的一些问题。

custom compound view:一种通过组合原有安卓控件或者布局而实现的自定义view的方法,与常规的自定义view方法相比,一般来说不需要实现onDraw方法,选择这种方式的场景一般是 有一种布局经常被使用,并且这个布局里面有的元素有一些逻辑需要处理,我们希望将他们封装起来使用,其实这种方式的重点不在于view的自定义这个概念,而是在于封装。-译者注

我们选取一个自定义的组合视图作为例子,尝试了解是如何创建的。

就如你所看到的,我们这里有一个相当典型的view - 一个简单的卡片似的控件。因为在这个卡片中我们有一些逻辑要处理,我决定创建一个自定义的view。这是网上经常看到的一种方法:继承自一个现有的布局控件然后在初始化期间inflate一个自定义的布局:

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>

好了,当我们想要使用刚刚新建的自定义view的时候,我们只须将这个view像一般的控件那样添加到主布局中:

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>

看起来很简单,但是别慌,让我们看看view的层次结构:

如你所见,在包含卡片内容本身的RelativeLayout之外还有一层RelativeLayout(打问号的那个)。这是因为我们的Card类就是一个RelativeLayout(继承自RelativeLayout),当我们再inflate一个内容的时候,我们只是将内容添加到了这个RelativeLayout中。

当然,假如我们不对父RelativeLayout做任何事情,这并不是什么大问题。但是当我们的布局更复杂或者自定义view的数量增大的时候,你就会注意到性能方面的问题了。这是因为UI引擎要遍历,测量,摆放所有这些布局非常吃力。

简单说来就是 - 布局越深,越难遍历。因此,尽量使布局扁平化。

让我们看看该如何让布局更扁平化:

Merge

在本例中,减少布局数量的一种可取的方法就是让卡片本身的内容直接依附在父view中(即Card类)

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>

下面是我们所得到的效果:

好极了!我们消除了冗余的RelativeLayout,但是也让最顶层原本有的那些属性也没了 - 白色背景和padding。这是因为标签可以融合其内容,但是不包括自身,因此顶层的属性都丢失了。

有三种办法可以将它们添加回来:

1)在代码中添加:

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) 在主布局中添加card的时候添加:

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)定义一个stylable** 属性将这些值通过style提供给控件。感谢 @vovkab 为我指出该方法** :

我不喜欢这种方法,因为代码相互依赖的地方变多了,这个类复杂点还好说,如果类本身很简单,又有如此多的关联,不爽  - 译者注。

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();
    }
    ..........

注意在view的构造函数中指定的是我们的stylable。   

Include

另一种减少布局数量的方法是使用标签。因为Card.java是一个RelativeLayout,我们可以让他作为内容的跟布局,然后使用include将它包含在main activity的布局中:

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

这种情况下,我们不需要主动inflate内容 - 因为它本身就在那里(activity的setContentView会做这个事情)。因此不再需呀init()了,所有view的初始化都在onFinishInflate()回调方法中了。

现在的问题是如何将这个自定义view添加到主布局中。使用:

  其实我觉得这段应该和上面的那段顺序颠倒下- 译者注。

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>

我个人偏爱的是使用merge + stylable的方法,因为让你失去了对view的控制权。

有些人问我为什么不直接使用,而使用自定义view,这是因为这个view中我还有其他的逻辑。ps 做过的都知道咋回事了,我遇到的一种场景就是,不想使用Fragment,但是又要像Fragment那样是一个整体 -  译者注。

参考链接:

Official Android docs
Layout optimization tricks by Romain Guy