徒手擼一個框架-ButterKnife三步走

實現目標:

  1. 經過 @BindView() 實現ID自動綁定
  2. 經過 @OnClick() 實現點擊事件自動綁定

ButterKnife簡易原理解析:

  1. 經過 @BindView()@OnClick() 註解得到控件的ID及類型
  2. 拿到控件ID後自動生成一個以 XXActivity_ViewBinding 格式爲文件名的java文件
  3. 在生成 XXActivity_ViewBinding 的java文件(類文件)的同時,聲明一個 bind(Activity target) 方法並寫入如下語句:
    target.textView=(WidgetType)fb(viewId)fb(viewId).setOnClickListener()
  4. 經過 ButterKnife.bind(Activity activity) 傳入 Activity 實例並 執行 步驟四中的語句

步驟分析

經過以上的分析咱們就能夠明確如下 極簡 步驟:java

  1. 聲明兩個註解 @BindView()@OnClick()
  2. 實現註解處理器
  3. 執行註解處理器生成的類文件中的方法

開始擼碼

碼前準備,環境配置

  1. 新建兩個module,分別命名爲annotations(用來聲明註解)、annotation_compiler(用來處理註解)
  2. module - annptation_compiler 下的 build.gradle 中添加註解處理依賴包
    (別問,問就是Google出的,用來處理註解的,AS3.4.1如下只須要compileOnly一行)
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
   compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
複製代碼

順手Sync下android

  1. module 之間相互依賴
    module(app) 依賴
implementation project(path: ':annotations')
   annotationProcessor project(path: ':annotation_compiler')
複製代碼

annotation_compiler module 依賴數組

implementation project(path: ':annotations')
複製代碼

準備工做到此結束bash

工程目錄

先看結構再看碼app

實現步驟1-聲明註解

annotation module 中新建兩個註解類
ide

分別實現如下方法:
綁定控件的註解是一個註解對應一個控件,因此每次只獲取一個控件Id,註解接收一個int值

綁定點擊事件的註解須要接收多個控件的Id,因此註解接收一個int[]

備註:gradle

  1. 注意interface關鍵字前面的 @ 註解標識
  2. @Target :即明確該註解做用在哪一種對象上面( eg: ElementType.METHOD - 註解做用在方法上,ElementType.FIELD - 註解做用在成員變量上)
  3. @Rentention :即明確該註解在什麼時候生效( eg: RetentionPolicy.SOURCE - 源碼期,RetentionPolicy.RUNTIME - 運行時)
  4. default 缺省值:-1=View.NoId

實現步驟2-註解處理

捋一下這裏的思路

每個帶有 @BindView@OnClick 註解聲明的Activity類都要相應的生成一個XXActivity_ViewBinding類,並在類中聲明一個 bind(Activity target) 方法,該方法可以接收 不一樣的Activity對象ui

  1. 聲明一個接口類
    因爲每一個自動生成的XXActivity_ViewBinding類中都有一個bind(Activity target) 方法,因此咱們抽象一個接口出來並最終讓XXActivity_ViewBinding類實現該接口,而且利用泛型來保證每一個實現該接口的方法都能接收不一樣的Activity對象module(app) 下新建一個Binder接口: 這裏的T將會用來 傳遞Activity對象 ,這樣咱們就能夠在 bind() 方法裏輕鬆調用 fb()setOnClcikListner()
  2. 建立註解處理類

  註解處理類的功能纔是真正用來進行註解處理而且建立咱們須要的java類文件的
   a. 在module(annotation_compiler)下新建AnnotationComPiler類google

@AutoService(Processor.class)  //註冊註解處理器,此處用到環境配置中依賴的AutoService庫
public class AnnotationCompiler extends AbstractProcessor {}
複製代碼

  b. 聲明Filer對象
  注意:Filer對象配合Writer對象能夠建立一個有內容的文件spa

//聲明文件對象(成員變量)
 private Filer filer;

 @Override
 public synchronized void init(ProcessingEnvironment processingEnvironment) {
     super.init(processingEnvironment);
     //就是這麼寫的,別問
     filer = processingEnvironment.getFiler();
 }
