《擼代碼學習 IOC注入技術2》—— 事件注入

不詩意的女程序媛不是好廚師~
轉載請注明出處,From李詩雨---https://blog.csdn.net/cjm2484836553/article/details/104581855

源代碼下載地址:https://github.com/junmei520/iocStudy
在這里插入圖片描述

在上一篇 的文章 《擼代碼 學習 IOC注入技術1 》—— 布局注入 與 控件注入中,我們已經自己通過敲代碼,一步一步實現了,運行時注入的---布局注入和控件注入。那么今天,我將來繼續敲代碼,來一步一步實現,事件的注入。

先來看一下我要達到的效果:


在這里插入圖片描述

即:我想通過這兩句代碼,就實現點擊事件。

根據上一篇中我們講的 布局注入 和 控件注入 的經驗,大家對于實現我們今天的 事件注入 有沒有什么想法和思路呢?

對!我們還是要自己造個女朋友InjectUtils,然后在BaseActivity中就進行注入。

那接下來呢?接下來繼續要怎么做?你會不會是這樣想的:


在這里插入圖片描述

你是不是想:"那還不簡單嗎?和之前的類似啊,先自定義一個注解OnClick,然后具體實現InjetUtils中的injectEvent()方法呀!"

那我有要問:"那你打算具體怎么實現injectEvent()呢?"

你是不是還會像這樣回答:“當然主要還是通過反射啦,①先獲取activity的所有方法;②再獲取方法上的 OnClick 注解,進而得到注解后面的id ,然后得到button;③最后反射執行 btn1.setOnClickListener(new View.OnClickListener() {...}巴拉巴拉巴拉...”

在這里插入圖片描述

emmm... 我想說,你這么想其實也沒有什么大問題,就是有點,emmm...,有點太low啦。因為如果你這樣做的話,那就是把代碼寫死了呀~~~

比如說,如果我還想加個長按事件呢,像這樣:


在這里插入圖片描述

你可能會說,那我就改injectEvent()代碼呀!

emmm...我忍!那如果我再繼續增加幾個事件呢?你還打算繼續改injectEvent()的內部代碼嗎?還打算增加許多的if/else或很多的謎之縮進嗎???你自己體會一下~~~

顯然這樣把代碼寫死是不可行的,那我們怎么樣才能使得我們自己的代碼變得靈活呢?

那我們就必須尋找不同事件的共同點了,然后把相同點抽取出來。

讓我們再用新的眼光來審視一下短按和長按事件:


在這里插入圖片描述

我們可以總結出,所有的事件都具有三要素:

  • 1.事件源
  • 2.事件
  • 3.事件的處理
  • 最后還要進行訂閱(訂閱關系)

既然知道了這一點,那我們再自定義注解OnClick的時候就可以把這三要素也加進去了。

下面我們就來正式的講講正確的思路了:

首先我們先自定義一個注解BaseEvent,它可以用來接受事件的三要素信息,并且將來會把它用在OnClick注解的身上:

@Target(ElementType.ANNOTATION_TYPE) //該注解是用在自定義注解上的
@Retention(RetentionPolicy.RUNTIME)  //可以保留到程序運行時
public @interface BaseEvent {
    Class<?> enventType();  //事件 ---> 即相當于 new View.OnClickListener()

    String setterMethod(); //訂閱關系 ---> 即相當于 setOnClickListener()

    String callbackMethod(); // 事件回調方法 ---> 即相當于 onClick()

}

然后我們再來定義OnClick注解,它的頭上使用了BaseEvent注解,并傳入三要素。

@Target(ElementType.METHOD) //該注解是用在方法上的
@Retention(RetentionPolicy.RUNTIME) //該注解可以保持到程序運行時
@BaseEvent(enventType = View.OnClickListener.class,
        setterMethod = "setOnClickListener",
        callbackMethod = "onClick")
public @interface OnClick {
    int[] value() default -1; //由于可能是多個id,所以此處要用數組來接收
}

使用的時候就這樣:

@OnClick({R.id.button1, R.id.button2})
public void click(View view) {
    //...具體操作...
}

如果要再加入其他的事件,也很好辦,比如我要再加一個長按事件,那我就只要多增加一個OnLongClick的注解就可以了,它的地方都不用做任何的修改。其實,這就是我們所說的 注解的多態。

//增加一個長按事件
@OnLongClick({R.id.button1, R.id.button2})
public boolean longClick(View view) {
    //...具體操作...
    return false;
}
//只要多增加一個自定義的注解就可以了,傳入具體的事件三要素。
@Target(ElementType.METHOD) //該注解是用在方法上的
@Retention(RetentionPolicy.RUNTIME) //該注解可以保持到程序運行時
@BaseEvent(enventType = View.OnLongClickListener.class,
        setterMethod = "setOnLongClickListener",
        callbackMethod = "onLongClick")
public @interface OnLongClick {
    int[] value() default -1; //由于可能是多個id,所以此處要用數組來接收
}

一些小說明:

1.由于我們在使用OnClick注解時傳入了控件的id, 所以在自定義BaseEvent注解時,事件源就沒有必要再傳進去了。

2.由于在使用OnClick注解時,可能傳入的是多個控件的id, 所以自定義OnClick注解時,要用int[]數組來接收。

好了,完成了這些鋪墊工作,下面就讓我們來集中精力實現InJectUtils中的injectEvent()方法吧~

首先,我們來思考一下,我們需要做哪些事情呢?


在這里插入圖片描述

我們來分析一下,首先由于我們的OnClick注解是用在方法上的,所以

  • 第一步,就是要獲取activity上的所有方法。(目的是為了可以找到使用了OnClick注解的click()方法)

  • 第二步,我們要對所有的注解進行遍歷,獲取每一個方法上的所有注解。(通過這一步我們可以獲取click()上的OnClick注解)

  • 第三步,我們要拿到注解類型對應的Class,通過Class去找,看看有沒有BaseEvent,如果有則說明這個方法就是事件方法。(通過這一步我們可以得到OnClick對應注解類型的Class,從而進一步找到BaseEvent,并且可以確定click()就是事件方法)

  • 第四步,從BaseEvent中拿到事件的三要素。

    (①通過enventType()-->得到事件:即相當于 new View.OnClickListener();

    ②通過setterMethod()-->得到訂閱關系:即相當于 setOnClickListener();

    ③通過callbackMethod()-->得到事件回調方法:即相當于 onClick())

  • 第五步,接下來就是反射執行

    btn1.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View view) {
    
         }
     });
    

    了。

