⚠️ The module created is a java module rather than an Android Library. If the latter is created, AbstractProcessor cannot be used
According to the book, I want to imitate butterknife myself
The final project structure is shown in the figure above. Among them, annotations is a Java library with different annotations, process is an annotation processor and a Java library, and butterKnife is an Android library to implement the final onbind() method
The final generated file is in this location (I found it wrong at the beginning. I thought it was checked many times under resource, but I didn't find it. Finally, I searched globally and found that the final generated file may be in this path because of the problems of as and gradle versions)
The essence of ButterKinfe is to write findViewById and OnClick instead of ourselves, which liberates our hands and uses BindView and other annotations to implement the corresponding methods. However, in essence, it still needs to find the resource id through findViewById() and bind it with the control, as shown in the following figure
Therefore, the essence of imitating ButterKnife is to define similar annotations and generate corresponding files and functions.
1. Create a new Java Library in the project to store annotations. The Library is called annotations, similar to the following figure
The Java library in the build.exe Gradle is configured as follows
plugins { id 'java-library' } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) } java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 }
2. In the project, create a new Java Library to store the annotation Processor. This Library is called process (pay attention to the path. I accidentally lost some paths at the beginning of writing, resulting in failure to generate correct results). Since process is an annotation Processor, it is necessary to manually or automatically generate the Processor file, as shown in the following figure
I use Google's open source autoservice, build Gradle is configured as follows
plugins { id 'java-library' } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) annotationProcessor'com.google.auto.service:auto-service:1.0-rc4' compileOnly 'com.google.auto.service:auto-service:1.0-rc3' implementation project(path: ':annotations') } java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 }
And add @ AutoService (Processor.class) to the processor
Take a simple example, BindString
Before ButterKnife, use string, similar to
public class MainActivity extends AppCompatActivity { @BindView(R.id.text_view) TextView textView; //@BindString(R.string.app_name) String meg = null; @OnClick(R.id.text_view) public void onClick(View view){ switch (view.getId()){ case R.id.text_view: meg = getResources().getString(R.string.app_name); Toast.makeText(MainActivity.this,meg,Toast.LENGTH_LONG).show(); break; } }
After use
public class MainActivity extends AppCompatActivity { @BindView(R.id.text_view) TextView textView; @BindString(R.string.app_name) String meg; @OnClick(R.id.text_view) public void onClick(View view){ switch (view.getId()){ case R.id.text_view: Toast.makeText(MainActivity.this,meg,Toast.LENGTH_LONG).show(); break; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); TextView textView = findViewById(R.id.text_view); } }
Generated MainActivity$ViewBinder file
package com.example.farmedemo; import android.view.View; public class MainActivity$ViewBinder{ public MainActivity$ViewBinder(final com.example.farmedemo.MainActivity target){ target.textView =(android.widget.TextView)target.findViewById(2131230975); (target.findViewById(2131230975)).setOnClickListener(new View.OnClickListener() { public void onClick(View p0) { target.onClick(p0); } }); target.meg =(java.lang.String)target.getBaseContext().getResources().getString(2131623963); } }
AnnotationCompiler
package com.example.process; import com.example.annotations.BindString; import com.example.annotations.BindView; import com.example.annotations.OnClick; import com.google.auto.service.AutoService; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Filer; import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Name; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; @AutoService(Processor.class) public class AnnotationCompiler extends AbstractProcessor { //The object that generated the file Filer filer; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); //Initializes the object that generated the file filer = processingEnvironment.getFiler(); } /** * Declare the annotation to be processed by the annotation processor * @return */ @Override public Set<String> getSupportedAnnotationTypes() { Set<String> types = new HashSet<>(); types.add(OnClick.class.getCanonicalName()); types.add(BindView.class.getCanonicalName()); types.add(BindString.class.getCanonicalName()); return types; } /** * Declare the java source version supported by the annotation processor * @return */ @Override public SourceVersion getSupportedSourceVersion() { return processingEnv.getSourceVersion(); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { //Get the annotation in the program and correspond to the class object it belongs to one by one Map<TypeElement, ElementForType> andParseTargets = findAndParseTargets(roundEnvironment); //Write file if(andParseTargets.size()>0){ Iterator<TypeElement> iterator = andParseTargets.keySet().iterator(); Writer writer = null; while (iterator.hasNext()){ TypeElement typeElement = iterator.next(); ElementForType elementForType = andParseTargets.get(typeElement); String activityName = typeElement.getSimpleName().toString(); //Get new class name String newClazzName = activityName+"$ViewBinder"; //Get package name String packageName = getPackageName(typeElement); //Create a java file try { JavaFileObject sourceFile = filer.createSourceFile( packageName+ "." + newClazzName); writer = sourceFile.openWriter(); StringBuffer stringBuffer = getStringBuffer(packageName, newClazzName, typeElement, elementForType); writer.write(stringBuffer.toString()); } catch (IOException e) { e.printStackTrace(); }finally { if(writer != null){ try { writer.close(); } catch (IOException e) { e.printStackTrace(); } } } } } return false; } /** * Get all the annotations and map the annotations to the Activity one by one * @param roundEnvironment */ private Map<TypeElement, ElementForType> findAndParseTargets(RoundEnvironment roundEnvironment) { Map<TypeElement, ElementForType> map = new HashMap<>(); //Get all nodes in the module that use BindView Set<? extends Element> viewElelments = roundEnvironment.getElementsAnnotatedWith(BindView.class); //Get all nodes in the module that use OnClick Set<? extends Element> methodElements = roundEnvironment.getElementsAnnotatedWith(OnClick.class); //Get all nodes in the module that use BindString Set<?extends Element> stringElements = roundEnvironment.getElementsAnnotatedWith(BindString.class); //Traverse all member variable nodes and encapsulate them into ElementForType object one by one for (Element viewElelment : viewElelments) { //transformation VariableElement variableElement = (VariableElement) viewElelment; //Get its previous node. The previous node of the member variable node is the class node TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement(); //Get the value corresponding to the class node in the map ElementForType elementForType = map.get(typeElement); List<VariableElement> viewElements; //If this class node already exists in the map if(elementForType !=null){ //Gets the collection of control nodes in the value corresponding to the class node viewElements = elementForType.getViewElements(); //If the collection is empty, a new collection is created and placed in the value corresponding to the class node if(viewElements == null){ viewElements = new ArrayList<>(); elementForType.setViewElements(viewElements); } }else{ //If elementForType is empty, a new one is created elementForType = new ElementForType(); //Also create a new collection of control nodes viewElements = new ArrayList<>(); elementForType.setViewElements(viewElements); if(!map.containsKey(typeElement)){ map.put(typeElement,elementForType); } } //Finally, the object of the node traversed by the control is placed in the node encapsulation class of the control viewElements.add(variableElement); } //Traverse the nodes of all click event methods and encapsulate them in objects for (Element methodElement : methodElements) { ExecutableElement executableElement = (ExecutableElement) methodElement; TypeElement typeElement = (TypeElement) executableElement.getEnclosingElement(); ElementForType elementForType = map.get(typeElement); List<ExecutableElement> executableElements; logUtil(elementForType+""); if(elementForType !=null){ executableElements = elementForType.getMethodElements(); if(executableElements == null){ executableElements = new ArrayList<>(); elementForType.setMethodElements(executableElements); } }else{ elementForType = new ElementForType(); executableElements = new ArrayList<>(); elementForType.setMethodElements(executableElements); if(!map.containsKey(typeElement)){ map.put(typeElement,elementForType); } } executableElements.add(executableElement); } //Traverse member variable nodes and encapsulate for (Element viewElelment : stringElements) { //transformation VariableElement variableElement = (VariableElement) viewElelment; //Get its previous node. The previous node of the member variable node is the class node TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement(); //Get the value corresponding to the class node in the map ElementForType elementForType = map.get(typeElement); List<VariableElement> viewElements; //If this class node already exists in the map if(elementForType !=null){ //Gets the collection of control nodes in the value corresponding to the class node viewElements = elementForType.getStringElements(); //If the collection is empty, a new collection is created and placed in the value corresponding to the class node if(viewElements == null){ viewElements = new ArrayList<>(); elementForType.setStringElements(viewElements); } }else{ //If elementForType is empty, a new one is created elementForType = new ElementForType(); //Also create a new collection of control nodes viewElements = new ArrayList<>(); elementForType.setStringElements(viewElements); if(!map.containsKey(typeElement)){ map.put(typeElement,elementForType); } } //Finally, the object of the node traversed by the control is placed in the node encapsulation class of the control viewElements.add(variableElement); } return map; } /** * Method to get package name * @param typeElement */ public String getPackageName(Element typeElement){ //Get package name PackageElement packageOf = processingEnv.getElementUtils().getPackageOf(typeElement); Name qualifiedName = packageOf.getQualifiedName(); return qualifiedName.toString(); } public void logUtil(String message){ Messager messager = processingEnv.getMessager(); messager.printMessage(Diagnostic.Kind.NOTE,message); } /** * Gets the method of the assembly statement of the class * @param packageName * @param newClazzName * @param typeElement * @param elementForType * @return */ public StringBuffer getStringBuffer(String packageName,String newClazzName, TypeElement typeElement,ElementForType elementForType ) { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("package " + packageName + ";\n"); stringBuffer.append("import android.view.View;\n"); stringBuffer.append("public class " + newClazzName + "{\n"); stringBuffer.append("public " + newClazzName + "(final " + typeElement.getQualifiedName() + " target){\n"); if (elementForType != null && elementForType.getViewElements() != null && elementForType.getViewElements().size() > 0) { List<VariableElement> viewElements = elementForType.getViewElements(); for (VariableElement viewElement : viewElements) { //Get type TypeMirror typeMirror = viewElement.asType(); //Gets the name of the control Name simpleName = viewElement.getSimpleName(); //Get resource ID int resId = viewElement.getAnnotation(BindView.class).value(); stringBuffer.append("target." + simpleName + " =(" + typeMirror + ")target.findViewById(" + resId + ");\n"); } } if (elementForType != null && elementForType.getMethodElements() != null && elementForType.getMethodElements().size() > 0) { List<ExecutableElement> methodElements = elementForType.getMethodElements(); for (ExecutableElement methodElement : methodElements) { int[] resIds = methodElement.getAnnotation(OnClick.class).value(); String methodName = methodElement.getSimpleName().toString(); for (int resId : resIds) { stringBuffer.append("(target.findViewById(" + resId + ")).setOnClickListener(new View.OnClickListener() {\n"); stringBuffer.append("public void onClick(View p0) {\n"); stringBuffer.append("target." + methodName + "(p0);\n"); stringBuffer.append("}\n});\n"); } } } if (elementForType != null && elementForType.getStringElements() != null && elementForType.getStringElements().size() > 0) { List<VariableElement> variableElements = elementForType.getStringElements(); for (VariableElement variableElement : variableElements) { //Get type TypeMirror typeMirror = variableElement.asType(); //Gets the name of the control Name simpleName = variableElement.getSimpleName(); //Get resource ID int resId = variableElement.getAnnotation(BindString.class).value(); stringBuffer.append("target." + simpleName + " =(" + typeMirror + ")target.getBaseContext().getResources().getString(" + resId + ");\n"); stringBuffer.append("}\n}\n"); } // } return stringBuffer; } return stringBuffer; } }
ElementForType
package com.example.process; import java.util.List; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; public class ElementForType { //A collection of all nodes bound to the View's member variables List<VariableElement> viewElements; //A collection of nodes for all click time methods List<ExecutableElement> methodElements; List<VariableElement> stringElements; public List<VariableElement> getStringElements() { return stringElements; } public void setStringElements(List<VariableElement> stringElements) { this.stringElements = stringElements; } public List<VariableElement> getViewElements() { return viewElements; } public void setViewElements(List<VariableElement> viewElements) { this.viewElements = viewElements; } public List<ExecutableElement> getMethodElements() { return methodElements; } public void setMethodElements(List<ExecutableElement> methodElements) { this.methodElements = methodElements; } }
It can be seen that butterknife has done a lot for us. The above code is very different from the real butterknife. Butterknife is generally not used for component development because it will lead to the repetition of control Id.