複製代碼

  c. 重寫getSupportedAnnotationTypes(),篩選咱們的目標註解

/** * 聲明註解處理器要處理的註解 */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>();
        //getCanonicalName()獲取的是包名+類名
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }
複製代碼

  d. 重寫getSupportedSourceVersion(),聲明支持的java版本
  這裏咱們用默認就能夠

/** * 聲明註解處理器支持的java版本 */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        //processingEnv這是父類變量
        return processingEnv.getSourceVersion();
    }  
複製代碼

  d. 重寫process(),生成XXActivity_ViewBinding類文件
  首先看一下咱們須要的XXActivity_ViewBinding類的樣子

//聲明包路徑
package com.junt.annotationdemo;
//因爲實現了Binder接口,因此要聲明導入Binder
import com.junt.annotationdemo.Binder;
//點擊事件須要實例化View.OnClickListener()接口,因此還要聲明導入View包
import android.view.View;

public class MainActivity_ViewBinding implements Binder<com.junt.annotationdemo.MainActivity>{
    @Override
    public void bind(final com.junt.annotationdemo.MainActivity target) {
    //首先對於即用到了@BindView又用到了@OnClick的控件,咱們在生成代碼時應該須要如下內容
    //textView-說明咱們須要獲取控件名,
    //(android.widget.TextView)-說明咱們還須要知道控件的類型
    //2131165319-控件的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();
                }
            }
        });
    //其次,對於僅使用了@OnClick的控件,僅須要一個Id便可
        target.findViewById(2131165320).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    target.onClick(view);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
}
複製代碼

  在生成的java類文件中寫入代碼說明
好比咱們須要寫入

package com.junt.annotationdemo;
複製代碼

只須要調用

writer.write("package com.junt.annotationdemo;\n")
複製代碼

所有流程代碼