我們來看一下代碼實現:

 private static void injectEvent(Object context) {
        Class<?> clazz = context.getClass();
        //1.獲取該activity上的所有方法
        Method[] methods = clazz.getDeclaredMethods();

        //2.循環遍歷方法,拿到每一個方法上的所有注解
        for (Method method : methods) {
            Annotation[] annotations = method.getAnnotations();
            //3.循環遍歷注解,拿到注解類型對應的Class,通過class去找,看看有沒有BaseEvent
            for (Annotation annotation : annotations) {
                //拿到注解類型對應的Class
                Class<?> annotationClass = annotation.annotationType();
                //通過Class去找,看看有沒有BaseEvent
                BaseEvent baseEvent = annotationClass.getAnnotation(BaseEvent.class);

                //如果沒有BaseEvent,則表示當前方法不是一個事件處理的方法
                if (baseEvent == null) {
                    continue;
                }

                //4.如果有BaseEvent,則表示是事件處理的方法,那我們就去拿事件的三要素
                //拿到三要素
                Class<?> eventType = baseEvent.enventType();
                String setterMethodStr = baseEvent.setterMethod();
                String callbackMethod = baseEvent.callbackMethod();

                //5.接下來我們要反射執行
//                   btn1.setOnClickListener(new View.OnClickListener() {
//                        @Override
//                        public void onClick(View view) {
//
//                        }
//                    });
            }
        }
    }

關于第五步,反射執行btn.setOnClickListener(new View.OnClickListener(){ public void onClick()}),我們要單獨提出來分析一下。

在這里插入圖片描述
  • 1.首先我們需要拿到事件源,(即對應的view控件,此處即指btn按鈕)。

    那我們要怎么做呢?對,首先我們要拿到控件id.

    ?那控件id要怎么樣才能拿到呢?我們是不是在上面已經拿到了注解類型對應的Class,那我們就可以根據方法名,通過反射拿到value()對應的method;然后我們再反射執行valueMethod,就可以拿到id了。

    ?有了id就好辦了,再反射執行findViewById,就可以拿到對應的view控件了。

  • 2.要拿到事件(即此處的[new View.OnClickListener()]),這個我們通過上面的BaseEvent已經得到了,即eventTpye。

  • 3.我們還要拿到訂閱關系(即setOnClickListener()),這個也好辦,通過上面的BaseEvent我們不是已經拿到了方法名的字符串了嗎,那再通過反射拿到對應的setterMethod就可以啦。

  • 4.我們,是不是還差一個事件的處理(onClick())。但是,我們寫的這個框架,并不知道將來按鈕要具體執行哪些操作呀?對于未知的東西我們該怎么處理呢?對啦!用動態代理。

在這里插入圖片描述

那下面我們就來具體看看這個動態代理該怎么寫吧~

首先我們要自定義一個MyInvocationHandler類,因為要代理的真實對象是activity中的click()方法,所以,我們需要兩個屬性,并在構造函數中直接傳入,我們還知道最終會調用這個類里的invoke()方法,而這里要執行的應該是真實對象要執行的操作,所以此處直接調用activityMethod.invoke(activity,objects);

