ButterKnife實現(事件初始化)

一個星期沒更新博客了,雖然目前博客很亂!最近比較忙,視力有些下降,不過ButterKnife的事件實現終算完成了!

新博客地址

ButterKnifeDemo實現(註解完善,方便閱讀)

目標

之所以butterknife可以實現點擊view的時候調用註解過的方法,其實是在點擊的回調方法中調用目標類的相應註釋過的方法:

view.setOnClickListener(new DebouncingOnClickListener() {
            @Override
            public void doClick(View v) {
            //HomeActivity.click(View v)
                target.click(v);
            }
        });

拿我的demo爲例,我們最終是要生成一個如下的類:

這裏寫代碼片

package com.turbo.demo.apt;
import android.view.View;
import com.turbo.apt.library.internal.DebouncingOnClickListener;
import com.turbo.apt.library.internal.Finder;
import com.turbo.apt.library.internal.ViewBinder;

/**
 * Created by LuckyTurbo on 16/4/11.
 *
 * 待生成的類
 */
public class MainActivity_ViewBinder<T extends MainActivity> implements ViewBinder<T> {
    @Override
    public void bind(Finder finder, final T target, Object source) {
        View view = null;
        view = finder.findRequiredView(source,R.id.rightView,"rightView 我 啊");
        target.rightView = finder.castView(view,R.id.rightView,"rightView 我 啊");
        view.setOnClickListener(new DebouncingOnClickListener() {
            @Override
            public void doClick(View v) {
                target.click(v);
            }
        });

    }
}

那麼下面要做的就是收集各種註解數據,然後再根據這些數據生成最終的類。

收集數據

絕大部分方法的數據都是從註解中獲取的,那麼我們如何設計註解呢?

註解設計分析

需求:
1.id
2.設置方法 (example:setOnLongClickListener)
3.接口類型 (android.view.View.OnLongClickListener)
4.接口中的方法:
    1.方法名 (onLongClick)
    2.參數類型 (android.view.View)
    3.方法返回值 (boolean)

這麼多東西如果在每個註解裏面都加上,想想都噁心,但是註解並沒有繼承這一說,但是我們可以在註解的基礎上再進行註解,上層註解裏面可以設定默認值,每次給上層註解填值就可以了
——其實我們想要的就是規範,就是模版,其實就是針對規範編程

下面看下主要的幾個註解:

  • @OnClick
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
@ListenerClass(
        targetType = "android.view.View",
        setter = "setOnClickListener",
        // 這個是butterKnife中的防卡點擊
        type = "com.turbo.apt.library.internal.DebouncingOnClickListener",
        method = @ListenerMethod(
                name = "doClick",
                parameters = "android.view.View",
                returnType = "void"
        )
)
public @interface OnClick {
    int[] value() default {View.NO_ID};
}
  • @ListenerClass
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)// 靠,獲取註解的註解好像必需要使用runtime,不然取不到
public @interface ListenerClass {
    //    @ListenerClass(
    //            targetType = "android.view.View",
    //            setter = "setOnClickListener",
    //            type = "butterknife.internal.DebouncingOnClickListener",
    //            method = @ListenerMethod(
    //                    name = "doClick",
    //                    parameters = "android.view.View"
    //            )
    //    )

    // 某view
    String targetType();
    // 設置方法的名稱
    String setter();
    // 接口全稱
    String type();
    /** Enum which declares the listener callback methods. Mutually exclusive to {@link #method()}. */
    // 跟method互斥
    Class<? extends Enum<?>> callbacks() default NONE.class;
    /**
     * Method data for single-method listener callbacks. Mutually exclusive with {@link #callbacks()}
     * and an error to specify more than one value.
     */
    ListenerMethod method();

    // callback的默認值
    enum NONE {}
}
  • @ListenerMethod
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ListenerMethod {

    // listener 方法 的 名字
    String name();

    // 方法參數
    String[] parameters() default {};

    // 方法返回類型
    String returnType() default "void";

    /** If {@link #returnType()} is not {@code void} this value is returned when no binding exists. */
    // 如果returnType 不是void,就返回這個值
    String defaultReturn() default "null";
}

註解收集數據結構

這裏寫圖片描述

註解收集數據邏輯

