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

来源:互联网 发布:集美大学网络营业厅 编辑:程序博客网 时间:2024/05/22 06:56

前言

之前对注解着一块的知识一直很少使用,只知道基本概念,需要用反射操作,恰好最近的项目中有使用ButterKnife这种注解框架,感觉好用很多,当然,我这里写的跟ButterKnife不太一样,ButterKnife用的是编译时注解,我这里用的是运行时注解,不过学东西,总要一步一步来嘛,先可以应用上,后续再考虑性能问题。

注解

讲到注解,就不得不谈谈注解了,注解是什么,例如一个重写的方法,上面会声明@Override,那么我们就知道这是一个重写的方法了。
Java中有四个元注解,分别是:

@Target,@Retention,@Documented,@Inherited ,是Java 的api提供的,是专门用来定义注解的注解,其作用分别如下:

@Target 表示该注解用于什么地方,可能的值在枚举类 ElemenetType 中,包括:
ElemenetType.CONSTRUCTOR—————————-构造器声明
ElemenetType.FIELD ————————————–域声明(包括 enum 实例)
ElemenetType.LOCAL_VARIABLE————————- 局部变量声明
ElemenetType.METHOD ———————————-方法声明
ElemenetType.PACKAGE ——————————— 包声明
ElemenetType.PARAMETER ——————————参数声明
ElemenetType.TYPE————————————— 类,接口(包括注解类型)或enum声明

@Retention 表示在什么级别保存该注解信息。可选的参数值在枚举类型 RetentionPolicy 中,包括:
RetentionPolicy.SOURCE ———————————注解将被编译器丢弃
RetentionPolicy.CLASS ———————————–注解在class文件中可用,但会被VM丢弃
RetentionPolicy.RUNTIME VM——-将在运行期也保留注释,因此可以通过反射机制读取注解的信息。

@Documented 将此注解包含在 javadoc 中 ,它代表着此注解会被javadoc工具提取成文档。在doc文档中的内容会因为此注解的信息内容不同而不同。相当与@see,@param 等。

@Inherited 允许子类继承父类中的注解。

常用的注解一般会声明两个注解:
@Target和@Retention,来确定我们定义的注解的作用域和注解的作用时间,像之前的@Override我们点进注解中可以看到:

@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public @interface Override {}

作用域是针对方法,而@Retention这个注解是RetentionPolicy.SOURCE,也就是只是做个区分,最终会被编译器丢弃掉。

我们在点击进入ButterKnifer的任意一个注解库:

//// Source code recreated from a .class file by IntelliJ IDEA// (powered by Fernflower decompiler)//package butterknife;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Retention(RetentionPolicy.CLASS)@Target({ElementType.FIELD})public @interface InjectViews {    int[] value();}

可以看到,这里的@Retention注解内容都是RetentionPolicy.CLASS,也就是会在生成的class文件中,android这边应该就是会编译成.dex文件,但最终还是会被虚拟机丢弃,这边在往深的我就不懂了,所以这里也就不搬门弄斧了。

我这里用到的@Retention注解内容是RetentionPolicy.RUNTIME ,也就是在运行时会保留。

对View的绑定

用到注解的话就要用到反射,所以大家还是需要对反射常用的类和方法有一定的了解。

我们平时写界面的时候,总是不断的,每个界面都要写上setContentView,findView等这些不断重复的代码,这些代码对我们的水平提升不大,但是我们还要反复的去敲,很浪费开发时间,所以,我们平时会用一些注解库等来解决这些问题,当然在kotlin里这种操作很简单(题外话),但是我们对这些原理还是要有一些了解,所以,为了避免这种重复的劳动力操作,我们还是要简化这种操作的。

我们参考ButterKnife的写法,在activity中也以这样的一种方式来写:

setContentView的注解

package com.example.panhao.retrofittest;import android.app.Activity;import android.os.Bundle;import android.support.annotation.Nullable;import android.util.Log;import android.view.View;import android.widget.Button;import android.widget.Switch;import static android.content.ContentValues.TAG;/** * Created by panhao on 17-6-17. */@ContentView(R.layout.activity_login)public class LoginActivity extends Activity {    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        InjectUtil.inject(this);    }}

可以看到,在activity类外声明了一个注解,要想这个注解起作用,首先我们得声明一个注解:

package com.example.panhao.retrofittest;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/** * Created by panhao on 17-6-17. */@Target(ElementType.TYPE)//在类的作用范围内@Retention(RetentionPolicy.RUNTIME)//运行时注解public @interface ContentView {    int value();}

