关闭

浅析ButterKnife的实现 (四) —— OnClick

标签: 实现ButterKnifeJava编译时注解OnClick
2710人阅读 评论(0) 收藏 举报
分类:

相关文章:

浅析ButterKnife的实现 (一) —— 搭建开发框架

浅析ButterKnife的实现 (二) —— BindResource

浅析ButterKnife的实现 (三) —— BindView

讲完了View注解,下面来介绍怎么给View设置点击监听。

@OnClick

定义个用来设置点击监听的注解:

/**
 * 点击注解
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
    @IdRes int[] value() default -1;
}
这里同样用 @IdRes 限定了属性的取值范围为 R.id.xxx,并且属性值是个数组,所以可以同时给多个View设置点击监听。该注解的目标对象是方法(ElementType.METHOD),并且属性有个默认值-1,这个后面会提到。有一点需要注意的是,实际开源项目中还定义了个元注解 @ListenerClass 来设置各种点击注解,这样在代码中就可以对所有监听器做统一处理,不过这样代码逻辑就变得有点复杂,这里我们只对 OnClick进行单独处理,这样代码会显得简单清晰,如果想了解更多内容可看开源源码。

再来看注解处理器的处理:

@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 略...
        // 处理OnClick
        for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) {
            if (VerifyHelper.verifyOnClick(element, messager)) {
                ParseHelper.parseOnClick(element, targetClassMap, erasedTargetNames,
                        elementUtils, typeUtils, messager);
            }
        }
        // 略...
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(BindString.class.getCanonicalName());
        annotations.add(BindColor.class.getCanonicalName());
        annotations.add(Bind.class.getCanonicalName());
        annotations.add(OnClick.class.getCanonicalName());
        return annotations;
    }
}
一样的代码模板,我们只需要关注解析方法就行了,检测也没特别要解释的。直接来看解析处理:

public final class ParseHelper {

    static final int NO_ID = -1;
    
	/**
     * 解析 OnClick 资源
     *
     * @param element        使用注解的元素
     * @param targetClassMap 映射表
     * @param elementUtils   元素工具类
     */
    public static void parseOnClick(Element element, Map<TypeElement, BindingClass> targetClassMap,
                                 Set<TypeElement> erasedTargetNames,
                                 Elements elementUtils, Types typeUtils, Messager messager) {

        ExecutableElement executableElement = (ExecutableElement) element;
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

        int[] ids = element.getAnnotation(OnClick.class).value();
        String name = executableElement.getSimpleName().toString();

        // 检测是否有重复 ID
        Integer duplicateId = _findDuplicate(ids);
        if (duplicateId != null) {
            _error(messager, element, "@%s annotation contains duplicate ID %d. (%s.%s)", Bind.class.getSimpleName(),
                    duplicateId, enclosingElement.getQualifiedName(), element.getSimpleName());
            return;
        }

        // 如果未设置ID则默认为 NO_ID,则把外围类作为要设置点击的对象
        for (int id : ids) {
            if (id == NO_ID) {
                // 有 NO_ID 则只能有一个ID
                if (ids.length > 1) {
                    _error(messager, element, "@%s annotation contains invalid ID %d. (%s.%s)",
                            OnClick.class.getSimpleName(), NO_ID, enclosingElement.getQualifiedName(),
                            element.getSimpleName());
                    return;
                }
                // 判断外围类的类型是 VIEW 的子类或接口
                if (!_isSubtypeOfType(enclosingElement.asType(), VIEW_TYPE) && !_isInterface(enclosingElement.asType())) {
                    _error(messager, element, "@%s annotation without an ID may only be used with an object of type "
                                    + "\"%s\" or an interface. (%s.%s)",
                            OnClick.class.getSimpleName(), VIEW_TYPE,
                            enclosingElement.getQualifiedName(), element.getSimpleName());
                    return;
                }
            }
        }

        List<? extends VariableElement> methodParameters = executableElement.getParameters();
        // OnClickListener 的方法void onClick(View v),只有一个参数 View,所有我们的方法不能超过1个参数,可以为0
        if (methodParameters.size() > 1) {
            _error(messager, element, "@%s methods can have at most 1 parameter(s). (%s.%s)",
                    OnClick.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName());
            return;
        }
        // 返回值类型也要满足 OnClickListener 的方法void onClick(View v)
        TypeMirror returnType = executableElement.getReturnType();
        if (returnType instanceof TypeVariable) {
            TypeVariable typeVariable = (TypeVariable) returnType;
            returnType = typeVariable.getUpperBound();
        }
        if (returnType.getKind() != TypeKind.VOID) {
            _error(messager, element, "@%s methods must have a 'viod' return type. (%s.%s)",
                    OnClick.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName());
            return;
        }

        Parameter[] parameters = Parameter.NONE;
        // 我们已经知道方法参数最多只能有一个,且必须为 View 的子类或接口
        if (!methodParameters.isEmpty()) {
            parameters = new Parameter[1];
            TypeMirror typeMirror = methodParameters.get(0).asType();
            // 泛型处理
            if (typeMirror instanceof TypeVariable) {
                TypeVariable typeVariable = (TypeVariable) returnType;
                typeMirror = typeVariable.getUpperBound();
            }
            // 必须为 View 的子类或接口
            if (!_isSubtypeOfType(typeMirror, VIEW_TYPE) && !_isInterface(typeMirror)) {
                _error(messager, element, "Unable to match @%s  method arguments. (%s.%s)",
                        OnClick.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName());
                return;
            }
            parameters[0] = new Parameter(0, TypeName.get(typeMirror));
        }

        OnClickBinding methodViewBinding = new OnClickBinding(name, Arrays.asList(parameters));
        BindingClass bindingClass = _getOrCreateTargetClass(element, targetClassMap, elementUtils);
        for (int id : ids) {
            bindingClass.addMethodBinding(id, methodViewBinding);
        }

        erasedTargetNames.add(enclosingElement);
    }
}

我们按步骤来说明:

1、获取注解的ID值并检测是否有重复 ID;

2、如果使用默认 NO_ID(-1),则ID列表只能存在一个并且将外围类作为要设置点击监听的对象,同样需要判断外围类的类型是 VIEW 的子类或接口

3、检测方法的参数和返回值类型,因为我们已经知道 OnClickListener接口的方法为void onClick(View v),所以我们可以根据这个来直接判断;

4、获取参数的确切类型并保存为 Parameter,参数类型必须为VIEW 的子类或接口

5、生成 OnClickBinding 并添加到 BindingClass中;

来看下 Parameter 和 OnClickBinding 的定义:

/**
 * 方法的参数
 */
final class Parameter {

    static final Parameter[] NONE = new Parameter[0];

    // 参数的位置索引
    private final int listenerPosition;
    // 参数的类型
    private final TypeName type;

    Parameter(int listenerPosition, TypeName type) {
        this.listenerPosition = listenerPosition;
        this.type = type;
    }

    int getListenerPosition() {
        return listenerPosition;
    }

    TypeName getType() {
        return type;
    }

    public boolean requiresCast(String toType) {
        return !type.toString().equals(toType);
    }
}

Parameter 主要就是保存了参数的位置索引和参数类型,这个应该很好理解。

/**
 * 点击方法信息
 */
final class OnClickBinding implements ViewBinding {

    private final String name;
    private final List<Parameter> parameters;

    OnClickBinding(String name, List<Parameter> parameters) {
        this.name = name;
        this.parameters = Collections.unmodifiableList(new ArrayList<>(parameters));
    }

    public String getName() {