Android 自定義註解詳細用法,手寫Butterknife黃油刀

前言

本篇文章主要講解 Java 註解在Android中的常見用法html

Java 註解(Annotation)

Java 註解(Annotation)又稱 Java 標註,是 JDK5.0 引入的一種註釋機制。 Java 語言中的類、方法、變量、參數和包等均可以被標註。和 Javadoc 不一樣,Java 標註能夠經過反射獲取標註內容。在編譯器生成類文件時,標註能夠被嵌入到字節碼中。Java 虛擬機能夠保留標註內容,在運行時能夠獲取到標註內容 。 固然它也支持自定義 Java 標註。java

內置的註解

Java 定義了一套註解,共有 7 個,3 個在 java.lang 中,剩下 4 個在 java.lang.annotation 中。git

做用在代碼的註解是

  • @Override - 檢查該方法是不是重寫方法。若是發現其父類,或者是引用的接口中並無該方法時,會報編譯錯誤。
  • @Deprecated - 標記過期方法。若是使用該方法,會報編譯警告。
  • @SuppressWarnings - 指示編譯器去忽略註解中聲明的警告。

做用在其餘註解的註解(或者說 元註解,自定義註解核心)是:

  • @Retention - 標識這個註解怎麼保存,是隻在代碼中,仍是編入class文件中,或者是在運行時能夠經過反射訪問。
  • @Documented - 標記這些註解是否包含在用戶文檔中。
  • @Target - 標記這個註解應該是哪一種 Java 成員。
  • @Inherited - 標記這個註解是繼承於哪一個註解類(默認 註解並無繼承於任何子類)

註解的組成部分

Annotation.java

package java.lang.annotation;
public interface Annotation {

    boolean equals(Object obj);

    int hashCode();

    String toString();

    Class<? extends Annotation> annotationType();
}
複製代碼

ElementType.java

package java.lang.annotation;

public enum ElementType {
    TYPE,               /* 類、接口(包括註釋類型)或枚舉聲明 */

    FIELD,              /* 字段聲明(包括枚舉常量) */

    METHOD,             /* 方法聲明 */

    PARAMETER,          /* 參數聲明 */

    CONSTRUCTOR,        /* 構造方法聲明 */

    LOCAL_VARIABLE,     /* 局部變量聲明 */

    ANNOTATION_TYPE,    /* 註釋類型聲明 */

    PACKAGE             /* 包聲明 */
}
複製代碼

RetentionPolicy.java

package java.lang.annotation;
public enum RetentionPolicy {
    SOURCE,            /* Annotation信息僅存在於編譯器處理期間,編譯器處理完以後就沒有該Annotation信息了 */

    CLASS,             /* 編譯器將Annotation存儲於類對應的.class文件中。默認行爲 */

    RUNTIME            /* 編譯器將Annotation存儲於class文件中,而且可由JVM讀入 */
}
複製代碼

更多基本知識點參考,有更全面的,這裏就不過多描述了 Java 註解詳細講解以及基礎語法 github

話很少說,先上代碼,再逐一解釋:

  • MainActivity
@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

    @ViewInject(R.id.text_welcome)
    TextView text_welcome;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ViewUntil.inject(this);
        initData();
    }

    private void initData() {
        text_welcome.setText("我是經過註解後成功修改的值");
    }
}
複製代碼

總結:這個 MainActivity 沒啥好講解的,相似於黃油刀用法,使用了@ContentView 和 @ViewInject 這倆個自定義註解分別獲得對應的佈局和對應的控件,並最後,修改了 text_welcome 的顯示文本面試

  • ContentView
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ContentView {
    int value();
}
複製代碼

總結:這裏使用了 @Target, @Retention, @interface 這三個關鍵代碼,如今分別對這三個進行講解markdown

  1. @Retention(RetentionPolicy.RUNTIME)

它的做用是指定 Deprecated 的策略是 RetentionPolicy.RUNTIME。這就意味着,編譯器會將Deprecated 的信息保留在 .class 文件中,而且能被虛擬機讀取【參考上面註解的組成部分】 2. @Target(ElementType.TYPE) 它的做用是指定該 Annotation 的類型是 ElementType.TYPE。這就意味着,ContentView 是來修飾"類、接口(包括註釋類型)或枚舉聲明"的註解。【參考上面註解的組成部分】 3. @interface 它的做用是實現了 java.lang.annotation.Annotation 接口,即該註解就是一個Annotation。 定義 Annotation 時,@interface 是必須的。ide

  • ViewInject
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ViewInject {
    int value();
}

複製代碼

