通過動(dòng)態(tài)代理動(dòng)態(tài)設(shè)置點(diǎn)擊事件

要求通過注解+反射+動(dòng)態(tài)代理的方式實(shí)現(xiàn)類似如下的事件點(diǎn)擊監(jiān)聽智哀,例如

view.setOnClickListener(object: View.OnClickListener{
    override fun onClick(v: View?) {
        // do something
    }
})

頁(yè)面的實(shí)現(xiàn)效果如下:

class ClickActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.next_layout)
        
        inject(this)
//        ProxyClick.injectByJava(this)
    }

    @OnClick(R.id.btn1, R.id.btn2)
    fun click12(view: View) {
        if (view.id == R.id.btn1) {
            Toast.makeText(this, "click1", Toast.LENGTH_SHORT).show()
        }

        if (view.id == R.id.btn2) {
            Toast.makeText(this, "click2", Toast.LENGTH_SHORT).show()
        }
    }

    @OnLongClick(R.id.btn3, R.id.btn4, R.id.btn5)
    fun click345(view: View): Boolean {
        if (view.id == R.id.btn3) {
            Toast.makeText(this, "long click3", Toast.LENGTH_SHORT).show()
            return true
        }

        if (view.id == R.id.btn4) {
            Toast.makeText(this, "long click4", Toast.LENGTH_SHORT).show()
            return true
        }

        if (view.id == R.id.btn5) {
            Toast.makeText(this, "long click5", Toast.LENGTH_SHORT).show()
            return true
        }
        return false
    }
}

附帶的要求:注入的inject方法能夠兼容常見的一些設(shè)置監(jiān)聽的方法阀湿。

問題1:如何做到inject方法的兼容舶掖?

既然是兼容畜侦,那么就代表該方法一旦完成旭寿,便不要再去修改其內(nèi)部實(shí)現(xiàn)芥映。顯然,如果在inject內(nèi)部判斷是哪種類型的點(diǎn)擊事件是不可靠的了滚秩,因?yàn)槲磥硪蛩夭豢深A(yù)期专执,如果將來新增了某種事件監(jiān)聽,只能再往里添加相應(yīng)監(jiān)聽邏輯郁油,達(dá)不到代碼兼容的目的本股。

參考view.setOnClickListener(View.OnClickListener)攀痊,并從分析中可以看出,不能像修補(bǔ)丁一樣一個(gè)一個(gè)判斷痊末,而且監(jiān)聽方法和方法參數(shù)又是未知的蚕苇,則我們就只能在inject方法參數(shù)中傳入監(jiān)聽的方法簽名和參數(shù)類型簽名,然后反射的方式實(shí)現(xiàn)類似view.setOnClickListener(View.OnClickListener)凿叠,但這樣做的話顯然inject方法和view就成了一對(duì)一的關(guān)系,和直接view.setOnClickListener(View.OnClickListener)又有什么區(qū)別嚼吞?

子問題:如何隱藏監(jiān)聽方法簽名和監(jiān)聽方法參數(shù)簽名盒件?

再看下題目要求是注解+反射+動(dòng)態(tài)代理,那么可行的解決辦法來了舱禽。
我們可以將事件監(jiān)聽方法簽名和事件監(jiān)聽參數(shù)類型簽名放到注解當(dāng)中炒刁,但同時(shí)我又要求頁(yè)面方法上的注解不要標(biāo)注這些簽名,例如當(dāng)我寫入了@OnClick(R.id.xx)時(shí)誊稚,在inject當(dāng)中就能獲取到當(dāng)前的監(jiān)聽方法和監(jiān)聽參數(shù)類型簽名翔始,那么該如何做呢?

這時(shí)我們就要在注解之上要加上自定義的元注解了里伯,并在該元注解之內(nèi)標(biāo)注上那些簽名即可城瞎。實(shí)現(xiàn)方式如下:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.ANNOTATION_CLASS)
annotation class EventType(
    val listenType: KClass<*>,
    val listenMethod: String
)

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
@EventType(listenType = View.OnClickListener::class, listenMethod = "setOnClickListener")
annotation class OnClick(@IdRes vararg val value: Int)

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
@EventType(listenType = View.OnLongClickListener::class, listenMethod = "setOnLongClickListener")
annotation class OnLongClick(@IdRes vararg val value: Int)

然后剩下的邏輯就是搜集頁(yè)面帶有事件注解的方法,然后根據(jù)注解的value獲取到當(dāng)前注解的所有view疾瓮,根據(jù)EventType的listenMethod獲取監(jiān)聽方法簽名脖镀,根據(jù)listenType獲取監(jiān)聽參數(shù)類型簽名,然后反射的方式實(shí)現(xiàn)類似view.setOnClickListener(View.OnClickListener)等等的邏輯狼电。

問題2:如何動(dòng)態(tài)執(zhí)行頁(yè)面注解的方法蜒灰?

設(shè)置監(jiān)聽的問題是設(shè)置好了,如何去執(zhí)行頁(yè)面注解的方法呢肩碟?
常規(guī)的寫法就是view.setOnClickListener(View.OnClickListener)强窖,但是剛才分析了,這行代碼是反射執(zhí)行的削祈,并不是手動(dòng)寫出的翅溺,如果想用手動(dòng)寫的話,又回到最上面的老問題了岩瘦,不能保證代碼兼容性未巫,要加好多不同事件類型監(jiān)聽的判斷∑裘粒可以想象叙凡,類似new View.OnClickListener{xxx}是完全動(dòng)態(tài)的,也可能是View.OnLongClickListener{xxx}或者其他等等密末。

