美文网首页Android之旅
动手撸一个ButterKnife

动手撸一个ButterKnife

作者: h2coder | 来源:发表于2017-12-02 23:29 被阅读49次
  • 核心思想
  • 使用注解,提供开始注入的方法,找到注解上的id值,findViewById找该id值。

解决方法

  • 自定义注解,反射拿到Activity上的控件变量,取出控件变量的注解的id值,用Activity的findViewById,找该id,找到后,将控件变量设置到设置了注解的对应的变量。

  • 步骤

  • 1、定义一个ViewInjector注入器接口

public interface ViewInjector {
    /**
     * 以Activity为注入对象
     *
     * @param activity Activity实例
     */
    void inject(Activity activity);
}
  • 2、定义ViewInject注解,提供外部绑定id使用
@Target(ElementType.FIELD)//设置注解使用范围为变量
@Retention(RetentionPolicy.RUNTIME)//设置注解生命时长,运行时
public @interface ViewInject {
    /**
     * View value
     */
    int value();
}
  • 3、一个类实现ViewInjector接口,作为注入器实例,提供一个inject()入口方法,使用时在Activity的onCreate()时调用,传入Activity的实例。
public class ViewInjectorImpl implements ViewInjector {
    private ViewInjectorImpl() {
    }

    private static final class SingletonHolder {
        private static final ViewInjectorImpl instance = new ViewInjectorImpl();
    }

    public static ViewInjectorImpl getInstance() {
        return SingletonHolder.instance;
    }

