Android自己动手做查找控件、绑定监听的注解框架

原创 2015年09月13日 00:36:26

现在好多第三方开发框架都提供了注解方式设置控件绑定监听的方法。本着知其然知其所以然的态度,今天小菜自己撸了一遍,其中不少参考了xUtils的源码,在这里特别感谢下大神的开源精神,感谢巨人为我们提供肩膀!!

先说明下注解格式
一般格式
@注解类名(key=value)
当有多个key参数时格式
@注解类名(key1=value1,key2=value2,···)
如果参数的值是数组
@注解类名(key={value1,value2,···})
当key的值为“value”时 key就可以省略了
@注解类名(value) 或者 @注解类名({value1,value2,···})
还有已提供默认值的参数可以省略感兴趣的自己去看看Java注解机制吧,这里就不说了。

  • 绑定ContentView

这个最简单,就是拿到注解参数然后调用一下activity的setContentView方法。

  • 新建注解类
    新建注解类
@Target(ElementType.TYPE)//指定修饰类
@Retention(RetentionPolicy.RUNTIME)//设置可以于运行时获取注解参数信息
public @interface ContentView {
    int value();//默认的key设置注解时可以省略 "value=" 这里对应的是资源Id 
}

保存后就可以在代码里面使用我们声明的注解了,如下

@ContentView(R.layout.activity_test)
public class MainActivity extends Activity{

}

不过这种需要运行时注入代码的注解并不会自己生效需要我们编写相应的提取注解参数并处理的代码。

  • 编写处理类

新建一个ViewUtils类 静态的inject()方法处理注解。

public class ViewUtils {

    private static final String TAG = "ViewInject";
    public static void inject(@NonNull Activity activity){
        Class<?> clazz = activity.getClass();
        //isAnnotationPresent() 判断目标类是否存在相应的注解信息
        if(clazz.isAnnotationPresent(ContentView.class)){
            ContentView annotation = clazz.getAnnotation(ContentView.class);   //获取类的注解对象
            int layoutResId = annotation.value();   //获取注解对象里的参数
            activity.setContentView(layoutResId);
        }
    }

最后在onCreate里面调用 ViewUtils.inject()方法 整个流程就Over了。

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      ViewUtils.inject(this);
  }

其实这个注解意义不大,对我们来说代码量有增无减,没有什么意义,使用注解应该是为了减少工作量不是为了增加工作量的。这个主要是为了练手而已,下面我们要做的这个使用频率代码重复量就比较大了,这时候注解的便捷性才能有所体现。

  • 绑定控件

这个其实也简单,就是用反射调用布局容器对象的findViewById方法查找对象,然后将得到的对象赋给相应的字段。

使用时如下

@ViewId(R.id.txt1) 
public TextView txt1;

下面开始自己做@ViewId这个注解

  • 新建注解类
@Target(ElementType.FIELD)     //成员变量         
@Retention(RetentionPolicy.RUNTIME)     
public @interface ViewId {
    int value();
}
  • 编写处理类

还是上面的ViewUtils类,绑定控件我们先以activity为例后面再扩展到其他对象。

        Class<?> clazz = activity.getClass();
        ·····
        Field[] fields = clazz.getDeclaredFields();
        ViewId viewId = null;
        //遍历类成员变量
        for(Field field:fields){
            //检查是否有ViewId注解参数
            if(field.isAnnotationPresent(ViewId.class)) {
                //获取注解对象 viewId.value()得到的值即为对应的资源Id
                viewId = field.getAnnotation(ViewId.class);
                //设置修改成员变量的权限
                field.setAccessible(true);
                try {//为成员变量赋值
                    field.set(activity,activity.findViewById(viewId.value()));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } 
            }
        }

如果要注入的对象不是activity呢 比如常见的 listview优化时使用的ViewHolder 布局对应的是 containerView 这时候需要做相应的修改

