Android 注解、反射动态代理实现 Butterknife

实现的功能

实现效果

  • setContentView

import android.content.Intent;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

// 项目中,很多的界面,Activity。BaseActivity
// 抽取很多层的父类

// setContentView(int)
@ContentView(R.layout.activity_main)
public class MainActivity extends BaseActivity  {

    @InjectView(R.id.btn)
    private Button btn;
    @InjectView(R.id.tv)
    private TextView tv;

    @Override
    protected void onResume() {
        super.onResume();
        // Button btn = findViewById(R.id.btn);
        Toast.makeText(this, btn.getText().toString(), Toast.LENGTH_SHORT).show();

    }

  • BindView
    @InjectView(R.id.btn)
    private Button btn;
    @InjectView(R.id.tv)
    private TextView tv;
  • OnClick 、OnLongClick
  @OnClick({R.id.btn, R.id.tv}) // 有可能注解值是没有控件注入赋值的
    public void click(View btn) {
        switch (btn.getId()) {
            case R.id.btn:
                // Toast.makeText(this, "btn click", Toast.LENGTH_SHORT).show();
                startActivity(new Intent(this, RViewActivity.class));
                break;

            case R.id.tv:
                Toast.makeText(this, "tv click", Toast.LENGTH_SHORT).show();
                break;
        }
    }

    @OnLongClick({R.id.btn, R.id.tv})
    public boolean longClick(View btn) {
        switch (btn.getId()) {
            case R.id.btn:
                Toast.makeText(this, "btn longClick", Toast.LENGTH_SHORT).show();
                break;

            case R.id.tv:
                Toast.makeText(this, "tv longClick", Toast.LENGTH_SHORT).show();
                break;
        }
        return true;
    }
  • RecyclerView 条目点击
  @OnItemClick(R.id.recyclerView)
    public void itemClick(View view, UserInfo info, int position) {
        Toast.makeText(this, "OnItemClick\n" + info.getPassword(), Toast.LENGTH_SHORT).show();
    }

    @OnItemLongClick(R.id.recyclerView)
    public boolean itemLongClick(View view, UserInfo info, int position) {
        Toast.makeText(this, "OnItemLongClick\n" + info.getPassword(), Toast.LENGTH_SHORT).show();
        return true;
    }

代码实现

  • setContentView
    • 添加注解:
    • 申明注解作用在类上:
@Target(ElementType.TYPE) // 该注解作用于类,接口或者枚举类型上
@Retention(RetentionPolicy.RUNTIME) // 注解会在class字节码文件中存在,jvm加载时可以通过反射获取到该注解的内容
public @interface ContentView {
    // int类型布局
    int value();
}

  • 实现添加 Layout
    // 布局的注入
    private static void injectLayout(Activity activity) {
        // 获取类
        Class<? extends Activity> clazz = activity.getClass();
        // 获取类的注解
        ContentView contentView = clazz.getAnnotation(ContentView.class);
        if (contentView != null) {
            // 获取注解的值(R.layout.xxxmain)
            int layoutId = contentView.value();
            try {
                // 获取指定的方法(setContentView)坑:getMethod
                Method method = clazz.getMethod("setContentView", int.class);
                // 执行方法
                method.invoke(activity, layoutId);
                // 另一种写法:
                // activity.setContentView(layoutId);
            } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }

    }

上面的代码思路为:注入activity,拿到 activity的class.获取ContentView这个注解类,拿到注解值 通过上面的class 进行反射 填充 layout 的id

  • BindView
    • 添加注解
    • 作用于属性上
@Target(ElementType.FIELD) // 属性上
@Retention(RetentionPolicy.RUNTIME)

public @interface InjectView {
  int value();
}

  • 获取注解 findViewById
 // 控件的注入
    private static void injectViews(Activity activity) {
        // 获取类
        Class<? extends Activity> clazz = activity.getClass();
        // 获取类的所有属性
        Field[] fields = clazz.getDeclaredFields();
        // 循环,拿到每个属性
        for (Field field : fields) {
            // 获得属性上的注解
            InjectView injectView = field.getAnnotation(InjectView.class);
            // 获取注解的值
            if (injectView != null) { // 并不是所有的属性都有注解
                int viewId = injectView.value();
                // 获取findViewById方法,并执行
                try {
                    Method method = clazz.getMethod("findViewById", int.class);
                    Object view = method.invoke(activity, viewId); //变量付值
                    // 另一种写法:
                    // view = activity.findViewById(viewId);
                    // 还有坑:访问修饰符
                    field.setAccessible(true); // 设置访问权限private
                    field.set(activity, view); // 属性的值赋给控件,在当前Activity
                    // 颜色、属性等赋值过程参考如上实现
                } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
                    e.printStackTrace();
                }
            }

        }
    }