    @Override
    public void inject(Activity activity) {
        if (activity == null) {
            return;
        }
        //绑定控件
        bindViewId(activity);
    }
}
  • 4、提供bindViewId()方法,开始查找Activity上的变量,取出变量,取出变量上的注解,取出注解上的id,使用Activity的findViewById查找id,找到后,如果不为空,则将该View对象设置回那个使用了注解的变量。(代码上的注释已经注释得很清楚呐)

    /**
     * 绑定View的Id
     */
    private void bindViewId(Activity activity) {
        try {
            //1.获取所有的成员变量
            Class<? extends Activity> clazz = activity.getClass();
            //2.遍历所有成员变量,找到使用了ViewInject注解的成员变量(所有类型,包括private)
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                //设置允许访问
                field.setAccessible(true);
                ViewInject annotation = field.getAnnotation(ViewInject.class);
                if (annotation != null) {
                    //3.将使用了注解的成员变量上标记的id值取出
                    int id = annotation.value();
                    //4.调用activity的findViewById查找控件
                    if (id > 0) {
                        View view = activity.findViewById(id);
                        //5.将控件设置给成员变量
                        field.set(activity, view);
                    } else {
                        throw new RuntimeException("ViewInject annotation must have view value");
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • 6、新建ViewInjectManager管理类,提供调用
public class ViewInjectManager {

    /**
     * 获取注入器实现对象
     *
     * @return 注入器实例
     */
    public static ViewInjector getOperate() {
        return ViewInjectorImpl.getInstance();
    }
}
  • 6、使用,使用就很简单啦
@ViewInject(R.id.startBtn)
    public Button startBtn;
    
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewInjectorImpl.getInstance().inject(this);
        //如果注入成功,Button的文字就会变为“bind success”
        toastBtn.setText("bind success");
    }

添加OnClick、OnLongClick使用

  • 能绑定控件还不够,黄油刀我们用得最多的就是onClick和OnLongClick。下面我们就开始定义吧。

  • 基本思想也是和绑定控件一样,只是注解作用于方法上,这时候反射的就不是变量,而是方法,然后找出使用OnClick、OnLongClick注解的方法,取出注解上的id,找控件,设置onClick、onLongClick,在监听回调时,invoke调用Activity上写的方法。

  • 1、定义接口

//点击事件注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    int[] value();
}

//长按事件注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnLongClick {
    int[] value();
}
  • 2、反射Activity方法,找出方法上使用的注解,设置监听,监听回调时反射调用Activity上使用了注解的方法。(同样,代码上的注释已经解释了步骤,大家应该看得懂的)
/**
     * 绑定View的OnClick事件
     *
     * @param activity
     */
    private void bindViewEvent(final Activity activity) {
        //1.获取所有的方法
        Class<? extends Activity> clazz = activity.getClass();
        //2.遍历所有的方法
        Method[] methods = clazz.getDeclaredMethods();
        //3.获取标记了OnClick注解的方法
        for (final Method method : methods) {
            OnClick onClickAnnotation = method.getAnnotation(OnClick.class);
            if (onClickAnnotation != null) {
                //4.取出id,查找View
                int id = onClickAnnotation.value();
                View view = activity.findViewById(id);
                //5.给View绑定onClick,点击时执行
                view.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        try {
                            Class<?>[] parameterTypes = method.getParameterTypes();
                            int paramsCount = parameterTypes.length;
                            if (paramsCount == 0) {
                                method.invoke(activity, new Object[]{});
                            } else {
                                method.invoke(activity, v);
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
            //长按事件
            OnLongClick onLongAnnotation = method.getAnnotation(OnLongClick.class);
            if (onLongAnnotation != null) {
                int id = onLongAnnotation.value();
                View view = activity.findViewById(id);
                if (view != null) {
                    view.setOnLongClickListener(new View.OnLongClickListener() {
                        @Override
                        public boolean onLongClick(View v) {
                            try {
                                Class<?>[] parameterTypes = method.getParameterTypes();
                                int paramsCount = parameterTypes.length;
                                Object o;
                                if (paramsCount == 0) {
                                    o = method.invoke(activity, new Object[]{});
                                } else {
                                    o = method.invoke(activity, new Object[]{v});
                                }
                                return (boolean) o;
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                            return false;
                        }
                    });
                }
            }
        }
    }
  • 3、使用,用过黄油刀的都会啦,很简单
@OnClick(R.id.toastBtn)
    public void OnClick(View view) {
        switch (view.getId()) {
            case R.id.toastBtn:
                toast("onClick !!!");
                break;
        }
    }

    @OnLongClick(R.id.toastBtn)
    public boolean onLongClick(View view) {
        toast("onLongClick !!!");
        return true;
    }

    private void toast(String msg) {
        Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
    }

改进

  • 已经实现了绑定控件、绑定控件点击、长按事件,还有什么可以改进的呢?

  • 例如点击、长按事件,应该可以接收多个控件id,所以注解上的int value(),就应该改为int[] value(),拿取的时候,for循环去做就好啦,设置同一个监听就好。

  • 我们只支持了Activity,我们可以支持Fragment,RecycleView、ListView的ViewHolder,他们有什么共性呢?他们都是View的容器,只要有View,就能findViewById,所以可以抽取一个公共都调用的方法,无论支持Activity还是Fragment还是ViewHolder甚至只要是一个持有View对象的自定义控件,都可以绑定。

  • 还有一个问题就是,拿取变量只在传进来的Activity对象,如果控件变量写在父类就找不到了,所以找之前,应该先递归去找一轮的父类,全部绑定一遍。

  • 还有就是一个可以优化的效率问题,像Activity、Fragment,这些系统提供的类,我们无法去添加注解,并且变量巨多,方法巨多,递归查找他们根本就是没必要的!!所以递归父类的时候,应该去忽略。

  • 可以支持绑定布局文件,这样onCreate里面的setContentView也可以在Activity类上注解。

  • 反射效率比较低,并且如果反射拿取的Activity上变量很多的时候,遍历的个数就会增加,速度自然会慢,如果可以,可以进阶改为编译时注解和注解解释器,在编译时生成对应的代码,引用时引用生成的代码,自然效率会更高,毕竟少了反射和遍历。

结语

  • 文章上面只是一个简单的在Activity绑定控件和控件事件,像Fragment上去绑定,其实也是一个道理,只是抽取多几个方法,最后都是调用到同一个绑定方法。
  • 上述优化,除了编译时注解,在github上的项目已经做了优化,详情请看项目啦。
  • GitHub链接

相关文章

网友评论

    本文标题:动手撸一个ButterKnife

    本文链接:https://www.haomeiwen.com/subject/abbobxtx.html