现在好多第三方开发框架都提供了注解方式设置控件绑定监听的方法。本着知其然知其所以然的态度,今天小菜自己撸了一遍,其中不少参考了xUtils的源码,在这里特别感谢下大神的开源精神,感谢巨人为我们提供肩膀!!
先说明下注解格式
一般格式
@注解类名(key=value)
当有多个key参数时格式
@注解类名(key1=value1,key2=value2,···)
如果参数的值是数组
@注解类名(key={value1,value2,···})
当key的值为“value”时 key就可以省略了
@注解类名(value) 或者 @注解类名({value1,value2,···})
还有已提供默认值的参数可以省略感兴趣的自己去看看Java注解机制吧,这里就不说了。
这个最简单,就是拿到注解参数然后调用一下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的参数来解决这种问题,不过我觉得用处不是很大,小伙伴们自己斟酌吧。