有了注解,当然要有注解的解释方法,我们在activity的onCreate方法里调用的InjectUtil的inject方法,这个方法里就对注解做了解析,代码如下:

public static void inject(Activity context) {        injectLayout(context);    }     /**     * 注入界面布局     *     * @param context     */    public static void injectLayout(Activity context) {        Class<? extends Activity> clazz = context.getClass();        //拿到注解        ContentView contentView = clazz.getAnnotation(ContentView.class);        if (contentView == null) {            Log.i(TAG, "injectLayout: ======");            return;        }        int layoutId = contentView.value();        context.setContentView(layoutId);    }

我们传入一个activity,通过Class的getAnnocation方法获得activity类上的注解,判断注解是否是我们需要的注解,是的话就找到注解传入的layoutId进行setContentView操作,我这里没有安装模拟器所以没有截图,真机上是可以绑定到view的大家可以试一下。

findView的注解

package com.example.panhao.retrofittest;import android.app.Activity;import android.os.Bundle;import android.support.annotation.Nullable;import android.util.Log;import android.view.View;import android.widget.Button;import android.widget.Switch;import static android.content.ContentValues.TAG;/** * Created by panhao on 17-6-17. */@ContentView(R.layout.activity_login)public class LoginActivity extends Activity {    @InjectView(R.id.btn)    Button btn;    @InjectView(R.id.switches)    Switch switches;    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        InjectUtil.inject(this);        btn.setText("哈哈哈哈哈哈哈哈");        switches.setChecked(true);    }}
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical">    <Button        android:id="@+id/btn"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="測試注解" />    <Switch        android:id="@+id/switches"        android:layout_width="wrap_content"        android:layout_height="wrap_content" /></LinearLayout>

在布局文件中声明一个button和一个switch,在activity中通过注解进行绑定,在onCreate时改变内容看是否完成了view的绑定,如果没完成肯定会报空指针。

