自定义注解实现简单的ButterKnife功能,简化findViewById和setOnClickListener

本文介绍如何使用自定义注解简化Android开发中的UI元素绑定和点击事件处理,包括@BindId、@BindView和@BindClick三个注解的实现与应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ButterKnife我们应该都很熟悉了,在编写布局的时候可以省略很多findViewById、setOnClickListener等代码,精简代码结构。

本篇文章,会使用自定义注解,实现类似ButterKnife的@Bind和@OnClick注解的功能。
需要注意的是,ButterKnife对注解的解析是在编译期间,而我们实现的方式,是在运行的时候动态解析注解的,性能上会有一点损耗。

新建一个Demo项目,生成默认的MainActivity和布局文件,然后在布局文件里加一个按钮:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_hello1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.377"
        android:text="button1"
        android:textAllCaps="false"/>

</androidx.constraintlayout.widget.ConstraintLayout>

如上,添加的button的id为btn_hello1。

默认情况下我们想要在MainActivity里使用这个button,设置点击事件,就得先findViewById,然后setOnClickListener。如果有多个控件需要这么用的话,就需要编写一大段重复的findViewById等处理,很麻烦。

下面看如何使用自定义注解来简化这个处理,本文提供了三个注解示例。
Demo已上传到这里:https://github.com/liwuchen/AnnotationDemo

一、自定义注解类@BindId,用来替代findViewById操作:

首先创建注解BindId.java:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindId {
    int id();
}

在MainActivity中这样使用它:
声明Button变量:

@BindId(id=R.id.btn_hello1)
Button btnHello1;

然后动态解析这个注解:在setOnContentView之后,调用下面的parseBindId()方法即可代替findViewById的操作。