//holder对注解成员变量的容器 container是布局容器 即所有控件的父容器对象 可以是activity 也可以是view
public static void injectView(@NonNull final Object holder,@NonNull Object container){
        Class<?> clazz = container.getClass();
        Class<?> hClazz = holder.getClass();
        //Activity 注入contentView
        if(clazz.isAnnotationPresent(ContentView.class) && container instanceof Activity){
            ContentView annotation = clazz.getAnnotation(ContentView.class);
            ((Activity)container).setContentView(annotation.value());
        }
        //判断是否存在findViewById
        Method vFind = null;
        try {
            vFind = clazz.getMethod("findViewById",Integer.TYPE);
        } catch (NoSuchMethodException e) {
            Log.d(TAG,"error no method (findViewById) found");
            return;
        }
        //注入findViewById
        Field[] fields = hClazz.getDeclaredFields();
        ViewId viewId = null;
        for(Field field:fields){
            if(field.isAnnotationPresent(ViewId.class)) {
                viewId = field.getAnnotation(ViewId.class);
                field.setAccessible(true);
                try {
                //利用反射执行findViewById获取view对象
                Object obj = vFind.invoke(container,viewId.value())
                    field.set(holder,obj);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
}

修改后原先的注入activity的方法就变成

public static void inject(@NonNull Activity activity){
        injectView(activity,activity);
}
  • 为控件绑定监听事件

下面来说说为控件绑定监听事件,这个算是比较复杂的,我们先从简单的说起
以绑定onClick事件为例

   @Click(R.id.btn) 
   //该方法的参数类型和返回值类型需要和要绑定监听事件的回调方法一致
   public void onClick(View view){
         Toast.makeText(this, "注入测试!", Toast.LENGTH_SHORT).show();
   }
  • 新建注解类
@Target(ElementType.METHOD)//指定此注解用于修饰方法
@Retention(RetentionPolicy.RUNTIME)
public @interface Click {
    int[] value();//可以为多个控件绑定同一个监听方法
}
  • 编写处理类

接着上面的代码

Method[] methods = hClazz.getDeclaredMethods();
View view = null;
Click click = null;
if(m.isAnnotationPresent(Click.class)){
    click = m.getAnnotation(Click.class);
    m.setAccessible(true);
    final Method method = m;
    for(int id:click.value()) {
    try {
        view = (View) vFind.invoke(container,id);
        //这里的硬编码设置不利于扩展
        view.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View v) {
                  try {
                      method.invoke(holder,v);
                  } catch (IllegalAccessException e) {
                      e.printStackTrace();
                  } catch (InvocationTargetException e) {
                      e.printStackTrace();
                  }
              }
        });
    }catch (Exception e) {
        e.printStackTrace();
    }
}

注意注释的地方,上面的代码虽然可以正常运行成功,但是不利于扩展,如果我们要增加其他的绑定监听比如 onLongClick onTouch onScroll onItemClick等等难道要做N多判断有些监听view类型里没有难道还要强转到相应的类再去设置监听?很明显不应该这么做,参考了下xUtils的源码绑定监听也可以去动态设置,这里需要使用动态代理,如下面的代码有不懂的地方,请自行百度关于动态代理的详细介绍。

  • 为注解添加注解

创建用于修饰注解的注解类

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)    //指定用于修饰注解
public @interface EventInfo {
    Class<?> listenerClass();   //要设置的监听对应的内部类
    String setterName();        //设置监听的方法名
    String methodName();        //监听类内部的回调方法的方法名
}

为监听注解添加注解修饰

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventInfo(listenerClass = View.OnClickListener.class,setterName = "setOnClickListener",methodName = "onClick")
public @interface Click {
    int[] value();
}
  • 创建动态代理的处理器类
public class ProxyHandler implements InvocationHandler {
    private Object holder;      //执行方法的对象
    private Method actMethod;   //实际要执行的方法
    public ProxyHandler(Object holder,Method method){
        this.holder = holder;
        this.actMethod = method;
    }
    //当动态代理的接口被调用的时候实际执行的是这个方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        try {
            //反射调用绑定的方法
            return  actMethod.invoke(holder,args);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }
}
  • 修改处理类
        //注入点击事件
        Method[] methods = hClazz.getDeclaredMethods();
        View view = null;
        Class<? extends Annotation> annotationType = null;
        Method mValues = null,setter = null;
        int[] values = null;
        ProxyHandler proxyHandler = null;
        Object proxy = null;
        for(Method m:methods){
            //获取方法的注解对象列表  (一个方法可以有多种注解)
            Annotation[] annotations = m.getAnnotations();
            if(annotations!=null){
                //遍历注解对象
                for(Annotation annotation:annotations){
                    //获取注解对象的注解类型
                    annotationType = annotation.annotationType();
                    //判断该注解类是否为绑定监听的注解 即判断该注解类是否有EventInfo注解
                    if(annotationType.isAnnotationPresent(EventInfo.class)){
                        //获取EventInfo注解的对象
                        EventInfo eventInfo = annotationType.getAnnotation(EventInfo.class);
                        try {
                            //利用反射获取注解对象的values的值 这里因为指定annotation的具体类型所以不能直接使用 .value()
                            mValues = annotationType.getMethod("value", new Class<?>[0]);
                            values = (int[])mValues.invoke(annotation);

                            if(values!=null){
                                for(int id : values){
                                    view = (View) vFind.invoke(container,id);
                                    m.setAccessible(true);
                                    //用于生成动态代理对象
                                    proxyHandler = new ProxyHandler(holder,m);
                                    //反射获取监听对应的设置方法
                                    setter = view.getClass().getMethod(eventInfo.setterName(), new Class[]{eventInfo.listenerClass()});
                                    //生成动态代理对象 即要设置的监听接口的实例对象
                                    proxy = Proxy.newProxyInstance(eventInfo.listenerClass().getClassLoader(), new Class[]{eventInfo.listenerClass()}, proxyHandler);
                                    //设置监听
                                    setter.invoke(view,proxy);
                                }
                            }
                        } catch (NoSuchMethodException e) {
                            e.printStackTrace();
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }

                    }
                }
            }
        }