private Map<TypeElement, VBinderBuilder> findAndParseTargets(RoundEnvironment roundEnv) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Map<TypeElement, VBinderBuilder> vBinderBuilderMap = new LinkedHashMap<>();

        // 解析所有的@Bind 元素
        for (Element element : roundEnv.getElementsAnnotatedWith(Bind.class)) {
            TypeElement typeElement = (TypeElement) element.getEnclosingElement();
            VBinderBuilder builderClass = null;
            if (vBinderBuilderMap.containsKey(typeElement)) {
                builderClass = vBinderBuilderMap.get(typeElement);
            } else {
                String targetType = typeElement.getQualifiedName().toString();
                String classPackage = getPackageName(typeElement);
                // packageName$className_ViewBinder
                String className = getClassName(typeElement, classPackage) + APTConfig.SUFFIX;

                builderClass = new VBinderBuilder(classPackage, className, targetType);
                vBinderBuilderMap.put(typeElement, builderClass);
            }

            int value = element.getAnnotation(Bind.class).value();
            String elementName = element.getSimpleName().toString();
            TypeName typeName = TypeName.get(element.asType());
            FieldViewBinding fieldViewBinding = new FieldViewBinding(elementName, typeName, value);
            builderClass.addField(value, fieldViewBinding);
        }
        // 解析所有LISTENERS:方法上的事件註解
        for (Class<? extends Annotation> annotationClass : LISTENERS) {
            for (Element element : roundEnv.getElementsAnnotatedWith(annotationClass)) {
                // 方法元素
                ExecutableElement executableElement = (ExecutableElement) element;
                // 類元素(持有者元素)
                TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
                // 方法的註解對象
                Annotation annotation = element.getAnnotation(annotationClass);
                // 註解的value方法
                Method annotationValue = annotationClass.getDeclaredMethod("value");
                // 獲取註解的ids
                int[] ids = (int[]) annotationValue.invoke(annotation);
                // 方法名稱
                String name = executableElement.getSimpleName().toString();
                // 註解的註解
                ListenerClass listenerClass = annotationClass.getAnnotation(ListenerClass.class);
                // 註解註解的參數
                ListenerMethod listenerMethod = listenerClass.method();
                // 方法參數
                List<? extends VariableElement> methodParameters = executableElement.getParameters();

                Parameter[] parameters = Parameter.NONE;
                // 真實方法參數
                if (!methodParameters.isEmpty()) {
                    parameters = new Parameter[methodParameters.size()];
                    // 註解的註解中的參數類型
                    String[] parameterTypes = listenerMethod.parameters();

                    for (int i = 0; i < methodParameters.size(); i++) {
                        VariableElement methodParameter = methodParameters.get(i);
                        // 方法參數
                        TypeMirror methodParameterType = methodParameter.asType();
                        if (methodParameterType instanceof TypeVariable) {
                            TypeVariable typeVariable = (TypeVariable) methodParameterType;
                            methodParameterType = typeVariable.getUpperBound();
                            messager.printMessage(Diagnostic.Kind.ERROR, ((TypeVariable) methodParameterType).asElement().getSimpleName());
                        }
                        for (int j = 0; j < parameterTypes.length; j++) {
                            // 封裝了真實參數的位置,類型
                            parameters[j] = new Parameter(j, TypeName.get(methodParameterType));
                        }
                    }
                }
                // name:方法名 parameters:方法參數parameter required:方法上是否有Optional註解(一般不會有,所以爲true)
                // 一般都是true,我們這裏沒有給註解設置Optional註解
                MethodViewBinding methodViewBinding = new MethodViewBinding(name, Arrays.asList(parameters), true);
                VBinderBuilder builderClass = getOrCreateTargetClass(vBinderBuilderMap, enclosingElement);
                for (int id : ids){
                    builderClass.addMethod(id,listenerClass,listenerMethod,methodViewBinding);
                }
            }
        }
        return vBinderBuilderMap;
    }

ViewBinder綁定幫助類生成

根據上一篇的實現步驟,我們生成一個實現ViewBinder接口,及其方法,實現控件初始化的類已經不是一件難事,現在最大的困難就是如何通過JavaPoet實現類似下面的代碼:

view.setOnClickListener(new DebouncingOnClickListener() {
            @Override
            public void doClick(View v) {
                target.click(v);
            }
        });

根據butterKnife代碼我給出具體的解決步驟:
以@OnLongClick爲例:

// new DebouncingOnClickListener()
// 這裏就是一個空接口以及一個父類
TypeSpec.Builder interfaceEventBuilder = TypeSpec.anonymousClassBuilder("")
                    .superclass(ClassName.bestGuess(keyListenerClass.type()));
// 這裏代表的是 方法:public void doClick
MethodSpec.Builder interfaceMethodBuilder = MethodSpec.methodBuilder(method.name())
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC)
                        .returns(bestGuess(method.returnType()));
// 第一個參數  是參數類型  第一個參數是參數名(p0 p1 p2 ...)
// 方法中的參數(View v)
                   interfaceMethodBuilder.addParameter(bestGuess(parameterTypes[i]), "p" + i);
// 調用target的對應方法:target.click(v);
codeBuilder.add("target.$L(", methodViewBinding.getName());

