之前在整理过一些自定义注解、反射机制、动态代码知识点,这一次重新温习一次,从下面以例子行车,走起来。
正题
- 注解类
- 事件监听工具类
- 测试
注解类
我们为什么要使用注解类呢,原因很简单,那就是拿注解作为一种标记,用到的时候就判断是否是自己定义的注解,是命中,可以做相应的处理了。
既然是点击事件监听,那么直接定义一个点击事件监听注解类。
/**
* @Author 安仔夏天很勤奋
* Create Date is 2020/4/30
* Des
*/
@Target(ElementType.METHOD)//作用在方法上
@Retention(RetentionPolicy.RUNTIME)//保留到运行时期
public @interface MyOnClick {
@IdRes int[] viewId() default {-1};
}
不止一个监听事件吧,是的,我们再定义一个长按事件注解类吧,还可以根据需要添加事件监听。
/**
* @Author 安仔夏天很勤奋
* Create Date is 2020/4/30
* Des
*/
@Target(ElementType.METHOD)//作用在方法上
@Retention(RetentionPolicy.RUNTIME)//保留到运行时期
public @interface MyOnLongClick {
@IdRes int[] viewId() default {-1};
}
这样看上去好像挺不错的,但我们还要做完美一些,怎么个完美呢?点击事件和长按事件在都要view.setOn...Listener的,为了后面减少重复性代码,再定义一个监听事件类型的注解类。
/**
* @Author 安仔夏天很勤奋
* Create Date is 2020/4/30
* Des
*/
@Target(ElementType.ANNOTATION_TYPE)//作用注解上,注解的注解
@Retention(RetentionPolicy.RUNTIME)//保留在运行期
public @interface ListenerEventType {
Class listenerType();//点击事件的类类型 根据不同点击事件设置
String setOnListener();//设置监听的方法名 根据不同点击事件设置
}
听上去这个监听事件类型注解类,好像就相当于一个公共参数或变量抽出来了,是的,可以这么理解。但。。。怎么关联上点击事件注解类和长按注解类呢,哎哟,大伙注意啦监听事件类型注解类的作用域是作用在注解上的,也就是说注解的注解。关键是怎么关联起来,不要急,很简单的,下面代码:
/**
* @Author 安仔夏天很勤奋
* Create Date is 2020/4/30
* Des
*/
//绑定监听的类型
@ListenerEventType(listenerType = View.OnClickListener.class,setOnListener = "setOnClickListener")
@Target(ElementType.METHOD)//作用在方法上
@Retention(RetentionPolicy.RUNTIME)//保留到运行时期
public @interface MyOnClick {
@IdRes int[] viewId() default {-1};
}
/**
* @Author 安仔夏天很勤奋
* Create Date is 2020/4/30
* Des
*/
//注解的注解 绑定监听的类型
@ListenerEventType(listenerType = View.OnLongClickListener.class,setOnListener = "setOnLongClickListener")
@Target(ElementType.METHOD)//作用在方法上
@Retention(RetentionPolicy.RUNTIME)//保留到运行时期
public @interface MyOnLongClick {
@IdRes int[] viewId() default {-1};
}
现在知道是怎么关联起来了啦,如果还不懂清楚的,就到自定义注解及注解的使用,你会有收获的哦。所有的注解类都定义好了,就开搞事件监听工具类。
事件监听工具类
写这个工具类的时候,首先我们要想清楚一件事,我们的View点击事件是作用在那的,可以作用在Activity、定义View上等,我这里只写作用在Activity里的。大致线路:获取所有Activity对象的所有方法 -> 获取方法上所有的注解 -> 获取注解的类型,找到自己定义的注解就命中 -> 获取View的实例对象 -> 通过动态代码 执行setListener里的onClick/onLongClick 方法等
/**
* @Author 安仔夏天很勤奋
* Create Date is 2020/4/30
* Des
*/
public class ClickListenerUtils {
public static void bindListener(final Activity activity){
Class<? extends Activity> aClass = activity.getClass();
if(aClass == null){
return;
}
//获取所有Activity对象的所有方法
Method[] methods = aClass.getDeclaredMethods();
for (final Method method : methods) {
//获取方法上所有的注解
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
//获取注解的类型,找到自己定义的注解就命中
Class<? extends Annotation> annotationType = annotation.annotationType();
//因为annotationType是ListenerEventType的子类型,
// 所有判断注解的父类型是定义的ListenerEventType就命中目标
if(annotationType.isAnnotationPresent(ListenerEventType.class)){
//获取监听事件类型
ListenerEventType eventType = annotationType.getAnnotation(ListenerEventType.class);
//获取监听类型对象
Class listenerType = eventType.listenerType();
//获取监听类型对象的set方法
String setListener = eventType.setOnListener();
try {
//通过反射注解子类方法,拿到方法的值 int[] viewId()
//注意:因为定义了MyOnClick、MyOnLongClick,注解里方法必相同,这样就不用去管方法名不同的情况啦
Method annotationMethod = annotationType.getDeclaredMethod("viewId");
Object viewIdValue = annotationMethod.invoke(annotation);
//得到多个viewId
int[] viewIds = (int[]) viewIdValue;
//做一个强壮性处理
if(viewIds.length==1 && viewIds[0] == -1){
return;
}
// method.setAccessible(true);//授权
// //很容易想到就简原则,这样去反射监听事件
// for (int viewId : viewIds) {
// View view = activity.findViewById(viewId);
// //根据注解定义的类型去反射监听事件 这样做好麻烦,也不优雅,通过动态代理方式更加优雅一些
// view.setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View v) {
// try {
// method.invoke(activity,v);
// } catch (IllegalAccessException e) {
// e.printStackTrace();
// } catch (InvocationTargetException e) {
// e.printStackTrace();
// }
// }
// });
//
// view.setOnLongClickListener(new View.OnLongClickListener() {
// @Override
// public boolean onLongClick(View v) {
// try {
// method.invoke(activity,v);
// } catch (IllegalAccessException e) {
// e.printStackTrace();
// } catch (InvocationTargetException e) {
// e.printStackTrace();
// }
// return true;
// }
// });
//
// }
method.setAccessible(true);//授权
//通过动态代理方式更加优雅
ListenerHandle<Activity> listenerHandle = new ListenerHandle<>(method,activity);
Object proxyInstance = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, listenerHandle);
for (int viewId : viewIds) {
View view = activity.findViewById(viewId);
//获取View的实例对象
Class<? extends View> viewClass = view.getClass();
//不用去关心是那一种事件监听,只要在注解配置好就可以了
Method viewMethod = viewClass.getMethod(setListener,listenerType);
//通过动态代码 执行setListener里的onClick/onLongClick 方法等
viewMethod.invoke(view,proxyInstance);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
}
//这里为什么是泛型,其实是作扩展用的,可能在自定义view注入 T = Activity/View
static class ListenerHandle<T> implements InvocationHandler{
private Method mMethod;
private T target;
public ListenerHandle(Method method,T target){
this.mMethod = method;
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return mMethod.invoke(target,args);
}
}
}
通过上面的工具类知道,里面我注释得很清楚了,是主要一点就是当我们拿到了注解上的viewIds时,最容易想到直接判断是那个点击事件类型直接setOn...Listener了,出现大量的if...else语句,为更加优雅我通过动态代理去处理了,做个甩手掌柜,自动处理不同的setOn...Listener方法。都讲了那么多了,开始写测试类测试一把。
测试类
/**
* @Author 安仔夏天很勤奋
* Create Date is 2020/4/30
* Des
*/
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ClickListenerUtils.bindListener(this);
}
@MyOnClick(viewId = {R.id.btn_one, R.id.btn_two})
public void myOnClick(View view){
switch (view.getId()) {
case R.id.btn_one:
Toast.makeText(this, "onclick one",Toast.LENGTH_SHORT).show();
break;
case R.id.btn_two:
Toast.makeText(this, "onclick two",Toast.LENGTH_SHORT).show();
break;
}
}
@MyOnLongClick(viewId = {R.id.btn_one})
public boolean myLongClick(View view) {
Toast.makeText(this, "long click one",Toast.LENGTH_SHORT).show();
return true;
}
//默认值是-1 做了处理,不会监听事件
@MyOnLongClick()
public boolean myLongClick1(View view) {
Toast.makeText(this, "long click one",Toast.LENGTH_SHORT).show();
return true;
}
}
亲自测试过的,当然没有什么大问题,当然也可以写得不好,或有错误的,请多多指教。
总结
- 了解注解的使用或作用
- 反射,取Method对象,是有区别的,反射机制详情
- 要搞懂动态代码的目的