//代理的是 new View.OnClickLisener()對象
//并且最終執行的是activity的click()方法
public class MyInvocationHandler implements InvocationHandler {
    private Object activity;
    private Method activityMethod;

    public MyInvocationHandler(Object activity, Method activityMethod) {
        this.activity = activity;
        this.activityMethod = activityMethod;
    }

    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        return activityMethod.invoke(activity, objects);
    }
}

好了,那我們就把第五步的步驟也補充上去吧:

private static void injectEvent(Object context) {
        Class<?> clazz = context.getClass();
        //1.獲取該activity上的所有方法
        Method[] methods = clazz.getDeclaredMethods();

        //2.循環遍歷方法,拿到每一個方法上的所有注解
        for (Method method : methods) {
            Annotation[] annotations = method.getAnnotations();
            //3.循環遍歷注解,拿到注解類型對應的Class,通過class去找,看看有沒有BaseEvent
            for (Annotation annotation : annotations) {
                //拿到注解類型對應的Class
                Class<?> annotationClass = annotation.annotationType();
                //通過Class去找,看看有沒有BaseEvent
                BaseEvent baseEvent = annotationClass.getAnnotation(BaseEvent.class);

                //如果沒有BaseEvent,則表示當前方法不是一個事件處理的方法
                if (baseEvent == null) {
                    continue;
                }

                //4.如果有BaseEvent,則表示是事件處理的方法,那我們就去拿事件的三要素
                //拿到三要素
                Class<?> eventType = baseEvent.enventType();
                String setterMethodStr = baseEvent.setterMethod();
                String callbackMethod = baseEvent.callbackMethod();

                //5.接下來我們要反射執行
//                   btn1.setOnClickListener(new View.OnClickListener() {
//                        @Override
//                        public void onClick(View view) {
//
//                        }
//                    });

                //5.1首先我們需要拿到事件源,(即對應的view控件,此處即指btn按鈕)
                try {
                    //先獲取注解中的value方法,即 OnClick中的value()
                    Method valueMethod = annotationClass.getDeclaredMethod("value");
                    //再反射執行 OnClick注解的 value()方法,得到id
                    int[] ids = (int[]) valueMethod.invoke(annotation);
                    for (int id : ids) {
                        //反射執行context.findViewById(id)得到對應的view
                        Method findViewByIdMethod = clazz.getMethod("findViewById", int.class);
                        View view = (View) findViewByIdMethod.invoke(context, id);
                        if (view == null) {
                            continue;
                        }

                        //5.2要拿到事件(即[new View.OnClickListener()]),這個我們通過上面的BaseEvent已經得到了,即eventTpye。
                        //5.3拿到訂閱關系(即setOnClickListener()),即根據setterMethodStr得到setterMethod

                        //5.4動態代理了  //activity==context    click===method
                        MyInvocationHandler myInvocationHandler = new MyInvocationHandler(context, method);
                        Object proxy = Proxy.newProxyInstance(eventType.getClassLoader(),
                                new Class[]{eventType}, myInvocationHandler);

                        //  讓proxy執行的click()
                        //參數1  setOnClickListener()的名稱
                        //參數2  new View.OnClickListener()對象
                        Method setterMethod = view.getClass().getMethod(setterMethodStr, eventType);
                        // 反射執行  view.setOnClickListener(new View.OnClickListener())
                        setterMethod.invoke(view, proxy);
                        //這時候,點擊按鈕時就會去執行代理類中的invoke方法()了
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        }
    }

好了,到這里我們所有的事件注入工作就都完成了,趕快在測試一下吧:

//在MainActivity中進行使用測試
@OnClick({R.id.button1, R.id.button2})
public void click(View view) {
    switch (view.getId()) {
        case R.id.button1:
            Toast.makeText(this, "短按下了", Toast.LENGTH_SHORT).show();
            break;
        case R.id.button2:
            Toast.makeText(this, "短按下了222", Toast.LENGTH_SHORT).show();
            break;
    }
}

//增加一個長按事件,注意這里的方法返回類型要和系統中的保持一致
@OnLongClick({R.id.button1, R.id.button2})
public boolean longClick(View view) {
    switch (view.getId()) {
        case R.id.button1:
            Toast.makeText(this, "好好學習", Toast.LENGTH_SHORT).show();
            break;
        case R.id.button2:
            Toast.makeText(this, "天天向上", Toast.LENGTH_SHORT).show();
            break;
    }
    return false;
}

運行結果:

在這里插入圖片描述
在這里插入圖片描述
源代碼下載地址:https://github.com/junmei520/iocStudy

積累點滴,做好自己~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容