当制作一款复杂的应用程序时,你会在不同的场合中经常重复使用一些组合控件。解决这个问题的办法之一是创建一个囊括逻辑和布局的视图,以便可以重复使用而不用在不同的场合中写重复的代码。这篇文章将会教你如何使用组合布局去创建简单易用的自定义的布局。
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应用中使用组合控件了。