项目中通过findViewById方式来初始化控件显得代码很臃肿,并且类之间的耦合度很高。为了解决这个问题,有两种方式可以实现:
- IOC:反射+注解方式,运行时处理
- APT:比如butterKnife,编译期处理
下面将用IOC的方式实现布局控件的运行时注解。
一、布局注解
1、创建布局注解类
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface LayoutInject {
int value();
}
注解在类之上,value值就是R.layout.xxx。
2、解析注解
/**
* 注册布局
*/
private static void injectLayout(Object object) {
Class<?> activityClass = object.getClass();
LayoutInject layoutAnnotation = activityClass.getAnnotation(LayoutInject.class);
if(layoutAnnotation != null){
//拿到布局id
int layoutId = layoutAnnotation.value();
try {
//调用setContentView(layoutId)
Method setContentViewMethod = activityClass.getMethod("setContentView", int.class);
setContentViewMethod.setAccessible(true);
//反射
setContentViewMethod.invoke(object,layoutId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
首先拿到布局id,然后通过反射调用setContentView()方法,将布局填充进去。
二、控件注解
1、创建控件注解类
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
int value();
}
注解在属性之上,value值就是R.id.xxx。
2、解析注解
/**
* 注册控件
*/
private static void injectViews(Object object) {
Class<?> activityClass = object.getClass();
//所有的字段属性
Field[] allFields = activityClass.getDeclaredFields();
//1、遍历找到ViewInject注解的属性
for (Field field : allFields) {
ViewInject viewAnnotation = field.getAnnotation(ViewInject.class);
if(viewAnnotation == null){
continue;
}
//2、拿到注解的值
int viewId = viewAnnotation.value();
try {
//3、调用findViewById(viewId)
Method findViewByIdMethod = activityClass.getMethod("findViewById", int.class);
findViewByIdMethod.setAccessible(true);
//4、反射得到view
Object view = findViewByIdMethod.invoke(object, viewId);
//5、重新赋值activity中的view
field.setAccessible(true);
field.set(object,view);
} catch (Exception e) {
e.printStackTrace();
}
}
}
首先需要遍历所有的属性,然后拿到符合条件的属性注解,通过反射调用findViewById()方法,拿到当前控件,最重要的就是需要将拿到的控件重新赋值给activity中的控件。
三、事件注解
控件的事件包括点击事件、长按事件、拖拽事件等。如果针对每一个事件都进行单独的设置,代码会冗余。所以有没有一种通用的形式?那么就需要了解事件的共同点:
- 事件名称:比如 setOnClickListener
- 事件的类:比如 View.OnClickListener
- 事件的回调方法名:比如 onClick
找到了共同点,就可以展开下面的工作了。
1、创建事件三部曲注解类
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnBaseClick {
/**
* 设置事件的名称 比如 setOnClickListener
*/
String clickName();
/**
* 设置事件类 比如 View.OnClickListener
*/
Class clickClass();
/**
* 设置事件的回调 比如 onClick
*/
String clickCallback();
}
注意它是注解在注解之上的。
2、创建点击事件注解类(onClick事件)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@OnBaseClick(
clickName = "setOnClickListener",
clickClass = View.OnClickListener.class,
clickCallback = "onClick")
public @interface OnClick {
int value();
}
这里使用到三部曲的注解类,并把当前事件的具体信息传递过去。
3、创建长按事件注解类(onLongClick事件)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@OnBaseClick(
clickName = "setOnLongClickListener",
clickClass = View.OnLongClickListener.class,
clickCallback = "onLongClick")
public @interface OnLongClick {
int value();
}
同样使用到三部曲的注解类。
/**
* 注册事件
*/
private static void injectEvents(final Object object) {
Class activityClass = object.getClass();
Method[] allMethods = activityClass.getDeclaredMethods();
for (final Method method : allMethods) {
//拿到方法上所有的注解
Annotation[] annotations = method.getAnnotations();
for (Annotation methodAnnotation : annotations) {
Class<? extends Annotation> aClass = methodAnnotation.annotationType();
//拿到注解上的注解
OnBaseClick annotation = aClass.getAnnotation(OnBaseClick.class);
if(annotation == null){
continue;
}
//拿到事件的方法名
String clickName = annotation.clickName();
//拿到事件的类
Class clickClass = annotation.clickClass();
//拿到事件的回调方法
final String clickCallback = annotation.clickCallback();
try {
//拿到控件id,通过反射
Method valueMethod = aClass.getDeclaredMethod("value");
valueMethod.setAccessible(true);
int viewId = (int) valueMethod.invoke(methodAnnotation);
//拿到控件
Method findViewByIdMethod = activityClass.getMethod("findViewById", int.class);
findViewByIdMethod.setAccessible(true);
//拿到view
Object view = findViewByIdMethod.invoke(object, viewId);
//给view设置事件
final Method eventMethod = view.getClass().getMethod(clickName, clickClass);
eventMethod.setAccessible(true);
//使用动态代理拦截事件
Object proxy = Proxy.newProxyInstance(clickClass.getClassLoader(), new Class[]{clickClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method2, Object[] args) throws Throwable {
//执行activity的方法
return method.invoke(object,null);
}
});
eventMethod.invoke(view,proxy);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
以上代码,步骤如下:
(1)遍历当前activity的所有方法(allMethods )
(2)遍历每个方法所有的注解(allMethods )
(3)找到当前注解的类型,符合OnBaseClick注解的满足条件
(4)拿到注解的值:事件名称、事件类、事件回调方法(clickName ,clickClass ,clickCallback )
(5)反射得到当前控件(view )
(6)反射得到当前控件的事件方法(eventMethod)
(7)如果需要事件执行,需要通过动态代理的方式,拦截事件。执行activity的事件方法(proxy )
(8)设置动态代理,反射执行
四、使用
@LayoutInject(R.layout.activity_butterknife)
public class ButterKnifeActivity extends AppCompatActivity {
@ViewInject(R.id.btn1)
Button btn1;
@ViewInject(R.id.btn2)
Button btn2;
TextView textView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ViewInjector.injectActivity(this);
}
@OnClick(R.id.btn1)
public void clickEvent(){
Toast.makeText(this,"点击事件",Toast.LENGTH_SHORT).show();
}
@OnLongClick(R.id.btn2)
public boolean longClickEvent(){
Toast.makeText(this,"长按事件",Toast.LENGTH_SHORT).show();
return false;
}
}
onCreate()方法中调用ViewInjector.injectActivity()完成注解。