Android 自定义UI组合控件设计方法
1,概述
在设计Android程序的时候,为了提高编程效率和维持统一的风格,往往需要把一些UI组件组合在一起,包装成一个独立的组件单元,在使用中作为一个整体,象使用系统控件一样地使用。这样的组件单元,我们称之为自定义UI组合控件。
Android 自定义UI组合控件在程序设计中具有重要意义,它可以简化程序设计难度、提高代码复用性、降低代码耦合度、提高程序模块化、降低程序的维护的难度,随着软件项目日趋庞大和功能日趋复杂,自定义控件的作用越来越突出。
Android开发环境ADT为我们提供了象按钮文本框等等的一些常用组件,通过研究源码,我们发现Android是这样实现组件定义的:
第一步:在atts.xml 中定义控件属性
第二步:定义一个View的子类,实现属性参数的获取、内部子控件的定义及响应事件的定义。
自定义组件示意图:
|
|
|
|
|
根据这样的思路,我们也可以用同样的方法定义自己的控件,本文依照系统的设计思路,来讲解自定义组合控件的设计方法。
2,控件设计
2.1 设计控件属性
控件属性存放在atts.xml文件中。atts.xml中以declare-styleable为单元定义了一系列属性集合, 它位于res/values目录下.定义的内容格式为:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name = "属性集合名">
<attr name="属性名" format = "属性类型"/>
……
</declare-styleable>
</resources>
其中declare-styleable定义的是属性集合,attr标签定义的是具体属性名称和类型,这些属性就是用户在使用控件时需要设置的属性。
2.2 定义自己的View,实现想要的控件
自定义View的基本思路是这样的:这个类继承自RelativeLayout。对应atts.xml的属性,类中要定义对应的field,同时定义控件中包含的子控件,比如Button,TextView等,另外还需要定义控件的布局属性LayoutParams, 确定子控件的显示位置。
定义这个类的重点是定义它的构造函数,在这里,必须重写带AttributeSet属性的构造函数,以便获取属性。构造函数要实现的功能有:用TypeArray获取控件属性值; 实例化各子控件,给各控件的属性赋值。
自定义View的设计如下所示:
package com.example.mypanel; //自定义包名
public class MyPanel extends RelativeLayout { //MyPanel为自定义View名
//自定义属性,与atts.xml一致
private String titleString;
//子控件, 即自定义控件中包含控件,比如按钮、文字、图片等。
private Button backBtn, exitBtn, forwardBtn;
//子控件布局属性,定义子控件在自定义控件中的位置
private LayoutParams backBtnParams, exitBtnParams, titleParams;
//构造函数
public MyPanel(Context context, AttributeSet attrs) {
super(context, attrs);
//用TypeArray获取控件属性值
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.myPanel);
//从TypedArray中读取
titleString = ta.getString(R.styleable.myPanel_title);
//回收TypedArray
ta.recycle();
// 把属性赋给具体组件
title = new TextView(context);
title.setText( titleString );
title.setTextSize(titleSize );
backBtn = new Button(context );
backBtn.setText( backBtnText );
exitBtn = new Button( context );
exitBtn.setText( exitBtnText );
//定义布局属性,指定子控件的显示位置
backBtnParams = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT ) ;
backBtnParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT );
addView( backBtn, backBtnParams );
exitBtnParams = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT ) ;
exitBtnParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT );
addView( exitBtn, exitBtnParams );
titleParams = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT ) ;
titleParams.addRule(RelativeLayout.CENTER_IN_PARENT );
addView( title, titleParams );
}
}
至此,我们自定义的控件已经具备了完整的外观形态。
2.3 在程序中调用自定义控件
应用自定义控件需要注意三点:
其一,在XML开头处引用自定义控件的包名来定义控件命名空间,在Eclipse下格式为:
xmlns:别名=http://schemas.android.com/apk/res/完整包名
在Android Studio下的格式为:
xmlns:别名=http://schemas.android.com/apk/res/res-auto
其二, 在引用自定义控件时提供完整类的路径
比如:<com.example.mypanel.MyPanel …… />
其三,自定义属性要用别名引用
比如:custom:title = "Hello Title"
custom:titleSize = "15dp"
下面来看具体实现:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res/com.example.mypanel"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<com.example.mypanel.MyPanel
android:id="@+id/panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#DFDFDF"
android:paddingTop="2dp"
custom:title = "Hello Title"
custom:titleSize = "15dp"
custom:titleColor = "#FF0E00"
custom:backBtnText = "BACK"
custom:exitBtnText = "EXIT"
/>
</RelativeLayout>
可见,引用自定义控件与引用系统控件方法基本类似。此时,可以运行程序查看显示效果了,如下图所示:
2.4 加入响应事件
如果在MyPanel中为按钮加入OnClick响应事件,即可直接实现控件对点击事件的响应,可是,作为一个控件,如果每次调用都更改定义控件的源程序,这违背了定义一个通用控件的初衷。控件的属性及事件的定义都应该由调用者实现。要实现这一点,需要用到接口回调机制。也就是在控件的定义中,按钮事件不直接实现具体行为,而是调用一个接口中的抽象方法,这些对调用者来说都是不可见的,调用者只要实现了该接口的抽象方法,具体的事件行为也就实现了。
在MyPanel.java中,定义了一个接口,包含两个抽象方法:
public interface MyPanelOnClickListener{
public void backBtnClick();
public void exitBtnClick();
}
private MyPanelOnClickListener listener; //该接口类型的MyPanel成员属性。
按钮的OnClick方法只调用抽象类中的方法:
public void onClick(View arg0) {
// TODO Auto-generated method stub
listener.backBtnClick();
}
然后定义事件的回调方法:
public void setMyPanelOnClickListener( MyPanelOnClickListener listener){
this.listener = listener;
}
它的参数是MyPanelOnClickListener类型,在调用者调用这个方法时,必须实例化这个类。也就是必须实例化接口的两个方法backBtnClick()与public void exitBtnClick(),这个例化的过程就是实现事件具体行为的过程。
下面给出加入响应事件后的MyPanel
public class MyPanel extends RelativeLayout {
private Button backBtn, exitBtn;
private TextView title;
private String titleString;
private int titleSize;
private int titleTextColor;
private String backBtnText;
private String exitBtnText;
private LayoutParams backBtnParams, exitBtnParams, titleParams;
//回调接口
public interface MyPanelOnClickListener{
public void backBtnClick();
public void exitBtnClick();
}
private MyPanelOnClickListener listener;
//回调事件
public void setMyPanelOnClickListener( MyPanelOnClickListener listener){
this.listener = listener;
}
public MyPanel(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.myPanel);
titleString = ta.getString(R.styleable.myPanel_title);
titleSize = ta.getDimensionPixelSize(R.styleable.myPanel_titleSize,10);
titleTextColor = ta.getColor(R.styleable.myPanel_titleColor, Color.BLUE);
backBtnText = ta.getString(R.styleable.myPanel_backBtnText);
exitBtnText = ta.getString(R.styleable.myPanel_exitBtnText);
ta.recycle();
// 把属性赋给具体组件
title = new TextView(context);
title.setText( titleString );
title.setTextSize(titleSize );
backBtn = new Button(context );
backBtn.setText( backBtnText );
exitBtn = new Button( context );
exitBtn.setText( exitBtnText );
//定义布局属性
backBtnParams = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT ) ;
backBtnParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT );
addView( backBtn, backBtnParams );
exitBtnParams = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT ) ;
exitBtnParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT );
addView( exitBtn, exitBtnParams );
titleParams = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT ) ;
titleParams.addRule(RelativeLayout.CENTER_IN_PARENT );
addView( title, titleParams );
//按钮事件
backBtn.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
listener.backBtnClick();
}
});
exitBtn.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
listener.exitBtnClick();
}
});
}
}
2.5 自定义控件在程序中的应用
调用者的OnCreate方法如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//实例化一个MyPanel
MyPanel panel = (MyPanel)this.findViewById(R.id.panel);
//为MyPanel设置点击事件
panel.setMyPanelOnClickListener(new MyPanel.MyPanelOnClickListener(){
@Override
public void backBtnClick() {
// TODO Auto-generated method stub
Toast.makeText(MainActivity.this, "BACK BUTTON", 1000).show();
}
@Override
public void exitBtnClick() {
// TODO Auto-generated method stub
Toast.makeText(MainActivity.this, "EXIT BUTTON", 1000).show();
finish();
}
});
}
在这里,首先实例化了一个MyPanel对象,其次,调用setMyPanelOnClickListener方法来设置点击事件,MyPanel.MyPanelOnClickListener的一个匿名内部类对象做为参数传递给setMyPanelOnClickListener方法。可见,调用MyPanel与调用其它系统控件的方法是一样的,这正是我们设计自定义控件要实现的目标。
2.6 为自定义控件设置其它方法
实际应用中的控件,应具备完整的功能。当然,也可以为我们自己设计的控件加上需要的功能。
比如,可以在MyPanel中加入一个隐藏按钮的方法
public void hideExitBtn(){
exitBtn. .setVisibility(View.INVISIBLE);
}
2.7程序间共享自定义控件
在一个应用里设计的自定义控件,有时在别的应用里也可以用。这时,我们可以把自定义控件设计成一个单独的项目,在新建项目时,选择“将项目作为library”