private void parseBindId() {
    Class<?> clazz = this.getClass();
    Field[] fields =clazz.getDeclaredFields();
    if (fields == null) {
        return;
    }
    for(Field field : fields) {
        if (field.isAnnotationPresent(BindId.class)) {
            BindId bindId = field.getAnnotation(BindId.class);
            field.setAccessible(true);
            try {
                View view = findViewById(bindId.id());
                field.set(this, view);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

这样,一个最简单的代替findViewById的注解就是这样了。

上面这个注解还可以简化一下,用xml中的控件的id直接作为变量名,就可以省去注解的id属性了。看看简化的版本:

二、上一个注解的简化版@BindView(不需要指定控件id的注解类,用来替代findViewById操作):

为和上一个注解区分开,我们创建新的注解类BindView.java:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
}

同时在布局文件里加一个Button,id为btnHello2。

    <Button
        android:id="@+id/btnHello2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_hello1"
        app:layout_constraintVertical_bias="0.074"
        android:text="button2"
        android:textAllCaps="false"/>

同样的,需要在Button2所在的Activity中使用注解:
在MainActivity中使用:

@BindView
Button btnHello2;

注意这个使用的时候,变量名必须与xml中的id保持一致,都为btnHello2。因为我们接下来解析的时候,会用id来绑定此控件。
解析如下,同样的,这个parseBindView()方法需要在setContentView()之后调用:

private void parseBindView() {
    Class<?> clazz = this.getClass();
    Field[] fields =clazz.getDeclaredFields();

    if (fields == null) {
        return;
    }
    Resources resources = getResources();
    String packageName = getPackageName();
    for(Field field : fields) {
        if (field.isAnnotationPresent(BindView.class)) {
            int id = resources.getIdentifier(field.getName(), "id", packageName);
            if (id > 0) {
                View view = findViewById(id);
                field.setAccessible(true);
                try {
                    field.set(this, view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

这样简化版的注解也就完成了。但是这个简化版的注解在Activity中使用时,不能对绑定的控件随意起名了,必须使用xml中控件的id作为控件的变量名。

接下来看看如何创建注解来代替setOnClickListener操作。

三、自定义注解@BindClick,替代setOnClickListener操作:

新建注解类BindClick.java:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindClick {
    int[] ids();
}

如果我们要给Button1和Button2设置点击事件,就在MainActivity中这样使用:

@BindClick(ids={R.id.btn_hello1, R.id.btnHello2})
public class MainActivity extends AppCompatActivity implements View.OnClickListener {

解析方法如下,同样的,在setContentView之后使用:

private void parseBindClick() {
    BindClick bindClick = getClass().getAnnotation(BindClick.class);
    if (bindClick != null) {
        int[] ids = bindClick.ids();
        for (int id : ids) {
            findViewById(id).setOnClickListener(this);
        }
    }
}

这样就完成了对点击事件的设置。
注意这里把OnClickListener设置为控件所在activity了,所以activity必须实现OnClickListener接口,并实现onClick()方法:

@Override
public void onClick(View view) {
    switch (view.getId()) {
        case R.id.btn_hello1:
            Toast.makeText(this, "点击了按钮1", Toast.LENGTH_SHORT).show();
            break;
        case R.id.btnHello2:
            Toast.makeText(this, "点击了按钮2", Toast.LENGTH_SHORT).show();
            break;
    }
}

点击处理,在onClick()里实现即可,和不用注解的写法一样的。

看到这里,可能你会疑问,这么多代码,还不如写个findViewById和setOnClickListener呢。

其实这个注解应该这样使用:
一般我们的项目,所有的activity都会继承一个BaseActivity。因此我们可以把解析注解的方法在BaseActivity里调用。这样就不用每个Activity都写一遍注解解析了。但是每个Activity还是要使用注解来声明变量的。

另外,BaseActivity可能也会用到我们的注解,因此在解析的时候,要同时解析控件所在Activity的父类,即BaseActivity,以BindId注解为例,解析代码改动如下:

private void parseBindId() {
        Class<?> clazz = this.getClass();
        Field[] fields =clazz.getDeclaredFields();
        
        /* 添加这段 解析父类的注解
         * 这里是逐层寻找父类的注解,直至父类为Activity类为止。
         * 实际应用的时候,我们的activity继承的哪个基本Activity类,这里就换成哪个。
        while (!clazz.getName().equals("android.app.Activity")) {
            clazz = clazz.getSuperclass();
            fields = concat(fields, clazz.getDeclaredFields());
        }
        */

        if (fields == null) {
            return;
        }
        for(Field field : fields) {
            if (field.isAnnotationPresent(BindId.class)) {
                BindId bindId = field.getAnnotation(BindId.class);
                field.setAccessible(true);
                try {
                    View view = findViewById(bindId.id());
                    field.set(this, view);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

最后,看看完整的Activity中的代码,如何使用这几个注解:

BaseActivity:


public abstract class BaseActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getLayoutId());

        parseBindView();
        parseBindId();
        parseBindClick();

        initView();
    }

    abstract int getLayoutId();
    abstract void initView();

    /**
     * 绑定view(代码中控件的变量名就是布局文件中该控件的id名)
     */
    private void parseBindView() {
        Class<?> clazz = this.getClass();
        Field[] fields =clazz.getDeclaredFields();

        //解析父类的注解
        while (!clazz.getName().equals("androidx.appcompat.app.AppCompatActivity")) {
            clazz = clazz.getSuperclass();
            fields = concat(fields, clazz.getDeclaredFields());
        }

        if (fields == null) {
            return;
        }
        Resources resources = getResources();
        String packageName = getPackageName();
        for(Field field : fields) {
            if (field.isAnnotationPresent(BindView.class)) {
                int id = resources.getIdentifier(field.getName(), "id", packageName);
                if (id > 0) {
                    View view = findViewById(id);
                    field.setAccessible(true);
                    try {
                        field.set(this, view);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

    }

    /**
     * 绑定view(代码中控件的变量名可以自己起)
     */
    private void parseBindId() {
        Class<?> clazz = this.getClass();
        Field[] fields =clazz.getDeclaredFields();

        //解析父类的注解
        while (!clazz.getName().equals("androidx.appcompat.app.AppCompatActivity")) {
            clazz = clazz.getSuperclass();
            fields = concat(fields, clazz.getDeclaredFields());
        }

        if (fields == null) {
            return;
        }
        for(Field field : fields) {
            if (field.isAnnotationPresent(BindId.class)) {
                BindId bindId = field.getAnnotation(BindId.class);
                field.setAccessible(true);
                try {
                    View view = findViewById(bindId.id());
                    field.set(this, view);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

    }

    private Field[] concat(Field[] a, Field[] b) {
        Field[] c= new Field[a.length + b.length];
        System.arraycopy(a, 0, c, 0, a.length);
        System.arraycopy(b, 0, c, a.length, b.length);
        return c;
    }


    /**
     * 解析BindClick注解,给使用注解的控件设置点击监听
     */
    private void parseBindClick() {
        BindClick bindClick = getClass().getAnnotation(BindClick.class);
        if (bindClick != null) {
            int[] ids = bindClick.ids();
            for (int id : ids) {
                findViewById(id).setOnClickListener(this);
            }
        }
    }
}

MainActivity;

@BindClick(ids={R.id.btn_hello1, R.id.btnHello2})
public class MainActivity extends BaseActivity {

    @BindId(id=R.id.btn_hello1)
    Button btnHello1;

    @BindView
    Button btnHello2;

    @Override
    int getLayoutId() {
        return R.layout.activity_main;
    }

    @Override
    void initView() {
        btnHello1.setText("@BindId示例(需要指定Id)");
        btnHello2.setText("@BindView示例(不需要指定Id)");
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_hello1:
                Toast.makeText(this, "点击了按钮1", Toast.LENGTH_SHORT).show();
                break;
            case R.id.btnHello2:
                Toast.makeText(this, "点击了按钮2", Toast.LENGTH_SHORT).show();
                break;
        }
    }
}

Demo已上传到这里:https://github.com/liwuchen/AnnotationDemo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值