總結:上面的 @Retention 與 @interface 已經說了,而 @Target(ElementType.FIELD),參考上面註解的組成部分oop

  • ViewUntil
public class ViewUntil {

    public static void inject(Object context) {
        injectLayout(context);
        injectView(context);
    }


    private static void injectView(Object context) {
    	//獲取對應的綁定對象
        Class<?> aClass = context.getClass();
        //獲取對應綁定對象的全部成員屬性
        Field[] fields = aClass.getDeclaredFields();
        for (Field field : fields) {
        	//獲得每個 被註解綁定的控件
            ViewInject viewInject = field.getAnnotation(ViewInject.class);
			//固然存在有些成員屬性,沒有被註解綁定,因此這裏跳過該變量便可
            if (viewInject == null) {
                continue;
            }
            //獲得每個被綁定的控件id
            int valueId = viewInject.value();
            try {
            //經過反射,拿到綁定對象的 findViewById 方法名
                Method method = aClass.getMethod("findViewById", int.class);
                //拿到對應方法名後,調用對應方法
                View view = (View) method.invoke(context, valueId);
// 將 view btn 產生聯繫
                field.setAccessible(true);
                field.set(context, view);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static void injectLayout(Object context) {
        int layouyId = 0;
        Class<?> clazz = context.getClass();
        ContentView contentView = clazz.getAnnotation(ContentView.class);
        if (contentView == null) {
            return;
        }
        //拿到註解裏面的設置的佈局id
        layouyId = contentView.value();

        try {
            // 經過反射拿到 setContentView 方法,而後調用該方法時,將註解獲得的佈局id,傳入進去,從而實現 加載佈局的功能
            Method contentMethod = context.getClass().getMethod("setContentView", int.class);
            contentMethod.invoke(context, layouyId);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

複製代碼

總結:該類目前有三個方法:inject,injectView,injectLayout佈局

  1. inject:該方法暴露給外部對象綁定使用
  2. injectLayout:初始化綁定對象對應的佈局
  3. injectView:初始化綁定對象對應的每一個控件

其餘的註釋寫的很全面,想必應該能看懂吧。post

運行效果

就這麼簡單

回顧

到這,咱們實現了佈局、控件的綁定。但並無實現控件的事件綁定(點擊、長按)相關的。 接下來的內容涉及到動態代理知識相關,建議先看完 Android 靜態代理與動態代理詳解

自定義註解進階用法(控件事件的綁定)

在開始以前,咱們先整理一下需求。控件的事件綁定,分爲點擊事件、長按事件。 一個控件可能會有多個事件; 一個事件也可能會綁定多個控件; 徹底是多對多的關係! 因而乎咱們須要自定義屬於本身的註解(@EventBase), 用來修飾對應的點擊事件(@OnClick)和長按事件(@OnLongClick)

@EventBase

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface EventBase {
//setOnLongClickListener
    String listenerSetter();
    /** * 事件監聽的類型 * @return * OnLongClickListener.class */
    Class<?> listenerType();

    /** * 事件被觸發以後,執行的回調方法的名稱 * @return * * onLongClick */
    String callbackMethod();

}
複製代碼

總結:這裏 @Target(ElementType.ANNOTATION_TYPE) 它的做用是指定 Inherited 的類型是 ANNOTATION_TYPE。這就意味着,@Inherited 只能被用來標註 "Annotation 類型"

@OnClick

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@EventBase(listenerSetter="setOnClickListener",listenerType = View.OnClickListener.class,callbackMethod ="onClick" )
public @interface OnClick{
    int[] value();
}

複製代碼

總結:這裏注意,多了一個 @EventBase 來修飾 OnClick 註解,表示 OnClick 對應類型,以及對應出發的方法都和系統的點擊事件一一對應了起來。

@OnLongClick

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@EventBase(listenerSetter="setOnLongClickListener",listenerType = View.OnLongClickListener.class,callbackMethod ="onLongClick" )
public @interface OnLongClick {
    int[] value();
}
複製代碼

總結:同上, @EventBase 修飾的 OnLongClick 註解,和系統的長按事件一一對應了起來。

注:這裏的長按和點擊對應的註解都用到了 @EventBase 註解,這裏可能會被誤認爲,這個似於類的繼承,擁有了父類的公開的內容。但千萬不要往這方面想。註解是不能被繼承的,只能拿來修飾其餘註解,這也是面試官很是喜歡問的一點。

繼續貼代碼 最新的ViewUntil

public class ViewUntil {

    public static void inject(Object context) {
        injectLayout(context);
        injectView(context);
        injectClick(context);
    }

    private static void injectClick(Object context) {
        //拿到綁定的對象
        Class<?> clazz = context.getClass();
        //獲取綁定對象全部的方法
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                Class<?> annotionType = annotation.annotationType();
                EventBase eventBase = annotionType.getAnnotation(EventBase.class);
                if (eventBase == null) {
                    continue;
                }
// 拿到事件三要素
                /** * setOnLongClickListener */
                String listenerSetter = eventBase.listenerSetter();
                /** * OnLongClickListener.class */
                Class<?> listenerType = eventBase.listenerType();
                /** * 事件被觸發以後,執行的回調方法的名稱 * @return * * onLongClick */
                String callBackMethod = eventBase.callbackMethod();
                try {
                    Method valueMethod = annotionType.getDeclaredMethod("value");
                    int[] viewId = (int[]) valueMethod.invoke(annotation);
                    for (int id : viewId) {
// 有多少個按鈕 id ---》findviewById View.setOnClickerListener
                        Method findViewById = clazz.getMethod("findViewById", int.class);
                        View view = (View) findViewById.invoke(context, id);
                        ListenerInvocationHandler listenerInvocationHandler = new ListenerInvocationHandler(context, method);
                        Method setListener = view.getClass().getMethod(listenerSetter, listenerType);
                        Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader(),
                                new Class[]{listenerType}, listenerInvocationHandler);
                        setListener.invoke(view, proxy);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            /** * btn button ====> value R.id.btn ===>findviewbyid * btn.setOnClickListener(new View.OnClickListener() { * @Override * public void onClick(View v) { * * } * }); */
// setOnClickListener Method 設置
//

        }


    }


    private static void injectView(Object context) {
        Class<?> aClass = context.getClass();
        Field[] fields = aClass.getDeclaredFields();
        for (Field field : fields) {
            ViewInject viewInject = field.getAnnotation(ViewInject.class);
            if (viewInject == null) {
                continue;
            }
            int valueId = viewInject.value();
            try {
                Method method = aClass.getMethod("findViewById", int.class);
                View view = (View) method.invoke(context, valueId);
// view btn 產生聯繫
                field.setAccessible(true);
                field.set(context, view);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static void injectLayout(Object context) {
        int layouyId = 0;
        Class<?> clazz = context.getClass();
        ContentView contentView = clazz.getAnnotation(ContentView.class);
        if (contentView == null) {
            return;
        }
        //拿到註解裏面的設置的佈局id
        layouyId = contentView.value();

        try {
            // 經過反射拿到 setContentView 方法,而後調用該方法時,將註解獲得的佈局id,傳入進去,從而實現 加載佈局的功能
            Method contentMethod = context.getClass().getMethod("setContentView", int.class);
            contentMethod.invoke(context, layouyId);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}
複製代碼

總結:這裏也沒啥可說的,只要理解了上面註解的使用,以及動態代理使用,就很是容易明白。 剛開始先拿到 對應方法 的 EventBase 註解,而後獲得註解裏面的事件三要素,接着經過反射找到對應的控件,最後將事件三要素以及對應控件經過動態代理的方式 實現了點擊、長按事件。

MainActivity

@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

    @ViewInject(R.id.text_welcome)
    TextView text_welcome;


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ViewUntil.inject(this);
        initData();
    }

    private void initData() {
        text_welcome.setText("我是經過註解後成功修改的值");
    }

    @OnLongClick({R.id.btn_one, R.id.btn_two})
    public boolean onLongClick(View view) {
        switch (view.getId()) {
            case R.id.btn_one:
                Toast.makeText(this, "你在長按按鈕----------》1", Toast.LENGTH_LONG).show();
                break;
            case R.id.btn_two:
                Toast.makeText(this, "你在長按按鈕----------》2", Toast.LENGTH_LONG).show();
                break;
        }
        return false;
    }


    @OnClick({R.id.btn_one, R.id.btn_two})
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_one:
                Toast.makeText(this, "你在點擊按鈕----------》1", Toast.LENGTH_LONG).show();
                break;
            case R.id.btn_two:
                Toast.makeText(this, "你在點擊按鈕----------》2", Toast.LENGTH_LONG).show();
                break;
        }
    }

}
複製代碼

ListenerInvocationHandler

//處理器
public class ListenerInvocationHandler implements InvocationHandler {
    private Object context;
    private Method activityMethod;

    private  final String TAG = "Handler";

    public ListenerInvocationHandler(Object context, Method activityMethod) {
        this.context = context;
        this.activityMethod = activityMethod;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Log.i(TAG, "invoke: ----調用前");
        return activityMethod.invoke(context, args);
    }
}
複製代碼

Demo地址:點我下載

相關文章
相關標籤/搜索