Android 自定义视图总结

Android 自定义视图总结


[TOC]

很多在开发的过程中,经常会需要把某个UI视图给单独抽取出来,以便重复使用,下面举个简单例子,分析一下。

比如我们这边有个这样的视图,如下所示,显示一个订单模块中,经常显示一个商品的信息、数量以及价格。

上面的显示商品的实体是这样的。

public class GoodsItem implements Serializable {

    public String name;

    public int count;

    public double price;

    @Override
    public String toString() {
        return "GoodsItem{" +
                "name='" + name + '\'' +
                ", count=" + count +
                ", price=" + price +
                '}';
    }
}


GoodsItem goodsItem = new GoodsItem();
goodsItem.name = "可口可乐";
goodsItem.count = 123;
goodsItem.price = 321;

正常情况

正常情况下,我们如果只需要用一次,那么我们定义好布局就好了,然后简单的赋值就好,下面是代码。

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:gravity="center"
    android:orientation="horizontal"
    android:padding="10dp"
    tools:showIn="@layout/activity_main">

    <TextView
        android:id="@+id/tvName"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        tools:text="可口可乐" />

    <TextView
        android:id="@+id/tvCount"
        android:layout_width="80dp"
        android:layout_height="wrap_content"
        android:gravity="right"
        tools:text="x1" />

    <TextView
        android:id="@+id/tvPrice"
        android:layout_width="80dp"
        android:layout_height="wrap_content"
        android:gravity="right"
        tools:text="¥100" />
</LinearLayout>
// normal
TextView tvName = (TextView) findViewById(R.id.tvName);
TextView tvCount = (TextView) findViewById(R.id.tvCount);
TextView tvPrice = (TextView) findViewById(R.id.tvPrice);
tvName.setText(goodsItem.name);
tvCount.setText(String.format("x%s", goodsItem.count));
tvPrice.setText(String.format("¥%s", goodsItem.price));

上面是最简单的方式,也是初学Android的时候常用的方式。

Databinding

Google官方给出了一个Databinding的方式,这样我们代码了里面就可以少些很多代码,在对上面的代码进行少许优化后,就可以使用Databinding的方式。

<?xml version="1.0" encoding="UTF-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:gravity="center"
        android:orientation="horizontal"
        android:padding="10dp"
        tools:showIn="@layout/activity_main">

        <TextView
            android:id="@+id/tvName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            tools:text="可口可乐" />

        <TextView
            android:id="@+id/tvCount"
            android:layout_width="80dp"
            android:layout_height="wrap_content"
            android:gravity="right"
            tools:text="x1" />

        <TextView
            android:id="@+id/tvPrice"
            android:layout_width="80dp"
            android:layout_height="wrap_content"
            android:gravity="right"
            tools:text="¥100" />
    </LinearLayout>
</layout>
// databind
binding.includeDatabinding.tvName.setText(goodsItem.name);
binding.includeDatabinding.tvCount.setText(String.format("x%s", goodsItem.count));
binding.includeDatabinding.tvPrice.setText(String.format("¥%s", goodsItem.price));

使用Databinding的最好好处,就不需要写烦人的findViewById了。

Databinding升级

如果使用Databinding绑定的形式,那么赋值的方式就更加容易了,在定义xml的时候定义传递一个GoodsItem对象,然后在界面赋值一个对象就好了,简单明了。

<?xml version="1.0" encoding="UTF-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="good"
            type="cn.mycommons.goodsdemo.GoodsItem" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:gravity="center"
        android:orientation="horizontal"
        android:padding="10dp"
        tools:showIn="@layout/activity_main">

        <TextView
            android:id="@+id/tvName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@{good.name}"
            tools:text="可口可乐" />

        <TextView
            android:id="@+id/tvCount"
            android:layout_width="80dp"
            android:layout_height="wrap_content"
            android:gravity="right"
            android:text='@{"x"+good.count}'
            tools:text="x1" />

        <TextView
            android:id="@+id/tvPrice"
            android:layout_width="80dp"
            android:layout_height="wrap_content"
            android:gravity="right"
            android:text='@{"¥"+good.price}'
            tools:text="¥100" />
    </LinearLayout>
</layout>
// databind with param
binding.includeDatabindingWithParam.setGood(goodsItem);

自定义View

刚刚的示例比较,可以使用简单复制就可以搞定,有时候,业务比较复制,可能还要处理手势事件,如果使用Databinding就不怎么方便了。
所以自定义View是我们另外的一种方式。

首先我们顶一个自定义的View,这个view是专门用来显示商品信息的。

public class GoodsItemView extends FrameLayout {

    private TextView tvName, tvCount, tvPrice;

    public GoodsItemView(Context context) {
        super(context);

        init();
    }

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

        init();
    }

    void init() {
        LayoutInflater.from(getContext()).inflate(R.layout.databinding, this);

        tvName = (TextView) findViewById(R.id.tvName);
        tvCount = (TextView) findViewById(R.id.tvCount);
        tvPrice = (TextView) findViewById(R.id.tvPrice);
    }

    public void updateUI(GoodsItem goodsItem) {
        tvName.setText(goodsItem.name);
        tvCount.setText(String.format("x%s", goodsItem.count));
        tvPrice.setText(String.format("¥%s", goodsItem.price));
    }
}

