要求通過注解+反射+動(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)盯荤,如有雷同,純屬巧合