在最近的一個客戶應用中,我遇到了一個需求,根據選定的值來生成指定數量的編輯框欄位,這樣使用者可以輸入人物資訊。最初我的想法是把這些邏輯放到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上獲取。