Achieve goals:

  1. Automatic ID binding via @bindView ()
  2. Click events are automatically bound with @onclick ()

ButterKnife simple principle analysis:

  1. Get the ID and type of the control via @bindView () and @onclick () annotations
  2. Once you get the control ID, a Java file named XXActivity_ViewBinding is automatically generated
  3. While generating the XXActivity_ViewBinding Java file (class file), declare a bind(Activity target) method and write the following statement: Target. TextView = (WidgetType) fb (viewId), fb (viewId). SetOnClickListener ()
  4. Pass in an Activity instance through butterknife. bind(Activity Activity) and execute the statement in Step 4

Step analysis

Through the above analysis, we can define the following minimalist steps:

  1. Declare two annotations @bindView (), @onclick ()
  2. Implementing annotation handlers
  3. Execute the methods in the class file generated by the annotation handler

Began to lu code

Code preparation, environment configuration

  1. Create two new modules, called Annotations and Annotation_compiler, for processing annotations.
  2. Module-annptation_compiler build. Gradle add dependency packages to compileOnly (AS3.4.1)
   annotationProcessor 'com. Google. Auto. Services: auto - service: 1.0 rc4'
   compileOnly 'com. Google. Auto. Services: auto - service: 1.0 rc4'
Copy the code

Conveniently under the Sync

  1. Module dependencies Main Module (APP) dependencies
   implementation project(path: ':annotations')
   annotationProcessor project(path: ':annotation_compiler')
Copy the code

Annotation_compiler module relies on

    implementation project(path: ':annotations')
Copy the code

That’s the end of the preparations

Project directory

Look at the structure first and then look at the code

Implement Step 1- Declare annotations

Create two new annotation classes in the Annotation Module






Remark:

  1. Notice the @ notation before the interface keyword
  2. METHOD – annotation on methods, ElementType.FIELD – annotation on member variables)
  3. @rentention: is to clarify when the annotation is in effect (eg: RetentionPolicy.SOURCE – SOURCE period, retentionPolicy. RUNTIME – RUNTIME)
  4. Default The default value is -1= view. NoId

Implement Step 2- annotation processing

So let’s get our thoughts straight here

Each Activity class with the @bindView / @onclick annotation declaration generates an XXActivity_ViewBinding class and declares a bind(Activity target) method in the class. This method can receive different Activity objects;

  1. Declare an interface class

    Due to each automatically generatedXXActivity_ViewBindingEach class has onebind(Activity target)Method, so we abstract an interface out and finally letXXActivity_ViewBindingClass implements the interface and uses generics to ensure that every method that implements the interface receives itDifferent Activity objectsmodule(app)Let’s go ahead and create a new oneBinder interfaceHere:TWill be used toPassing an Activity object, so that we can be inbind()Methodfb(),setOnClcikListner()
  2. Create annotation handling classes

The functionality of the annotation handling class is really the a. that does the annotation processing and creates the Java class files we need. Create a new AnnotationComPiler class under module(annotation_compiler)

@AutoService(Processor.class)  // Register the annotation handler, which uses the AutoService library that the environment configuration relies on
public class AnnotationCompiler extends AbstractProcessor {}
Copy the code

B. Declare the Filer object. Note that the Filer object and the Writer object can create a file with content

    // Declare file objects (member variables)
 private Filer filer;

 @Override
 public synchronized void init(ProcessingEnvironment processingEnvironment) {
     super.init(processingEnvironment);
     // Don't ask
     filer = processingEnvironment.getFiler();
 }
Copy the code

C. rewrite getSupportedAnnotationTypes (), screening of our target annotation

    /** * specifies the annotation handler to process */
    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        Set<String> types = new HashSet<>();
        //getCanonicalName() gets the package name + class name
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }
Copy the code

D. rewrite getSupportedSourceVersion (), statement of support we can use the default Java version here

    /** * Specifies the Java version supported by the annotation processor */
    @Override
    public SourceVersion getSupportedSourceVersion(a) {
        //processingEnv this is the parent variable
        return processingEnv.getSourceVersion();
    }  
Copy the code

D. Rewrite process() to generate the XXActivity_ViewBinding class file