整個方法的生成代碼如下:

    private void addMethodBindings(MethodSpec.Builder resultBuilder, VBinderData vbinderData) {
        Map<ListenerClass, Map<ListenerMethod, Set<MethodViewBinding>>> listenerMethodBindings = vbinderData.getMethodBindings();
        if (listenerMethodBindings.isEmpty()) {
            return;
        }
        Set<Map.Entry<ListenerClass, Map<ListenerMethod, Set<MethodViewBinding>>>> entries = listenerMethodBindings.entrySet();
        for (Map.Entry<ListenerClass, Map<ListenerMethod, Set<MethodViewBinding>>> entry : entries) {
            ListenerClass keyListenerClass = entry.getKey();
            Map<ListenerMethod, Set<MethodViewBinding>> methodValue = entry.getValue();
            // 創建接口實體類(空匿名類的接口父類)
            // anonymous 匿名
            TypeSpec.Builder interfaceEventBuilder = TypeSpec.anonymousClassBuilder("")
                    .superclass(ClassName.bestGuess(keyListenerClass.type()));
            // 根據ListenerClass 中的 參數 生成代碼
            ListenerMethod method = keyListenerClass.method();
            if (method != null) {
                MethodSpec.Builder interfaceMethodBuilder = MethodSpec.methodBuilder(method.name())
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC)
                        .returns(bestGuess(method.returnType()));
                String[] parameterTypes = method.parameters();
                for (int i = 0; i < parameterTypes.length; i++) {
                    // 第一個參數  是參數類型  第一個參數是參數名(p0 p1 p2 ...)
                    interfaceMethodBuilder.addParameter(bestGuess(parameterTypes[i]), "p" + i);
                }

                boolean hasReturnType = !"void".equals(method.returnType());
                // 代碼塊
                CodeBlock.Builder codeBuilder = CodeBlock.builder();
                if (hasReturnType) {
                    codeBuilder.add("return ");
                }

                // 一個id對應多個ListenerMethod
                if (methodValue.containsKey(method)) {
                    for (MethodViewBinding methodViewBinding : methodValue.get(method)) {
                        // 調用target的對應方法
                        codeBuilder.add("target.$L(", methodViewBinding.getName());
                        List<Parameter> parameters = methodViewBinding.getParameters();
                        // MethodListener註解的參數
                        String[] methodParameters = method.parameters();
                        // 優化了每次parameters.size()
                        for (int i = 0, count = parameters.size(); i < count; i++) {
                            if (i > 0) {
                                codeBuilder.add(", ");
                            }
                            Parameter parameter = parameters.get(i);
                            int listenerPosition = parameter.getListenerPosition();
                            // 類型不一樣,範型萬能轉換
                            if (parameter.requiresCast(methodParameters[i])) {
                                codeBuilder.add("finder.<$T>castParam(p$L, $S, $L, $S, $L)\n", parameter.getType(),
                                        listenerPosition, method.name(), listenerPosition, methodViewBinding.getName(), i);
                            } else {
                                codeBuilder.add("p$L", listenerPosition);
                            }
                        }
                        codeBuilder.add(");\n");
                    }
                } else if (hasReturnType) {
                    codeBuilder.add("$L;\n", method.defaultReturn());
                }
                interfaceMethodBuilder.addCode(codeBuilder.build());
                interfaceEventBuilder.addMethod(interfaceMethodBuilder.build());
            }
            // 如果不是view類型,需要強轉
            if (!APTConfig.VIEW_TYPE.equals(keyListenerClass.targetType())) {
                // targetType不是view的時候需要強轉
                resultBuilder.addStatement("(($T) view).$L($L)", bestGuess(keyListenerClass.targetType()),
                        keyListenerClass.setter(), interfaceEventBuilder.build());
            } else {
                // view.setOnClickListener(對象)
                resultBuilder.addStatement("view.$L($L)", keyListenerClass.setter(), interfaceEventBuilder.build());
            }
        }
    }

其他的具體步驟大家看下demo

總結

代碼這個東西,看跟敲是兩種體驗,也是兩種結果,以後會繼續堅持,總結下從butterKnife框架細節中我學到的知識:

  • BitSet : java中提供的此神器用來記錄我對一組數據的改動與否
  • @Retention(RUNTIME) :註解的註解是需要runtime的,即便僅在編譯期讀取,RetentionPolicy.CLASS也是不奏效的,必需是RetentionPolicy.RUNTIME
  • for (int i = 0, count = parameters.size(); i < count; i++) : 省去每次遍歷調用parameters.size()的同時,又優雅的初始化了count並附上了想要的值(智慧而優雅)
發佈了39 篇原創文章 · 獲贊 9 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章