當制作一款復雜的應用程序時,你會在不同的場合中經常重復使用一些組合控件。解決這個問題的辦法之一是創建一個囊括邏輯和布局的視圖,以便可以重復使用而不用在不同的場合中寫重復的代碼。這篇文章將會教你如何使用組合布局去創建簡單易用的自定義的布局。
1.介紹
在Android中,將多個控件打包在一起的視圖,被稱為組合視圖(或者組合控件)。本篇文章中,你將會創建一個控制滾動列表選擇的布局,會使用到Android SDK中的Spinner來獲取滾動列表的值。效果如下:

2.創建項目
我們創建一個SDK最低等級為4.0的工程。這個工程只包含一個空白的MainActivity,這個Activity僅僅做了初始化布局的工作:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
MainActivity對應的布局文件僅僅值包含了一個空的RelativeLayout。組合視圖將會在之后顯示出來。
3.創建一個組合控件
為了創建一個組合控件,你必須創建一個新的類來管理組合控件的視圖。對于兩側的spinner,需要兩個Button按鈕作為箭頭,以及需要一個TextView來顯示選擇的值。
首先,為組合控件的類創建一個sidespinner_view.xml布局文件,記住要使用<merge>標簽來包裹三個控件:
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<Button
android:id="@+id/sidespinner_view_previous"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/sidespinner_view_value"/>
<TextView
android:id="@+id/sidespinner_view_current_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp" />
<Button
android:id="@+id/sidespinner_view_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</merge>
然后,我們需要創建SideSpinner類來初始化布局,并且將箭頭圖標作為按鈕的背景圖片。到此為止,這個組合控件什么也做不了,因為我們還沒有展示任何東西。
public class SideSpinner extends LinearLayout {
private Button mPreviousButton;
private Button mNextButton;
public SideSpinner(Context context) {
super(context);
initializeViews(context);
}
public SideSpinner(Context context, AttributeSet attrs) {
super(context, attrs);
initializeViews(context);
}
public SideSpinner(Context context,
AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
initializeViews(context);
}
/**
* 填充布局
*
* @param context
* 布局所需要的Context
*/
private void initializeViews(Context context) {
LayoutInflater.from(context).inflate(R.layout.sidespinner_view, this);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//使用Android內置的圖片來作為前進和后退按鈕的背景圖片
mPreviousButton = (Button) this
.findViewById(R.id.sidespinner_view_previous);
mPreviousButton
.setBackgroundResource(android.R.drawable.ic_media_previous);
mNextButton = (Button)this
.findViewById(R.id.sidespinner_view_next);
mNextButton
.setBackgroundResource(android.R.drawable.ic_media_next);
}
}
你應該注意到了,這個組合視圖繼承了Linearlayout類。這意味著任何使用這個組合控件的視圖都能使用Linearlayout的屬性。這個組合控件看起來有點怪,因為它的根標簽為<merge>,而不是常見的<LinearLayout>或者<RelativeLayout>。
當你在MainActivity中添加組合控件時,這個組合控件的標簽將會扮演<LinearLayout>的角色。
4.將組合控件添加到布局文件中
現在編譯運行這個項目的話,你會發現能編譯通過,但是看不見任何東西。因為我們的組合視圖并不在MainActivity中。雙側滾動按鈕必須想其他控件一樣被添加到Activity,標簽的名字就是SideSpinner類的名字,當然要包含包名。
<com.cindypotvin.sidespinnerexample.SideSpinner
android:id="@+id/sidespinner_fruits"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"/>
由于我么好呢的SideSpinner類繼承自Linearlayout類,所以在<SideSpinner>標簽中就能使用Linearlayout的屬性。如果這時候運行項目,雙側滾動按鈕會顯示出來,但是沒有任何值出現。
5.向組合控件添加方法
如果我們想要真正的使用這個組合控件的話,還有幾件事情需要我們完成:向其中添加值,選擇值以及獲取值。
private CharSequence[] mSpinnerValues = null;
private int mSelectedIndex = -1;
public void setValues(CharSequence[] values) {
mSpinnerValues = values;
// 選擇字符串數組中的第一個值為默認值
setSelectedIndex(0);
}
public void setSelectedIndex(int index) {
if (mSpinnerValues == null || mSpinnerValues.length == 0)
return;
if (index < 0 || index >= mSpinnerValues.length)
return;
mSelectedIndex = index;
TextView currentValue;
currentValue = (TextView)this
.findViewById(R.id.sidespinner_view_current_value);
currentValue.setText(mSpinnerValues[index]);
// 如果顯示了第一個值,就隱藏向前按鈕
if (mSelectedIndex == 0)
mPreviousButton.setVisibility(INVISIBLE);
else
mPreviousButton.setVisibility(VISIBLE);
// 如果顯示了最后一個值,則隱藏最后一個按鈕
if (mSelectedIndex == mSpinnerValues.length - 1)
mNextButton.setVisibility(INVISIBLE);
else
mNextButton.setVisibility(VISIBLE);
}
public CharSequence getSelectedValue() {
if (mSpinnerValues == null || mSpinnerValues.length == 0)
return "";
if (mSelectedIndex < 0 || mSelectedIndex >= mSpinnerValues.length)
return "";
return mSpinnerValues[mSelectedIndex];
}
public int getSelectedIndex() {
return mSelectedIndex;
}
當布局里面所有的控件都被填充好,準備使用的時候,onFinishInflate方法會被調用。如果你需要修改組合控件的布局的話,就在這個方法里面修改。
因此,我們就可以在onFinishInflate方法里面,使用剛才創建的幾個方法,來控制Button按鈕的前進、后退的動作。
@Override
protected void onFinishInflate() {
// When the controls in the layout are doing being inflated, set
// the callbacks for the side arrows.
super.onFinishInflate();
// 當左箭頭按鈕被按下時,將選中的下標設置為前一個值
mPreviousButton = (Button) this
.findViewById(R.id.sidespinner_view_previous);
mPreviousButton
.setBackgroundResource(android.R.drawable.ic_media_previous);
mPreviousButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
if (mSelectedIndex > 0) {
int newSelectedIndex = mSelectedIndex - 1;
setSelectedIndex(newSelectedIndex);
}
}
});
// 當右箭頭按鈕被按下時,將選中的下標設置為后一個值
mNextButton = (Button)this
.findViewById(R.id.sidespinner_view_next);
mNextButton
.setBackgroundResource(android.R.drawable.ic_media_next);
mNextButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
if (mSpinnerValues != null
&& mSelectedIndex < mSpinnerValues.length - 1) {
int newSelectedIndex = mSelectedIndex + 1;
setSelectedIndex(newSelectedIndex);
}
}
});
// 設置第一個值為默認值
setSelectedIndex(0);
}
在我們新創建的setValues方法和setSelectedIndex方法中,我們可以從代碼中初始化雙側滾動按鈕和其他視圖一樣,你需要使用findViewById來找到組合視圖。然后就可以在返回的對象中調用組合視圖中的任何公共方法。
下面的代碼片段展示了如何在MainActivity方法中更新onCreate方法,這樣就能使用setValues方法。同時我們調用setSelectedIndex方法來將第二個值設置為默認值。
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化side spinner
SideSpinner fruitsSpinner;
fruitsSpinner = (SideSpinner)this
.findViewById(R.id.sidespinner_fruits);
CharSequence fruitList[] = { "Apple",
"Orange",
"Pear",
"Grapes" };
fruitsSpinner.setValues(fruitList);
fruitsSpinner.setSelectedIndex(1);
}
}
這時如果運行項目,你會發現雙側滾動按鈕顯示出來了,并且將Orange作為默認值顯示了出來。
6.向組合控件中添加布局屬性
在Android SDK里面可用的視圖都可以通過代碼來修改。但是一些屬性也可以直接在對應的視圖中設置。下面我們將會向雙側滾動按鈕這個組合控件添加一個屬性,來顯示按鈕的值。
首先我們要做的,就是在/res/values/attr.xml文件中定義屬性。每一個組合控件的的屬性都應當被<declare-styleable>標簽所囊括,對于我們這個組合控件,類名也應該顯示出來
<resources>
<declare-styleable name="SideSpinner">
<attr name="values" format="reference" />
</declare-styleable>
</resources>
在<attr>標簽中,name屬性值就是定義的新屬性的名字,而format屬性值就是新屬性值的類型。
對于顯示的值的集合,reference的意思是“指向在資源文件中定義好的字符串集合的引用”。通常來說,自定義屬性的類型有一下幾種:boolean,color,dimension,enum,integer,float以及stirng.
下面將會顯示如何創建values所指向的字符串。這些字符串必須添加在/res/values/strings.xml中就像下面的代碼:
<resources>
<string-array name="vegetable_array">
<item>Cucumber</item>
<item>Potato</item>
<item>Tomato</item>
<item>Onion</item>
<item>Squash</item>
</string-array>
</resources>
為了測試一下新的values屬性,在MainActivity布局文件中,創建一個新的組合控件。在使用這個新屬性之前,這個屬性必須要有一個前綴,而這個前綴就是在RelativeLayout中添加的一個命名空間,比如xmlns:sidespinner="http://schemas.android.com/apk/res-auto".
下面就是activity_main.xml的最終摸樣:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:sidespinner="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.cindypotvin.sidespinnerexample.SideSpinner
android:id="@+id/sidespinner_fruits"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"/>
<com.cindypotvin.sidespinnerexample.SideSpinner
android:id="@+id/sidespinner_vegetables"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_below="@id/sidespinner_fruits"
sidespinner:values="@array/vegetable_array" />
</RelativeLayout>
最后,SideSpinner類需要做一些修改,來讀取values屬性的值。每一個控件的值都可以在AttributeSet對象中獲取到,而這個AttributeSet對象則是在視圖構造方法中傳入的參數。
為了獲取自定義的values屬性的值。首先調用obtainStyledAttributes方法,這個方法需要傳入兩個參數,第一個是AttributeSet對象,第二個是Styleable的屬性引用。該方法返回的是一個TepedArray對象。
然后調用TypedArray對象的getter方法,來獲取正確的屬性值。傳入定義好的屬性名作為參數。下面的代碼段將向你展示如何修改組合控件的構造方法來獲取控件值的集合,并且將他們設置到組合控件中:
public SideSpinner(Context context) {
super(context);
initializeViews(context);
}
public SideSpinner(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray;
typedArray = context
.obtainStyledAttributes(attrs, R.styleable.SideSpinner);
mSpinnerValues = typedArray
.getTextArray(R.styleable.SideSpinner_values);
typedArray.recycle();
initializeViews(context);
}
public SideSpinner(Context context,
AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
TypedArray typedArray;
typedArray = context
.obtainStyledAttributes(attrs, R.styleable.SideSpinner);
mSpinnerValues = typedArray
.getTextArray(R.styleable.SideSpinner_values);
typedArray.recycle();
initializeViews(context);
}
如果你啟動應用,你會看到兩側的按鈕將會如我們想象中的那樣工作。
7.狀態保存
我們需要完成的最后一步就是保存和恢復控件狀態。當一個Activity被銷毀重建時(比如設備旋轉的時候),控件顯示的值應當自動保存和恢復,下面的代碼將實現這一功能。
private static String STATE_SELECTED_INDEX = "SelectedIndex";
private static String STATE_SUPER_CLASS = "SuperClass";
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable(STATE_SUPER_CLASS,
super.onSaveInstanceState());
bundle.putInt(STATE_SELECTED_INDEX, mSelectedIndex);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle)state;
super.onRestoreInstanceState(bundle
.getParcelable(STATE_SUPER_CLASS));
setSelectedIndex(bundle.getInt(STATE_SELECTED_INDEX));
}
else
super.onRestoreInstanceState(state);
}
8.總結
雙側滾動按鈕現在是完成了。兩側的按鈕都能正常工作。兩側的值也能自動恢復和創建。現在你就可以在Android應用中使用組合控件了。