在最近的一个客户应用中,我遇到了一个需求,根据选定的值来生成指定数量的编辑框字段,这样用户可以输入人物信息。最初我的想法是把这些逻辑放到Fragment中,只是根据选中值的变化来向线性布局容器中增加编辑框数量,但是那样做会使Fragment过度膨胀,而且没有太多可重用性方面的考虑。
这是一个绝好的机会,可以将这些交互功能封装到自定义视图中。自定义视图可以在整个应用范围重用(目前为止至少有2个地方),并且让测试封装功能变得我更加轻松。
什么是自定义视图
Android框架提供了很多的视图和布局,但有些情况下开发者需要创建自己的视图。有时候是扩展内置的类来增加功能,就像在文本框中支持自定义字体和字符间距。其他情况下,因为内置的视图不能提供所需的功能,就像径向刻度盘。
现在讨论的是自定义复合视图。视图由多个其他的视图组成,内置的或自定义的都可以,用来封装复杂的交互和功能。
在一个成熟且完整的Fragment完全满足我需求的情况下,我使用了复合视图,因为我想要一个可重用、可测试的组件。上面的例子很好的说明了这种情况。由于代码属于一个客户项目,所以我在这里创建了一个简单的项目展示如何创建和使用自定义视图。
自定义视图
在本例中,我们希望自定义视图添加编辑框,这样用户就可以输入任意数量的数据条目。在自定义视图中,可以通过使用一个包含了适当数量编辑框的简单容器视图(线性布局)实现,因而可以很容易地获取名称列表。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
; html-script: false ] /** * A custom compound view that displays an arbitrary * number of text views to enter your friends names. */ public class FriendNameView extends LinearLayout { private int mFriendCount; private int mEditTextResId; public FriendNameView(Context context) { this(context, null); } public FriendNameView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FriendNameView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setOrientation(VERTICAL); } public int getFriendCount() { return mFriendCount; } public void setFriendCount(int friendCount) { if (friendCount != mFriendCount) { mFriendCount = friendCount; removeAllViews(); for (int i = 0; i < mFriendCount; i++) { addView(createEditText()); } } } private View createEditText() { View v; if (mEditTextResId > 0) { LayoutInflater inflater = LayoutInflater.from(getContext()); v = inflater.inflate(mEditTextResId, this, true); } else { EditText et = new EditText(getContext()); et.setHint(R.string.friend_name); v = et; } return v; } public int getEditTextResId() { return mEditTextResId; } public void setEditTextResId(int editTextResId) { mEditTextResId = editTextResId; } /** * Returns a list of entered friend names. */ public List<String> getFriendNames() { List<String> names = new ArrayList<>(); for (int i = 0; i < getChildCount(); i++) { View v = getChildAt(i); if (v instanceof EditText) { EditText et = (EditText) v; names.add(et.getText().toString()); } } return names; } } |
当用户使用 setFriendCount(int) 方法设置朋友的数量时,我们重置基于输入的子编辑框字段数目。这里使用一个自定义布局的完成,但是将默认为一个简单的编辑框。
当我们要获取用户已经输入的名称列表时,可以不必关心自定义视图的内部结构。只需调用 getFriendNames()方法,视图就会汇集名称列表。
这就是这个简单的例子。作为一种特殊的副作用,因为这个视图只是由一个线性布局(父类)和编辑框视图组成,它们已经知道如何保存和恢复状态,所以无需对状态进行处理。
布局中包含自定义视图
当想要在Activity或Fragment布局中使用自定义视图时,可以像使用其它的视图一样,加入一些简单的XML。
1 2 3 4 5 |
; html-script: false ] <com.ryanharter.android.compoundviews.app.views.FriendNameView android:id="@+id/friend_names" android:layout_width="match_parent" android:layout_height="wrap_content"/> |
和其它的视图一样,可以使用 findViewById(int)方法来得到它。
1 2 |
; html-script: false ] mFriendNameView = (FriendNameView) findViewById(R.id.friend_names); |
在我们的MainActivity中,当数据拾取器的值变化时,我们可以非常容易地设置朋友的数量。当想要获取名称列表时,调用getFriendNames()方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
; html-script: false ] mFriendCountPicker.setOnValueChangedListener(new OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { mFriendNameView.setFriendCount(newVal); } }); mCountFriendsButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { List<String> names = mFriendNameView.getFriendNames(); Intent i = new Intent(MainActivity.this, FriendCountActivity.class); i.putStringArrayListExtra("names", new ArrayList<String>(names)); startActivity(i); } }); |
尽管这是一个刻意设计的例子,但是自定义复合视图是一个极好功能封装方式。如果不进行封装,功能代码将散落在整个活动和片段中。自定义复合视图提供了可测试、可重用的代码,让应用程序更稳定。我鼓励大家想一想,自己的应用程序中哪里可以使用自定义复合视图。如果可以的话并与其他开发者分享,它们是非常有用的。
本文的示例项目可以在Github上获取。