转载请注明出处:http://blog.csdn.net/blog_wang/article/details/38468547
相信很多使用过Afinal和Xutils的朋友会发现框架中自带View控件注解及
事件绑定功能,我们无需使用findViewById和setOnClickListener即可完成view初始化和监听事件,使用注解在很大程度上使
我们的代码看起来更加简洁,让我们的代码看起来不是那么冗余,那我们今天就来一探究竟,看看其中原理是如何来实现的。
Java注解相当于一种标记,标记可以加在包,类,字段,方法,方法的参数以及局部变量上。在程序中加入注解可以起到说明和配置的功能,Javac编译器,开发工具和其他程序可以用反射来了解你的类及各种元素上有无何种标记,根据你的标记,去做相应的事。
自定义注解
系统定义了许多注解类,但是都不满足我们的需求,那么我们想要实现注解的功能,就需要自定义注解类型,我们首先来了解自定义注解所需要知道的东西。
元注解是指注解的注解。包括 @Retention @Target @Document @Inherited四种
@Target(ElementType.TYPE) //接口、类、枚举、注解
//字段、枚举的常量
//用于描述参数
//用于描述构造器
//用于描述包
Target表示自定义的注解类型在什么地方使用,可以看出一个Target能使用多个ElementType,也就是说我们自定义的一个注解类型可以在字段、接口、包、方法上等多个地方使用,如:@Target({ElementType.FIELD,ElementType.METHOD}) 因此这个注解可以是字段注解,也可以是方法的注解
@Retention
//在运行时有效
Retention表示注解在什么范围内有效
@Document
Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员
@Inherited
Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
注解参数的可支持数据类型:
1.所有基本数据类型(int,float,boolean,byte,double,char,long,short)
2.String类型
3.Class类型
4.enum类型
5.Annotation类型
6.以上所有类型的数组
Annotation类型里面的参数该设定:
1.只能用public或默认(default)这两个访问权修饰.例如,String value();这里把方法设为defaul默认类型
2.参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和
String,Enum,Class,annotations等数据类型,以及这一些类型的数组.例如,String
value();这里的参数成员就为String
3.如果只有一个参数成员,最好把参数名称设为"value",后加小括号
上面我们详细介绍了自定义注解类所能注解的类型、范围、注解参数的数据类型,以及注解在什么时期有效。OK,了解了那么多,那我们就来自己自定义一个注解类
- @Target(ElementType.FIELD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface InjectView{
- /**
- * 默认控件ID
- */
- public static int DEFAULT_ID = -1;
- /**
- * 默认方法
- */
- public static String DEFAULT_METHOD = "";
- /**
- * 功能:接收控件ID
- * @return 返回设置ID
- */
- public int id() default DEFAULT_ID;
- /**
- * 功能:接收控件点击方法名
- * @return 返回设置方法名
- */
- public String click() default DEFAULT_METHOD;
- }
根据@Target和@Retention我们可以看出这个注解类用于描述字段、枚举,在运行时有效,跟在方法后面的default字段表示当没有使用该参数注解时,调用该方法会返回的默认值,那一个定义好的注解我们要如何使用呢?
- public class MainActivity extends FrameActivity
- {
- /**
- * 指定控件对应ID和点击事件需要调用的方法名
- *
- * 这里的ID和click是自定义注解中属性名,多个参数用逗号分开
- *
- */
- @InjectView(id = R.id.button,click = "click")
- private Button button;
- @Override
- protected void onCreate(Bundle savedInstanceState)
- {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- //当走到这步时,button已经实例化了,并且可以相应点击事件,但是怎么做到的呢?
- button.setText("我是通过注解来实例化的");
- }
- public void click(View v){
- switch (v.getId()) {
- case R.id.button:
- Toast.makeText(this,"我被点击了",Toast.LENGTH_SHORT).show();
- break;
- default:
- break;
- }
- }
- }
如果大家眼睛比较亮的话,相信大家已经看出我不是继承至Activity,对,这里初始化的工作全部放在FrameActivity类里面,方便其他Activity使用,我们来看看FrameActivity里面都做了什么操作
- public class FrameActivity extends Activity{
- @Override
- public void setContentView(int layoutResID){
- super.setContentView(layoutResID);
- //在子类执行完setContentView方法之后调用
- traversalsField();
- }
- /**
- * 遍历类变量,获取变量注解
- */
- private void traversalsField(){
- //获取类所有属性,包括public,private,protected
- Field[] fields = getClass().getDeclaredFields();
- if(null != fields && fields.length > 0){
- for(Field field : fields){
- //判断属性注解是否属于自定义注解接口
- if(field.isAnnotationPresent(InjectView.class)){
- //获取变量注解类型
- InjectView injectView = field.getAnnotation(InjectView.class);
- //得到设置的ID
- int id = injectView.id();
- //如果获取的ID不等于默认ID,则通过findViewById来查找出对象然后设置变量值
- if(id != InjectView.DEFAULT_ID){
- try{
- //类中的成员变量为private,故必须进行此操作
- field.setAccessible(true);
- field.set(this,findViewById(id));
- }catch (IllegalArgumentException e){
- e.printStackTrace();
- }catch (IllegalAccessException e){
- e.printStackTrace();
- }
- }
- //得到设置方法名
- String method = injectView.click();
- if(!method.equals(InjectView.DEFAULT_METHOD)){
- setViewClickListener(this,field,method);
- }
- }
- }
- }
- }
- /**
- * 给View设置点击事件
- * @param injectedSource 类对象
- * @param field 属性
- * @param clickMethod 方法名
- */
- private void setViewClickListener(Object injectedSource,Field field,String clickMethod){
- try {
- //将属性转成Object类型
- Object obj = field.get(injectedSource);
- //判断Object类型是否是view的实例,如果是强转成view并设置点击事件
- if(obj instanceof View){
- ((View)obj).setOnClickListener(new EventListener(injectedSource).click(clickMethod));
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- class EventListener implements OnClickListener{
- /**
- * 类对象
- */
- public Object obj;
- /**
- * 方法名
- */
- public String clickMethod;
- public EventListener(Object obj){
- this.obj = obj;
- }
- /**
- * click返回的是实现了OnClickListener接口的实例
- */
- public EventListener click(String clickMethod){
- this.clickMethod = clickMethod;
- return this;
- }
- //当view点击时会调用onClick方法
- @Override
- public void onClick(View v) {
- invokeClickMethod(obj, clickMethod, v);
- }
- private Object invokeClickMethod(Object obj, String methodName, Object... params){
- if(obj == null) {
- return null;
- }
- Method method = null;
- try{
- //获取类对象中以methodName和接受一个View参数的类型方法
- method = obj.getClass().getDeclaredMethod(methodName,View.class);
- if(method != null){
- //类中的方法为private,故必须进行此操作
- method.setAccessible(true);
- //执行方法,并传递当前对象
- return method.invoke(obj, params);
- }else{
- throw new Exception("no such method:" + methodName);
- }
- }catch(Exception e){
- e.printStackTrace();
- }
- return null;
- }
- }
- }
代
码中的注释还算是比较详细的,这里我就不在细说,大概说下实现原理,首先主要是获取当前类对象的所有属性包括public、private、
protected,然后循环判断属性是否采用注解并且是否是使用我们自定义的注解类,如果两种情况都满足则获取属性所指定的注解值,如果注解值不是默认
的就表示指定了明确的值,这里就可以根据获取到的ID值来findViewById()来获取对象,并且设置给当前属性来完成初始化。
指定控件的点击事件就比较麻烦,首先需要先将属性转成Object对象,然后判断是否是View的实例,如果是则设置点击事件,在
OnClickListener的onClick方法中获取当前类中以根据注解值来命名的方法,然后通过Method的invoke来调用执行,注意,这
里传递了事件对象本身,所以我们方法中必须要加入参数。
至此,我们所有的代码都已经写完了,我们运行起来看看效果