然在布局中引入就可以了,最好在Activity中找到所对应的对象,最后赋值就可以了。

<cn.mycommons.goodsdemo.GoodsItemView
    android:id="@+id/goodsItemView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />
    
// custom view
GoodsItemView goodsItemView = (GoodsItemView) findViewById(R.id.goodsItemView);
goodsItemView.updateUI(goodsItem);

自定义Module

有时候一个自定义View会有所限制,比如,自定义View不能代码混淆,而且自定义View会受到分类的限制。
有时候仅仅只是一处使用,单独提取一个View代价太大,那么我们可以单独自定义一个Module的方式,这样做的话,可以减少Activity的代码量。
话不多说,先看代码。

首先商品展示界面还在定义在Activity的布局中。

<include
    android:id="@+id/includeModule"
    layout="@layout/databinding" />

然后我们定义一个通用的Module接口,以及实现类。

public interface IModule {

    void create();
    void destroy();
}

public class GoodsItemModule implements IModule {
    @NonNull
    private final View rootView;
    private TextView tvName, tvCount, tvPrice;

    public GoodsItemModule(@NonNull View rootView) {
        this.rootView = rootView;
    }

    @Override
    public void create() {
        tvName = (TextView) rootView.findViewById(R.id.tvName);
        tvCount = (TextView) rootView.findViewById(R.id.tvCount);
        tvPrice = (TextView) rootView.findViewById(R.id.tvPrice);
    }

    public void updateUI(GoodsItem goodsItem) {
        tvName.setText(goodsItem.name);
        tvCount.setText(String.format("x%s", goodsItem.count));
        tvPrice.setText(String.format("¥%s", goodsItem.price));
    }

    @Override
    public void destroy() {
        tvName = null;
        tvCount = null;
        tvPrice = null;
    }
}

然后我们在Activity中,把商品展示的接的根视图传入到Module中,这样对商品展示的所有逻辑都是写在了GoodsItemModule中了,
这样就减少了耦合,Activity的代码量也比较少,同时 Module中定义了简单的生命周期,可以在Activity中调用,方便管理。

// module
goodsItemModule = new GoodsItemModule(findViewById(R.id.includeModule));
goodsItemModule.create();
goodsItemModule.updateUI(goodsItem);

// 这一段可以在Activity.onDestroy中执行
goodsItemModule.destroy();

Fragment

看到上面了方法,是不是想到了Fragment,其实Fragment也是Google提供的自定义Module的一种实现。Fragment提供更加完善的生命周期,不过也是一个值得吐槽的地方。
导致很多实用Fragment的时候不知道怎么用,以及实用过于复杂。

下面展示下Fragment的使用。

public class GoodsItemFragment extends Fragment {

    static final String EXTRA_GOODS_ITEM = "goods_item";

    public static GoodsItemFragment newFragment(GoodsItem goodsItem) {
        GoodsItemFragment fragment = new GoodsItemFragment();
        Bundle args = new Bundle();
        args.putSerializable(EXTRA_GOODS_ITEM, goodsItem);
        fragment.setArguments(args);
        return fragment;
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.databinding, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

       TextView tvName = (TextView) view.findViewById(R.id.tvName);
       TextView tvCount = (TextView) view.findViewById(R.id.tvCount);
       TextView tvPrice = (TextView) view.findViewById(R.id.tvPrice);

        GoodsItem goodsItem = (GoodsItem) getArguments().getSerializable(EXTRA_GOODS_ITEM);
        if (goodsItem != null) {
            tvName.setText(goodsItem.name);
            tvCount.setText(String.format("x%s", goodsItem.count));
            tvPrice.setText(String.format("¥%s", goodsItem.price));
        }
    }
}

以及Activity中的调用方式。

// fragment
getSupportFragmentManager()
        .beginTransaction()
        .add(binding.fragmentContainer.getId(), GoodsItemFragment.newFragment(goodsItem))
        .commit();

总结

上面展示了Android中自定义视图模块的常用方式,下面进行比较和总结:

  • 正常情况 : 简单明了,所有的开发人员都会,只不过代码比较冗余,不利于解耦。
  • Databinding : 简单明了,减少繁琐的findViewById代码,而且方便。
  • Databinding升级 : 更加简单明了,不过有少许的学习成本,不过现在已经是Android程序员的基本技能了。
  • 自定义View : 代码稍微复杂,开发繁琐,同时自定义View不能进行代码混淆,可以根据View的名字,猜想逻辑。
  • 自定义Module : 这种方式,简单粗暴,学习成本比较低,小项目内可以自由使用,不过不利于推广。
  • Fragment : 高级的自定义Module方式,有学习成本,利于推广和维护。

上面的自定义View和自定义Module是两种程序的实习方式,有句话叫做组合由于继承,所以有时候,我会选择后者。

个人的喜爱程度是这样,当然有时候会根据项目的实际情况进行选择,也有时候会进行组合使用。

Databinding升级 = Databinding > Fragment > 自定义Module > 正常情况 > 自定义View

以上就是作者对Android自定义视图总结,欢迎前来讨论。

示例代码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容