Android 自定义视图总结


Android 自定义视图总结


很多在开发的过程中,经常会需要把某个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自定义视图总结,欢迎前来讨论。

示例代码地址


文章作者: 流水不腐小夏
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 流水不腐小夏 !
 上一篇
CheckStyle出现Unable to create a Checker configLocation问题 CheckStyle出现Unable to create a Checker configLocation问题
CheckStyle出现Unable to create a Checker: configLocation问题 最近使用了Android Studio 3.0版本,同时Gradle版本由3.3升级到了3.5版本。 突然发现原先项目中静态代
2017-03-29
下一篇 
AndroidStudio插件:布局文件转化Databinding AndroidStudio插件:布局文件转化Databinding
AndroidStudio插件:布局文件转化Databinding 序这几天一直在做代码重构,因为Android是提供Databinding的MVVM架构,所以就想把代码里面所有的Butterknife代码换成Databinding形式。
2017-02-25
  目录