上面的主要思路是: 通过外面传人的activity 类,获取当前类下所有的属性,遍历所有的属性。拿到属性上的注解,获取注解值。获取当前类下findViewById,通过反射获取当前的属性View ,最终把值赋给当前控件。

  • 事件监听
    • 添加注解
    • 作用在方法上
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventBase(listenerSetter = "setOnClickListener", listenerType = View.OnClickListener.class, callBackListener = "onClick")
public @interface OnClick {

    int[] value();
}

具体作用在对应的类下对应的方法上:

@Target(ElementType.ANNOTATION_TYPE) // 放在注解的上面
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBase {

    // 事件的三个成员
    // 1、set方法名 setOnclickListener()
    String listenerSetter();

    // 2、监听的对象 View.OnclickListener
    Class<?> listenerType();

    // 3、回调方法 onclick(View view)
    String callBackListener();
}

  • 实现
// 事件的注入
    public static void injectEvents(Activity activity) {
        // 获取类
        Class<? extends Activity> clazz = activity.getClass();
        // 获取类的所有方法
        Method[] methods = clazz.getDeclaredMethods();
        // 遍历方法
        for (Method method : methods) {
            // 获取每个方法的注解(多个控件id)
            Annotation[] annotations = method.getAnnotations();
            // 遍历注解
            for (Annotation annotation : annotations) {
                // 获取注解上的注解
                // 获取OnClick注解上的注解类型
                Class<? extends Annotation> annotationType = annotation.annotationType();
                if (annotationType != null) {
                    // 通过EventBase指定获取
                    EventBase eventBase = annotationType.getAnnotation(EventBase.class);
                    if (eventBase != null) { // 有些方法没有EventBase注解
                        // 事件3大成员
                        String listenerSetter = eventBase.listenerSetter();  //setOnClickListener

                        Log.e("--main--","listenerSetter:"+listenerSetter);

                        Class<?> listenerType = eventBase.listenerType();    //View.OnClickListener.class
                        String callBackListener = eventBase.callBackListener();  //onClick
                        Log.e("--main--","callBackListener:"+callBackListener);

                        // 获取注解的值,执行方法再去获得注解的值
                        try {
                            // 通过annotationType获取onClick注解的value值
                            Method valueMethod = annotationType.getDeclaredMethod("value");
                            // 执行value方法获得注解的值
                            int[] viewIds = (int[]) valueMethod.invoke(annotation);

                            // 代理方式(3个成员组合)
                            // 拦截方法
                            // 得到监听的代理对象(新建代理单例、类的加载器,指定要代理的对象类的类型、class实例)
                            ListenerInvocationHandler handler = new ListenerInvocationHandler(activity);
                            // 添加到拦截列表里面
                            handler.addMethod(callBackListener, method);
                            // 监听对象的代理对象
                            // ClassLoader loader:指定当前目标对象使用类加载器,获取加载器的方法是固定的
                            // Class<?>[] interfaces:目标对象实现的接口的类型,使用泛型方式确认类型
                            // InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器的方法
                            Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(),
                                    new Class[]{listenerType}, handler);

                            // 遍历注解的值
                            for (int viewId : viewIds) {
                                // 获得当前activity的view(赋值)
                                View view = activity.findViewById(viewId);
                                // 获取指定的方法
                                Method setter = view.getClass().getMethod(listenerSetter, listenerType); //获取 setOnclickListener 里面的参数 View.Onclick
                                // 执行方法
                                setter.invoke(view, listener); //执行setOnclickListener里面的回调 onclick方法
                            }
                        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

上面的思路:注入当前类,获取类下所有的方法,获取对应方法下的注解方法。获取注解上面的注解,然后拿到注解上的三大类型,拿到最外层注解上id,通过遍历找到对应的View,通过反射执行。最后通过动态代理去拦截 执行回调的方法。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;

// 将回调的onClick方法拦截,执行我们自己自定义的方法(aop概念)
public class ListenerInvocationHandler implements InvocationHandler {

    // 需要拦截的对象
    private Object target;
    // 需要拦截的对象键值对
    private HashMap<String, Method> methodHashMap = new HashMap<>();

    public ListenerInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //method 等于拦截的onclick
        if (target != null) {
            // 获取需要拦截的方法名
            String methodName = method.getName(); // 假如是onClick
            // 重新赋值,将拦截的方法换为show
            method = methodHashMap.get(methodName); // 执行拦截的方法,show
            if (method != null) {
                return method.invoke(target, args);
            }
        }
        return null;
    }

    /**
     * 将需要拦截的方法添加
     * @param methodName 需要拦截的方法,如:onClick()
     * @param method 执行拦截后的方法,如:show()
     */
    public void addMethod(String methodName, Method method) {
        methodHashMap.put(methodName, method);
    }
}

你明白了吗。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容