前言
要读懂本文需要有自定义注解、反射以及Java动态代理的基础。代码里有注释,暂时不做过多的分析,后面有时间再补充。
一、定义注解:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Target(ElementType.FIELD)
public @interface BindView {
/**
* 传入View的id
* @return
*/
int value();
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Target(ElementType.METHOD)
public @interface OnClick {
/**
* 传入View的id
* @return
*/
int[] value() default -1;
}
二、定义绑定工具类
import android.app.Activity;
import android.util.SparseArray;
import android.view.View;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 在运行期利用注解传入的id通过反射,初始化View的实例
* <p>
* 因为是在运行期使用了反射,效率会比较低
*/
public class ButterKnife {
public static void bind(Activity activity) {
// 存放添加了@BindView的View实例:key为View的id,value为@BindView的View实例
SparseArray<View> viewMap = new SparseArray<>();
Class<?> clzz = activity.getClass();
// 1、扫描Activity里所有添加了@BinderView的成员(不包括父类的)
Field[] fields = clzz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(BindView.class)) {
BindView bindViewAnnotation = field.getAnnotation(BindView.class);
// 获取注解上的值(View Id)
int viewId = bindViewAnnotation.value();
// 根据View Id调用系统方法实例化成员
View view = activity.findViewById(viewId);
if (view != null) {
field.setAccessible(true);
try {
// 【重点】初始化View
field.set(activity, view);
viewMap.put(viewId, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
// 处理@OnClick的方法,通过使用Java的动态代理技术,为指定的View添加OnClickListener事件
Method[] methods = clzz.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(OnClick.class)) {
OnClick onClickAnnotation = method.getAnnotation(OnClick.class);
// 获取注解参数值
int[] viewIds = onClickAnnotation.value();
if (viewIds.length == 1 && viewIds[0] == View.NO_ID) {
// 如果没有传参数值,那么直接退出循环
break;
}
proxyOnClickListener(activity, method, viewIds, viewMap);
}
}
}
/**
* 为View添加OnClickListener动态代理对象
* 在系统调用OnClickListener.onClick()方法时,实际调用的是被@OnClick注解的方法
*
* @param target 添加了@OnClick注解方法的对象
* @param targetMethod 添加了@OnClick注解的方法
* @param viewIds 需要添加代理点击事件的View的id
* @param viewMap 已缓存的View。如果在解析@BindView里有缓存的View,那么直接拿来用,避免重复调用findViewById()
*/
private static void proxyOnClickListener(final Object target, final Method targetMethod,
int[] viewIds, SparseArray<View> viewMap) {
targetMethod.setAccessible(true);
View.OnClickListener listener = (View.OnClickListener) Proxy.newProxyInstance(target.getClass().getClassLoader(),
new Class[]{View.OnClickListener.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return targetMethod.invoke(target, args);
}
});
for (int viewId : viewIds) {
// 查一下是否有缓存
View view = viewMap.get(viewId);
if (view == null) {
// 没有缓存就直接findViewById实例化
view = ((Activity) target).findViewById(viewId);
}
view.setOnClickListener(listener);
}
}
}
三、写个测试的Activity
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.log.basedemo.R;
import com.log.basedemo.annoationProcessor.runtime.BindView;
import com.log.basedemo.annoationProcessor.runtime.ButterKnife;
import com.log.basedemo.annoationProcessor.runtime.OnClick;
/**
* 分别测试:对View进行自动绑定初始化(findViewById),以及添加OnclickListener事件处理的两种不同时期实现:
* 1、运行期
* 2、编译期(ButterKnife原理)
*/
public class TestActivity extends AppCompatActivity {
@BindView(R.id.textView)
private TextView textView;
@BindView(R.id.button)
private Button button;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.butterknife_demo);
ButterKnife.bind(this);
}
@OnClick(R.id.button)
private void handleClick(View view) {
Toast.makeText(this, "Button:" + button.toString(), Toast.LENGTH_SHORT).show();
textView.setText("我是由运行时注解初始化,并被动态代理设置的点击事件设置的!接收点击事件的View Id:" + view.getId());
}
}
运行效果图: