Java基础回归之注解Annotation【低仿ButterKnife实战篇】

前言

书接上回,上回说到库里对战湖人三分10投0中,真真气煞我库也,这下把气全撒在鹈鹕身上,一口气轰下破纪录的13记三分。 上回说到Java基础回归之注解Annotation【基础篇】,这回我们来真刀真枪实战。相信很多做安卓的同学都用过至少听过ButterKnife,没错,就是大神JakeWharton的黄油刀。本篇博文将结合注解和反射,实现类似Jake Wharton大神的ButterKnife注入框架,需要说明的是ButterKnife实现方式和我们的实现方式是有区别的,它里面是用到了APT技术,他是基于编译期的注入,所以效率比我们用反射高,本文是基于运行期的。但是这并不妨碍我们造轮子。
ps:对反射不熟悉可以看博主另外一篇博文:Java基础回归之反射Reflection,本文不讲解ButterKnife的用法。

取名

既然是低仿大名鼎鼎ButterKnife(黄油刀),那我们项目的名字也要低仿,就叫Shaver(剃须刀)吧..

Shaver的功能点

  • @Bind(R.id.btn1):用于注入view,代替繁琐的findViewById操作
  • @ContentView(R.layout.activity_main):用于代替注入contentView
  • @StringRes(R.string.string_shaver):用于注入String资源文件
  • @OnClick({R.id.btn1,R.id.btn2}):用于绑定view的监听事件

目标效果

目标效果

开始造轮子

我先帮大家捋一遍思路,其实说白了就是编写上述4个注解类,然后编写一个处理这四种注解的核心类。例如要处理绑定view的@Bind注解,我们需要将activity传入到核心类中,核心类反射获取到标注有@Bind注解的成员变量field,然后获取该注解的value,即view的id,最后将id利用activity.findViewById(value)获取到view,然后反射将获取到的view赋值给改成员变量filed,这样我们就成功将view注入进去了,其他三个注解同理,下面show you the code,代码注释很详细,请仔细看。

  • 首先理所应当,我们编写4个注解类:
  • @Bind注解类
package com.youzhi.shaver.core;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** * 代替findViewById的注解 */
@Target(ElementType.FIELD)//标注目标为成员变量
@Retention(RetentionPolicy.RUNTIME)//生命周期为运行时
public @interface Bind {  
  int value();//用于保存控件id
}
  • @ContentView注解类
package com.youzhi.shaver.core;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** * 代替setContentView的注解 */
@Target(ElementType.TYPE)//标注目标为类前
@Retention(RetentionPolicy.RUNTIME)//生命周期为运行时
public @interface ContentView {
    int value();//用于保存layoutId
}
  • @StringRes注解类
package com.youzhi.shaver.core;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** * 代替getResource().getString(R.string.xx) */
@Retention(RetentionPolicy.RUNTIME)//生命周期为运行时
@Target(ElementType.FIELD)//标注目标为成员变量
public @interface StringRes {
    int value();//用于保存string的id
}
  • OnClick注解类
package com.youzhi.shaver.core;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** * 代替setOnClickListener的注解 */
@Retention(RetentionPolicy.RUNTIME)//生命周期为运行时
@Target(ElementType.METHOD)//标注目标为方法上
public @interface OnClick {
    int[] value();//用于保存控件id集
}
  • 接下来是最核心的处理类,所有逻辑都在这个处理类上面。(ps:可以优化,例如用Map将view缓存起来...这里留给大家)
package com.youzhi.shaver.core;
import android.app.Activity;
import android.view.View;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/** * 注入接口 */
public class Shaver {
    public static void bind(Activity activity) {
        try {
            bindContentView(activity);
            bindStringRes(activity);
            bindViews(activity);
            bindClicks(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 注入contentView
     * @param activity
     */
    private static void bindContentView(Activity activity) {
        Class<? extends Activity> aClass = activity.getClass();
        ContentView annotation = aClass.getAnnotation(ContentView.class);
        if(null != annotation){
            int layoutId = annotation.value();//获得注解上的layoutId值
            activity.setContentView(layoutId);//将layoutId设置给activity
            //activity.setContentView(layoutId)也可用反射实现,但效率低且麻烦,所以直接使用上面setContentView(layoutId)方法,此处只是顺便说一下反射调方法 
           //aClass.getMethod("setContentView",int.class).invoke(activity,layoutId); 
       }
    }
    /**
     * 注入String资源
     * @param activity
     */
    private static void bindStringRes(Activity activity) throws IllegalAccessException {
        Class<? extends Activity> aClass = activity.getClass();
        Field[] fields = aClass.getDeclaredFields();
        //遍历成员变量取出被StringRes注解的field
        for (Field field : fields) {
            if(field.isAnnotationPresent(StringRes.class)){
                StringRes annotation = field.getAnnotation(StringRes.class);
                int stringId = annotation.value();//string资源文件id
                String stringValue = activity.getString(stringId);//获取到相应资源文件string值
                //反射赋值
                field.setAccessible(true);//破封装
                field.set(activity,stringValue);
            }
        }
    }
    /**
     * 注入view
     * @param activity
     * @throws IllegalAccessException
     */
    private static void bindViews(Activity activity) throws IllegalAccessException/*, NoSuchMethodException, InvocationTargetException */{
        //反射拿到@Bind注解的成员变量
        Class<? extends Activity> aClass = activity.getClass();
        Field[] fields = aClass.getDeclaredFields();//拿到所有成员变量
        for (Field field : fields) {
            //遍历成员变量,判断成员变量上是否有@Bind注解
            if (field.isAnnotationPresent(Bind.class)) {
                //如果有,拿出注解的value值,即控件id
                Bind bind = field.getAnnotation(Bind.class);
                int viewId = bind.value();
                View view = activity.findViewById(viewId);//获取到view对象
                //activity.findViewById(viewId)也可用反射实现,但效率低且麻烦,所以直接使用上面find方法,此处只是顺便说一下反射调方法
                //View view= (View) aClass.getMethod("findViewById",int.class).invoke(activity,viewId);
                field.setAccessible(true);//破封装
                field.set(activity, view);//将view设置给该成员变量
            }
        }
    }

    /**
     * 绑定监听事件
     * @param activity
     */
    private static void bindClicks(final Activity activity) {
        Class<? extends Activity> aClass = activity.getClass();
        Method[] declaredMethods = aClass.getDeclaredMethods();//反射获取方法
        //遍历方法,判断方法上是否有@OnClick注解
        for (final Method method : declaredMethods) {
            if(method.isAnnotationPresent(OnClick.class)){
                OnClick annotation = method.getAnnotation(OnClick.class);
                int[] viewIds = annotation.value();//拿到该方法上注解的view的id集
                for (int viewId : viewIds) {
                    final View view = activity.findViewById(viewId);
                    if(null != view){
                        view.setOnClickListener(new View.OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                try {
                                    method.setAccessible(true);//破封装
                                    method.invoke(activity,view);//调起该带有@OnClick注解方法
                                } catch (Exception e) {
                                    e.printStackTrace();
                                }
                            }
                        });
                    }
                }
            }
        }
    }
}

我们看到,Shaver类里面定义了一个入口方法bind(Activity activity) ,然后bind方法里面我们调用了4个方法,这4个方法分别是处理4个注解逻辑,其思路上面已经说了,就是通过反射扫描带有这4个注解的编程元素(类/方法/成员变量),然后获取注解上的value,拿到这些id后,我们就可以做很多事了。代码本身不复杂,这里就不多说了。

  • Shaver使用
package com.youzhi.shaver;
//省略各种导包
@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
    @Bind(R.id.btn1)
    Button btn1;
    @Bind(R.id.btn2)
    Button btn2;
    @Bind(R.id.tv1)
    TextView tv1;
    @Bind(R.id.et1)
    EditText et1;
    @StringRes(R.string.string_shaver)
    String stringRes;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Shaver.bind(this);
        Log.e("MainActivity", btn1.getText().toString() + " , " + btn2.getText().toString() + " , " + tv1.getText().toString() + " , " + et1.getText().toString());
        tv1.setText(stringRes);
    }
    @OnClick({R.id.btn1,R.id.btn2})
    public void onClicks(View view){
        switch (view.getId()){
            case R.id.btn1:
                Toast.makeText(this, "点击了btn1", Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn2:
                Toast.makeText(this, "点击了btn2", Toast.LENGTH_SHORT).show();
                break;
        }
    }
}

然后我们在String文件中有一个string_shaver,用于例子中注入@StringRes(R.string.string_shaver)

String资源文件.png

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:gravity="center_horizontal"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/btn1"
        android:layout_width="150dp"
        android:background="#33ff00ff"
        android:layout_marginBottom="10dp"
        android:text="按钮1"
        android:layout_height="30dp"/>
    <Button
        android:id="@+id/btn2"
        android:text="按钮2"
        android:layout_width="150dp"
        android:background="#33ffff00"
        android:layout_marginBottom="10dp"
        android:layout_height="30dp"/>
    <TextView
        android:id="@+id/tv1"
        android:layout_width="150dp"
        android:background="#4433bb00"
        android:layout_marginBottom="10dp"
        android:gravity="center"
        android:text="文本1"
        android:layout_height="30dp"/>
    <EditText
        android:id="@+id/et1"
        android:layout_width="150dp"
        android:background="#443300ff"
        android:layout_marginBottom="10dp"
        android:gravity="center"
        android:text="输入文本框1"
        android:layout_height="30dp"/>

</LinearLayout>
  • 运行结果及打印的log
运行结果.png

log.png

我们成功获取到各个view的文本,以及设置上点击事件,说明我们的Shaver起作用了!It works!

这里需要注意打印出来的文本框为什么运行结果是“我是字符串资源”而log是“文本1”?大家看仔细点,我们布局文件里面text是“文本1”,然后我们在MainActivity里面打印出来后,我们重新set了一次,改成string文件里的值了。

The End

我们的低仿ButterKnife到此结束,相信讲解的已经够仔细了,到这里我相信现在对反射及注解有更深入的理解和巩固。
最后,转载请注明出处。

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

推荐阅读更多精彩内容