package com.example.panhao.retrofittest;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/** * Created by panhao on 17-6-17. */@Target(ElementType.FIELD)//字段中声明有效@Retention(RetentionPolicy.RUNTIME)//编译时注解public @interface InjectView {    int value();}
/**     * 注入界面内容的布局     *     * @param context     */    public static void injectView(Activity context) {        Class<? extends Activity> clazz = context.getClass();        Field[] fields = clazz.getDeclaredFields();        for (Field field : fields) {            InjectView injectView = field.getAnnotation(InjectView.class);//获取所有标明该注解的对象            //injectView注解不为空时            if (injectView != null) {                int viewId = injectView.value();                View view = context.findViewById(viewId);                try {                    field.set(context, view);                } catch (IllegalAccessException e) {                    e.printStackTrace();                }            }        }    }

这里是对activity里的字段进行注入,所以要获得activity里所有的字段进行遍历判断是否含有我们的注解,含有我们的注解的话就通过findView操作绑定view到activity中。

对事件监听的注入

android 中有很多种事件监听,所以这里如果只是对单个事件的监听绑定定还好处理,但是我们要针对不同事件的监听进行绑定,参考ButterKnife源码,要用到注解的注解,来标明一些信息:

package com.example.panhao.retrofittest;/** * Created by panhao on 17-6-17. */public @interface EventControl {    /**     * 设置事件监听的方法     * @return     */    String listenerMethod();    /**     * 设置事件回调的方法     * @return     */    String callBack();    /**     * 获取监听类型     * @return     */    Class<?> getClazzType();}
package com.example.panhao.retrofittest;import android.app.Activity;import android.os.Bundle;import android.support.annotation.Nullable;import android.util.Log;import android.view.View;import android.widget.Button;import android.widget.Switch;import static android.content.ContentValues.TAG;/** * Created by panhao on 17-6-17. */@ContentView(R.layout.activity_login)public class LoginActivity extends Activity {    @InjectView(R.id.btn)    Button btn;    @InjectView(R.id.switches)    Switch switches;    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        InjectUtil.inject(this);        btn.setText("哈哈哈哈哈哈哈哈");        switches.setChecked(true);    }    @OnClick(R.id.btn)    public void handler(View view) {        Log.i(TAG, "onClick: 我被点击了哦~~~");    }}
package com.example.panhao.retrofittest;import android.view.View;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/** * Created by panhao on 17-6-17. */@Target(ElementType.METHOD)//访问权限@Retention(RetentionPolicy.RUNTIME)//运行时执行@EventControl(        listenerMethod = "setOnClickListener",        callBack = "onClick",        getClazzType = View.OnClickListener.class)public @interface OnClick {    int[] value();//对一组数据添加监听}

在OnClick注解上声明了刚刚定义的对注解的注解,标明了它的监听方法,回调方法和监听类类型,同样我们也可以声明OnItemClick,OnLongClick等其它注解,这也是所以我们要加上注解的注解的原因。

 private static void injectEvent(final Activity context) {        Method[] methods = context.getClass().getDeclaredMethods();        for (Method method : methods) {            //1.获取方法上的所有注解            Annotation[] annotations = method.getAnnotations();            //2判断注解是否是我们需要的注解:也就是注解上是否包含event            for (Annotation annotation : annotations) {                Class<? extends Annotation> annotationType = annotation.annotationType();                //获取注解上是否有我们约定好的注解                EventControl eventControl = annotationType.getAnnotation(EventControl.class);                if (eventControl == null) {                    //说明不是我们想要的注解,跳出此次循环                    continue;                }                //获取注解上声明好的内容                String listener = eventControl.listenerMethod();                String callback = eventControl.callBack();//要拦截的方法                Class<?> listenerType = eventControl.getClazzType();                final Map<String, Method> methodMap = new HashMap<>();                methodMap.put(callback, method);                try {                    Method valueMethod = annotationType.getDeclaredMethod("value");                    //第一个参数是接受者,也就是这里的注解对象,也就是调用annotation的value方法,如果后面有参数                    //则在后面声明不定参数                    int[] viewIds = (int[]) valueMethod.invoke(annotation);                    //遍历id找到view                    for (int viewId : viewIds) {                        final View view = context.findViewById(viewId);                        if (view != null) {                            //给view添加监听                            if (view == null) {                                continue;                            }                            //获取设置监听的方法,但是不知道对什么设置监听:onClick,onItemClick...                            Method setListenerMethod = view.getClass().getMethod(listener, listenerType);                            //这里只有listener的类型                            //根据对应类型生成对应的代理类对象                            Object proxyInstance = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, new InvocationHandler() {                                @Override                                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {                                    if (methodMap != null) {                                        //获取对应方法在map中是否存在,这里的是我们的handle方法                                        Method m = methodMap.get(method.getName());                                        if (m != null) {                                            //调用存入map中的方法,也就是我们在activity中声明监听注解的方法                                            return m.invoke(context, args);                                        }                                    }                                    return method.invoke(proxy, args);                                }                            });                            //给view设置监听                            setListenerMethod.invoke(view, proxyInstance);                        }                    }                } catch (NoSuchMethodException e) {                    e.printStackTrace();                } catch (InvocationTargetException e) {                    e.printStackTrace();                } catch (IllegalAccessException e) {                    e.printStackTrace();                }            }        }    }

这里的逻辑处理就要麻烦一点了,但只要理清思路还是很容易理解的

 1. 获取activity的所有方法 2. 获取每个方法上的所有注解 3. 遍历注解并判断注解上是否含有我们声明的注解的注解 4. 拿到注解的注解的基本信息 5. 拿到annotation上的value包含的所有id 6. 通过id获得view 7. 通过动态代理来生成需要的指定类型的代理对象,并拦截在注解的注解上声明好的方法,反射执行我们activity包含该注解的方法。 8. 调用setXXListener方法来给所有声明id的view添加该监听。

最后测试下对事件的绑定是否有效,效果如控制台打印所示
这里写图片描述

结语

结语这里本来像发一些感慨的,最近确实很忙,是我的事,不是我的事都往我身上推,我真的满烦的,不过想想也没什么,能力越大,责任越大,就算不能在当中学到什么,也能让我意识到我不是一无是处的存在,有的人说我太老实了,容易被别人占便宜,怎么说呢,我觉的这并不是什么老不老实的,只是做人的基本原则吧,答应别人的就好好做,可是有时候确实,一个人的力量真的很有限,还是希望大家不说多做什么,至少在团队中尽到自己的义务吧,如果都想着破罐子破摔,这个团队是很失败的,没有老实人该替你干活,你就算占了一时便宜,也并不能怎么样,你反而丧失了学习最好的时间,其实这反而是得不偿失的。好了,烦躁的胡乱bb了一堆,还是别胡思乱想了,洗洗睡了吧。

原创粉丝点击