有沒有好的辦法動(dòng)態(tài)創(chuàng)建當(dāng)前角色呢握爷?
當(dāng)遇到類似問題時(shí)跛璧,動(dòng)態(tài)代理的思路便能很好的解決你的問題!
我不需要知道你到底是什么類型新啼,你只需要把你想要的類型傳給我追城,我只需要?jiǎng)討B(tài)生成指定類型的代理即可,這就是代理的一大魅力燥撞!
然后只需要在代理的InvocationHandler內(nèi)部類invoke方法里反射調(diào)用頁(yè)面的頁(yè)面方法并反射執(zhí)行調(diào)用該方法即可座柱。

下面我將提供Kotlin和Java的兩種實(shí)現(xiàn)方法。

// Kotlin
fun inject(act: Activity) {
    act::class.java.declaredMethods.forEach { method ->
        method.annotations.forEach {
            // 判斷注解上是否存在EventType元注解(目前只有該判斷方式物舒,沒有java的annotationType方法)
            if (it.annotationClass.java.isAnnotationPresent(EventType::class.java)) {
                val et = it.annotationClass.java.getAnnotation(EventType::class.java)!!
                // 生成點(diǎn)擊事件監(jiān)聽的代理
                val proxy = Proxy.newProxyInstance(
                    et.listenType.java.classLoader,
                    arrayOf(et.listenType.java)
                ) { _, _, params ->
                    // 執(zhí)行監(jiān)聽時(shí)便調(diào)用頁(yè)面的當(dāng)前方法
                    method.isAccessible = true
                    method.invoke(act, *params)
                }

                // 獲取當(dāng)前注解的value色洞,即對(duì)應(yīng)OnClick或OnLongClick的value
                val ids = it.javaClass.getDeclaredMethod("value").invoke(it)!! as IntArray
                ids.map { id -> act.findViewById<View>(id) }
                    .forEach { v ->
                        // 設(shè)置監(jiān)聽,如setOnClickListener(OnClickListener)冠胯,其中OnClickListener即為proxy
                        val clickMethod = v.javaClass.getMethod(et.listenMethod, et.listenType.java)
                        clickMethod.isAccessible = true
                        clickMethod.invoke(v, proxy)
                    }
            }
        }
    }
}
// Java
public static void injectByJava(final Activity activity) {
    Method[] methods = activity.getClass().getDeclaredMethods();
    for (final Method method: methods) {
        for (Annotation annotation: method.getAnnotations()) {
            Class<? extends Annotation> annotationType = annotation.annotationType();
            // 判斷該注解是否是元注解火诸,注意和kotlin代碼判斷的不同
            if (annotationType.isAnnotationPresent(EventType.class)) {
                EventType et = annotationType.getAnnotation(EventType.class);
                // 生成點(diǎn)擊事件監(jiān)聽的代理
                Object proxy = Proxy.newProxyInstance(
                        et.listenType().getClassLoader(),
                        new Class[]{et.listenType()},
                        new InvocationHandler() {
                            @Override
                            public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
                                // 執(zhí)行監(jiān)聽時(shí)便調(diào)用頁(yè)面的當(dāng)前方法
                                method.setAccessible(true);
                                return method.invoke(activity, args);
                            }
                        });

                try {
                    // 獲取當(dāng)前注解的value,即對(duì)應(yīng)OnClick或OnLongClick的value
                    int[] ids = (int[]) annotation.getClass().getDeclaredMethod("value").invoke(annotation);
                    for (int i=0; i<ids.length; i++) {
                        // 設(shè)置監(jiān)聽荠察,如setOnClickListener(OnClickListener)置蜀,其中OnClickListener即為proxy
                        View view = activity.findViewById(ids[i]);
                        Method clickMethod = view.getClass().getMethod(et.listenMethod(), et.listenType());
                        clickMethod.setAccessible(true);
                        clickMethod.invoke(view, proxy);
                    }
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    }
   }

記錄日常點(diǎn)滴,每天成長(zhǎng)一點(diǎn)點(diǎn)悉盆。本文原創(chuàng)盯荤,如有雷同,純屬巧合

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末舀瓢,一起剝皮案震驚了整個(gè)濱河市廷雅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌京髓,老刑警劉巖航缀,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異堰怨,居然都是意外死亡芥玉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門备图,熙熙樓的掌柜王于貴愁眉苦臉地迎上來灿巧,“玉大人,你說我怎么就攤上這事揽涮】倥海” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵蒋困,是天一觀的道長(zhǎng)盾似。 經(jīng)常有香客問我,道長(zhǎng)雪标,這世上最難降的妖魔是什么零院? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任溉跃,我火速辦了婚禮,結(jié)果婚禮上告抄,老公的妹妹穿的比我還像新娘撰茎。我一直安慰自己,他們只是感情好打洼,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布龄糊。 她就那樣靜靜地躺著,像睡著了一般募疮。 火紅的嫁衣襯著肌膚如雪绎签。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天酝锅,我揣著相機(jī)與錄音,去河邊找鬼奢方。 笑死搔扁,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蟋字。 我是一名探鬼主播稿蹲,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼鹊奖!你這毒婦竟也來了苛聘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤忠聚,失蹤者是張志新(化名)和其女友劉穎设哗,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體两蟀,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡网梢,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了赂毯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片战虏。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖党涕,靈堂內(nèi)的尸體忽然破棺而出烦感,到底是詐尸還是另有隱情,我是刑警寧澤膛堤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布手趣,位于F島的核電站,受9級(jí)特大地震影響骑祟,放射性物質(zhì)發(fā)生泄漏回懦。R本人自食惡果不足惜气笙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望怯晕。 院中可真熱鬧潜圃,春花似錦、人聲如沸舟茶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)吧凉。三九已至隧出,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間阀捅,已是汗流浹背胀瞪。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留饲鄙,地道東北人凄诞。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像忍级,于是被迫代替她去往敵國(guó)和親帆谍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345