/**
     * 寫一個自動findViewById的文件
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    
        //獲取全部用到@BindView的節點元素
        Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        
        //根據節點所在的Activity進行分類(一個Activity中會有多個@BindBiew),便於接下來建立每個單獨的XXActivity_ViewBinding類文件
        Map<String, List<VariableElement>> mapBindView = new HashMap<>();
        for (Element element : elementsAnnotatedWith) {
            //獲取控件元素
            VariableElement variableElement = (VariableElement) element;
            //獲取控件元素的父元素Name(activity或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);
        }

        //獲取全部用到@onClick的節點
        Set<? extends Element> elementsAnnotatedWithClick=roundEnvironment.getElementsAnnotatedWith(OnClick.class);
        
        //一樣根據Activity進行分類(一個Activity中僅有一個@OnClick)
        Map<String, Element> mapClickView = new HashMap<>();
        for (Element element : elementsAnnotatedWithClick) {
            String activityName = element.getEnclosingElement().getSimpleName().toString();
            mapClickView.put(activityName, element);
        }

        //開始建立XXActivity_ViewBinding類文件
        Writer writer = null;
        //用For循環,每一次循環寫一個xxActivity_ViewBinding
        for (String activityName : mapBindView.keySet()) {  
         
            //取出activityName下的帶註解控件元素
            List<VariableElement> variableElements = mapBindView.get(activityName);
            
            //獲取activity所在包名(同一個Activity下的元素的父元素都是該Activity,因此任意取一個就行,這裏取0)
            Element enclosingElement = variableElements.get(0).getEnclosingElement();
            String packageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString();
            
            try {
                //實例化一個JavaFileObject對象(咱們寫的是.java文件--類文件)
                JavaFileObject sourceFile = filer.createSourceFile(
                        packageName + "." + activityName + "_ViewBinding");
                //實例化writer對象用來寫入向.java文件中寫入代碼
                writer = sourceFile.openWriter();
                
                //1.寫入-導包代碼
                writer.write("package " + packageName + ";\n");
                writer.write("import " + packageName + ".Binder;\n");
                writer.write("import android.view.View;\n");
                
                //2.寫入-聲明類及實現接口代碼
                writer.write("public class " + activityName + "_ViewBinding implements Binder<"
                        + packageName + "." + activityName + ">{\n");
                        
                //3.寫入-實現接口方法代碼
                writer.write(" @Override\n" +
                        " public void bind(final " + packageName + "." + activityName + " target) {\n");
                        
                //取出帶onCLick註解的元素Id
                Element clickVariableElement = mapClickView.get(activityName);
                int[] clickIds = clickVariableElement.getAnnotation(OnClick.class).value();
                List<Integer> id = new ArrayList<>(clickIds.length);

                //4.寫入-全部控件findViewById代碼
                for (VariableElement variableElement : variableElements) {
                    //獲取控件Name
                    String variableName = variableElement.getSimpleName().toString();
                    //獲取控件ID
                    int variableId = variableElement.getAnnotation(BindView.class).value();

                    //獲取控件Type
                    TypeMirror typeMirror = variableElement.asType();
                    //寫FindViewById
                    writeFindViewById(writer, variableName, typeMirror, variableId);
                    //同時看看這個控件元素有沒有@OnClick註解,有的話同時寫setOnClickListener
                    if (contains(clickIds, variableId)) {
                        //已經設置過點擊事件的全部控件Id
                        id.add(variableId);
                        writeSetOnClickListener(writer, packageName, activityName, variableName);
                    }
                }

                for (int clickId : clickIds) {
                    //若是id集合中沒有這個clickId則說明這個空間須要設置點擊事件,但還沒設置點擊事件
                    if (!id.contains(clickId)) {
                        writeSetOnClickListenerWithoutName(writer, packageName, activityName, clickId);
                    }
                }
                //5.寫入-補全類的構造
                writer.write(" \n }\n}\n");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
               //這個Activity已經設置過Id綁定與點擊事件、故移除
                mapClickView.remove(activityName);

                if (writer != null) {
                    try {
                        writer.close();
                        writer = null;
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        //處理Activity中僅有@OnClick綁定的狀況,上面的步驟僅是處理了全部含有@BindView的Activity(點擊事件也同時處理而且從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.寫入-導包代碼
                writer.write("package " + packageName + ";\n");
                writer.write("import " + packageName + ".Binder;\n");
                writer.write("import android.view.View;\n");
                
                //2.寫入-聲明類及實現接口代碼
                writer.write("public class " + activityName + "_ViewBinding implements Binder<"
                        + packageName + "." + activityName + ">{\n");
                
                //3.寫入-實現接口方法代碼
                writer.write(" @Override\n" +
                        " public void bind(final " + packageName + "." + activityName + " target) {\n");
                
                //取出改Activity下全部@OnClick的Id
                int[] clickViewIds = element.getAnnotation(OnClick.class).value();
                
                //4.寫入-setOnClickListener代碼
                for (int clickViewId : clickViewIds) {
                    writeSetOnClickListenerWithoutName(writer, packageName, activityName, clickViewId);
                }
                
                //5.補全類構造
                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;
    }
    
    
    /**
     * 寫入findViewById代碼
     * @param writer writer
     * @param variableName 控件名
     * @param typeMirror 控件類型
     * @param variableId 控件Id
     * @throws IOException 寫入異常
     */
    private void writeFindViewById(Writer writer, String variableName, TypeMirror typeMirror, int variableId) throws IOException {
        writer.write(" target." + variableName + "=(" + typeMirror + ")target.findViewById(" + variableId + ");\n");
    }

    /**
     * 已經調用過finfViewById()方法的控件設置點擊事件
     * @param writer writer
     * @param variableName 控件名
     * @throws IOException 寫入異常
     */
    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");
    }

    /**
     * 沒有調用過findViewById()方法的控件設置點擊事件
     * @param writer writer
     * @param viewId 控件Id
     * @throws IOException 寫入異常
     */
    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");
    }

    /**
     * 判斷數組中是否含有某個值
     * 這裏用來判斷,全部設置@OnClick註解的控件Id中是否也同時設置了@BindView
     * @param arr 某一個Activity下全部設置了@OnClick的控件Id
     * @param arg 某一個Activity下設置了@BindView的控件Id
     * @return 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;
    }
複製代碼

實現步驟3-執行XXActivity_ViewBinding中的onBind()方法

module(app)新建一個ButterKnife類

在Activity中使用

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息