// Declare the package path
package com.junt.annotationdemo;
// Declare the import of Binder because the Binder interface is implemented
import com.junt.annotationdemo.Binder;
// Click events need to instantiate the view.onClickListener () interface, so declare the import View package
import android.view.View;

public class MainActivity_ViewBinding implements Binder<com.junt.annotationdemo.MainActivity>{
    @Override
    public void bind(final com.junt.annotationdemo.MainActivity target) {
    // First, for the @bindView and @onclick controls, we should need the following when generating code
    //textView- we need to get the name of the control,
    //(Android.widget.textView)- We also need to know the type of the control
    //2131165319- Control Id
        target.textView=(android.widget.TextView)target.findViewById(2131165319);
        target.textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    target.onClick(view);
                } catch(Exception e) { e.printStackTrace(); }}});// Second, for a control that only uses @onclick, only an Id is required
        target.findViewById(2131165320).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    target.onClick(view);
                } catch(Exception e) { e.printStackTrace(); }}}); }}Copy the code

Writing code in the generated Java class file says for example we need to write

package com.junt.annotationdemo;
Copy the code

You just call

writer.write("package com.junt.annotationdemo; \n")
Copy the code

All process code

*/ @override public Boolean process(Set<? extends TypeElement>set, RoundEnvironment RoundEnvironment) {// Get all elements using @bindView Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class); // Classify nodes according to the Activity they are in (an Activity can have multiple @bindbiew), Map<String, List<VariableElement>> mapBindView = new HashMap<>();for(Element Element: elementsAnnotatedWith) {// Get control Element VariableElement VariableElement = (VariableElement) Element; // Get the parent element of the control element Name (Activity or fragment) String activityName = variableElement.getEnclosingElement().getSimpleName().toString(); List<VariableElement> variableElements = mapBindView.get(activityName);if(variableElements == null) { variableElements = new ArrayList<>(); mapBindView.put(activityName, variableElements); } variableElements.add(variableElement); } // get all nodes that use @onclick Set<? extends Element> elementsAnnotatedWithClick=roundEnvironment.getElementsAnnotatedWith(OnClick.class); Map<String, Element> mapClickView = new HashMap<>();for(Element element : elementsAnnotatedWithClick) { String activityName = element.getEnclosingElement().getSimpleName().toString(); mapClickView.put(activityName, element); } // start creating XXActivity_ViewBinding. Writer = null; // Write a xxActivity_ViewBinding each time through the For loopfor(String activityName : Mapbindview.keyset ()) {// Fetch the annotated control elements under activityName List<VariableElement> variableElements = mapBindView.get(activityName); // Get the package name of the activity. (All elements under the same activity have parent elements of that activity, so just pick one of them. Element enclosingElement = variableElements.get(0).getenClosingElement (); String packageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString(); Try {// instantiate a JavaFileObject (we're writing.java files -- class files) JavaFileObjectsourceFile = filer.createSourceFile(
                        packageName + "." + activityName + "_ViewBinding"); // Instantiate the writer object to write the code writer = to a. Java filesourceFile.openWriter(); //1. Write - writer.write("package " + packageName + "; \n");
                writer.write("import " + packageName + ".Binder; \n");
                writer.write("import android.view.View; \n"); //2. Write - declare class and implement interface code writer.write("public class " + activityName + "_ViewBinding implements Binder<"
                        + packageName + "." + activityName + ">{\n"); //3. Write - implement interface method code writer.write(" @Override\n" +
                        " public void bind(final " + packageName + "." + activityName + " target) {\n"); Element clickVariableElement = mapClickView.get(activityName); int[] clickIds = clickVariableElement.getAnnotation(OnClick.class).value(); List<Integer> id = new ArrayList<>(clickIds.length); //4. Write - all controls findViewById codefor(VariableElement variableElement : VariableElements) {/ / access controls the Name String variableName = variableElement. GetSimpleName (), toString (); / / access controls ID int variableId = variableElement. GetAnnotation (BindView. Class). The value (); // Get control Type TypeMirrortypeMirror = variableElement.asType(); // writeFindViewById writeFindViewById(writer, variableName,typeMirror, variableId); // See if the control element has an @onclick annotation, if so, write it at the same timesetOnClickListener
                    if(contains(clickIds, variableId)) {// All control ids that set the click event id.add(variableId); writeSetOnClickListener(writer, packageName, activityName, variableName); }}for(int clickId: clickIds) {// If the clickId is not present in the id collection, the space needs to set the click event, but has not set the click event yetif(! id.contains(clickId)) { writeSetOnClickListenerWithoutName(writer, packageName, activityName, clickId); }} //5. Write writer.write()" \n }\n}\n"); } catch (Exception e) { e.printStackTrace(); } finally {// This Activity has its Id bound to the click event, so remove mapClickView.remove(activityName);if(writer ! = null) { try { writer.close(); writer = null; } catch (Exception e) { e.printStackTrace(); }}}} // Handle the @onclick binding only. The above step only handles all activities that contain @bindView (click events are also handled and removed from mapClickView).if (mapClickView.size() <= 0) {
            return false;
        }
        for (String activityName : mapClickView.keySet()) {
            Element element = mapClickView.get(activityName);
            String packageName = processingEnv.getElementUtils().getPackageOf(element.getEnclosingElement()).toString();
            
            try {
                JavaFileObject sourceFile = filer.createSourceFile(
                        packageName + "." + activityName + "_ViewBinding");
                writer = sourceFile.openWriter(); //1. Write - writer.write("package " + packageName + "; \n");
                writer.write("import " + packageName + ".Binder; \n");
                writer.write("import android.view.View; \n"); //2. Write - declare class and implement interface code writer.write("public class " + activityName + "_ViewBinding implements Binder<"
                        + packageName + "." + activityName + ">{\n"); //3. Write - implement interface method code writer.write(" @Override\n" +
                        " public void bind(final " + packageName + "." + activityName + " target) {\n"); Int [] clickViewIds = element.getannotation (onclick.class).value(); int[] clickViewIds = element.getannotation (onclick.class).value(); //4. Write the -setonClickListener codefor(int clickViewId : clickViewIds) { writeSetOnClickListenerWithoutName(writer, packageName, activityName, clickViewId); } //5. Complete class writer.write(" \n }\n}\n");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if(writer! =null){ writer.close(); writer = null; } } catch (IOException e) { e.printStackTrace(); }}}return false; } /** * Write findViewById code * @param writer writer * @param variableName control name * @paramtypeMirror control type * @param variableId Control Id * @throws IOException Write exception */ private void writeFindViewById(Writer Writer, String variableName, TypeMirrortypeMirror, int variableId) throws IOException {
        writer.write(" target." + variableName + "=" + typeMirror + ")target.findViewById(" + variableId + "); \n"); @param writer writer @param variableName Control name * @throws IOException Write exception */ private void writeSetOnClickListener(Writer writer, String packageName, String className, String variableName) throws IOException { writer.write(" target." + variableName + ".setOnClickListener(new View.OnClickListener() {\n" +
                " @Override\n" +
                " public void onClick(View view) {\n" +
                " try {\n" +
                " target.onClick(view); \n" +
                " } catch (Exception e) {\n" +
                " e.printStackTrace(); \n" +
                " }\n" +
                " }\n" +
                "}); \n"); @param writer writer @param viewId control Id * @throws IOException Write exception */ private  void writeSetOnClickListenerWithoutName(Writer writer, String packageName, String className, int viewId) throws IOException { writer.write(" target.findViewById(" + viewId + ")" + ".setOnClickListener(new View.OnClickListener() {\n" +
                " @Override\n" +
                " public void onClick(View view) {\n" +
                " try {\n" +
                " target.onClick(view); \n" +
                " } catch (Exception e) {\n" +
                " e.printStackTrace(); \n" +
                " }\n" +
                " }\n" +
                "}); \n"); } /** * check whether the array contains a value * Is @bindView * @param arr the Id of all the @onclick controls in an Activity * @param arg The @bindView control Id * @ is set under an Activityreturn true/false
     */
    private boolean contains(int[] arr, int arg) {
        boolean isContain = false;
        if (arr.length == 0) {
            isContain = false;
        } else {
            for (int i : arr) {
                if (i == arg) {
                    isContain = true;
                    break; }}}return isContain;
    }
Copy the code

Implement Step 3- execute the onBind() method in XXActivity_ViewBinding

Module (app) creates a new ButterKnife class

Used in an Activity