大功告成!如果要添加其他的监听设置只需和Click注解一样设置下EventInfo的几个参数就可以了,如LongClick

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventInfo(listenerClass = View.OnLongClickListener.class,
    setterName = "setOnLongClickListener",
    methodName = "onLongClick")
public @interface LongClick {
    int[] value();
}

源码下载

Ps. 绑定控件的注解没有考虑多个控件Id相同的情况,xUtils的注解可以多设置一个parentId的参数来解决这种问题,不过我觉得用处不是很大,小伙伴们自己斟酌吧。

另外附一张findViewById 和 注解绑定控件的测试比较图 差距还是蛮大的。两者都是查询绑定30个控件
这里写图片描述

这里写图片描述

这里写图片描述

版权声明:本文为博主原创文章,转载请注明出处!

Android通过注解实现UI的监听器绑定

1 方法一:http://www.jianshu.com/p/9bee8e143410 public class PanViewInjector { public static void proc...
  • a314773862
  • a314773862
  • 2016年05月13日 20:05
  • 952

自定OnClickListener注解,减少点击事件监听器的代码

在上一篇文章 “自定的FindViewById注解,实现同名的R.id无需初始化”中讲到了自定用FindViewById减少部分相同的代码书写,实际开发过程发现,点击事件的监听器的注册(OnClick...
  • z23546498
  • z23546498
  • 2016年10月17日 23:53
  • 495

Android注解解析,注解用法,仿xUtils用注解初始化控件、点击事件(二)

Android注解解析,注解用法,仿xUtils用注解初始化控件、点击事件(二)
  • qq_31168123
  • qq_31168123
  • 2016年09月08日 16:52
  • 1571

Android 针对layout,view和监听的绑定注解

前言之前对注解着一块的知识一直很少使用,只知道基本概念,需要用反射操作,恰好最近的项目中有使用ButterKnife这种注解框架,感觉好用很多,当然,我这里写的跟ButterKnife不太一样,But...
  • pan861190079
  • pan861190079
  • 2017年06月20日 00:03
  • 379

Android自己动手做查找控件、绑定监听的注解框架

现在好多第三方开发框架都提供了注解方式设置控件绑定监听的方法。本着知其然知其所以然的态度,今天小菜自己撸了一遍,其中不少参考了xUtils的源码,在这里特别感谢下大神的开源精神,感谢巨人为我们提供肩膀...
  • w804518214
  • w804518214
  • 2015年09月13日 00:36
  • 786

Android自定义注解与注解器实现点击事件绑定

背景:前些天看过的butterKnife解析,感觉自己对注解这一块的了解缺口很大,所以稍微学习了一下,感觉还是很好玩的,所以记录下来。本文长期更新维护。 注解是什么?这个东西其实一直活在我们的代码中,...
  • Get_Zoom
  • Get_Zoom
  • 2017年10月11日 19:09
  • 120

Android之注解的使用——绑定android控件

注解介绍 注解最早是在Java的JDK5中出现的概念,并在JDK5推出后风行天下。 Android也继承了这种概念和语法。 这里介绍一下,注解在android中用的最火的一个例子,对控件的注解。...
  • yangzhaomuma
  • yangzhaomuma
  • 2016年04月18日 22:15
  • 1455

Android网络框架xUtils中的View的视图绑定注解操作

xUtils:当前比较火的框架,并且用处也是比较多。一.目前xUtils主要有四大模块: DbUtils模块:用于数据库的操作,也是需要注解方式指定表名和列名; ViewUtils模块:注解...
  • wenzhi20102321
  • wenzhi20102321
  • 2016年12月09日 17:07
  • 930

Android中的IOC框架,完全注解方式就可以进行UI绑定和事件绑定Demo

  • 2014年08月10日 14:44
  • 1.43MB
  • 下载

Android通过Xutils注解实例化以及事件绑定

最近突然发现,Android的控件定义以及事件绑定还有另外一种方法--通过注解方式 比如:XML布局文件         android:id="@+id/dbUtils_database" ...
  • wuxuhua123
  • wuxuhua123
  • 2015年08月04日 11:46
  • 90
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Android自己动手做查找控件、绑定监听的注解框架
举报原因:
原因补充:

(最多只允许输入30个字)