在上一篇文章中,我们使用编译期注解处理技术成功生成了自动绑定 View 的辅助类,这篇文章我们继续完善 KButterKnife 框架:为其添加 @OnClick 注解,完成自动绑定点击事件。
首先新建 OnClick 接口:
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FUNCTION)
annotation class OnClick(val value: IntArray)
同样使其只在源码中保留,因为在生成辅助类之后,我们便不再需要这个注解了。注解目标设置为 FUNCTION,表示此注解需要在函数上使用。
修改 InjectionPoint 类:
/**
* @param id view id
* @param fieldName variable name of view when using [BindView]
* @param methodName method name when using [OnClick]
* @param methodType the type of method parameter when using [OnClick]
*/
data class InjectionPoint(val id: Int,
val fieldName: String? = null,
val methodName: String? = null,
val methodType: String? = null)
之前我们只保存了两个字段,BindView 中的 id,以及 BindView 所注解的变量名。现在我们为其新增了两个字段,methodName 表示 onClick 所注解的函数名,methodType 表示 onClick 所注解的函数中声明的参数类型。需要注意的是,这个函数要么不含参数,要么只含一个 View 或其子类类型的参数。这是因为 setOnClickListener 时,我们只能拿到 view 本身这一个有用的参数。
在添加了 OnClick 注解后,最终生成的辅助类类似这样:
package com.example.butterknife
class MainActivity_ViewInjector() {
fun inject(activity: MainActivity) {
activity.findViewById<android.view.View>(2131230809).setOnClickListener { activity.startSecondActivity() }
}
}
为了生成这个辅助类,我们先将 GeneratedFileTemplate 类修改如下:
const val TEMPLATE = """package %s
class %s() {
fun inject(activity: %s) {
%s
}
}"""
const val FIELD_INJECTION = " activity.%s = activity.findViewById(%s)"
const val METHOD_INJECTION = " activity.findViewById<android.view.View>(%s).setOnClickListener { activity.%s(%s) }"
我们添加了 METHOD_INJECTION 模板,并将之前的 INJECTION 常量重命名为 FIELD_INJECTION,以示区分。
KButterKnifeProcessor 类修改如下:
const val SUFFIX = "_ViewInjector"
class KButterKnifeProcessor : AbstractProcessor() {
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment?): Boolean {
// The map of activity -> @BindView info
val injectionsByClass = linkedMapOf<TypeElement, MutableSet<InjectionPoint>>()
// Save all @BindView annotations to map
roundEnv?.getElementsAnnotatedWith(BindView::class.java)?.forEach { element ->
val enclosingElement = element.enclosingElement as TypeElement
var fieldInjections = injectionsByClass[enclosingElement]
if (fieldInjections == null) {
fieldInjections = mutableSetOf()
injectionsByClass[enclosingElement] = fieldInjections
}
val fieldName = element.simpleName.toString()
val id = element.getAnnotation(BindView::class.java).value
fieldInjections.add(InjectionPoint(id, fieldName = fieldName))
}
// Save all @OnClick annotations to map
roundEnv?.getElementsAnnotatedWith(OnClick::class.java)?.forEach { element ->
val executableElement = element as ExecutableElement
val enclosingElement = element.enclosingElement as TypeElement
var methodInjections = injectionsByClass[enclosingElement]
if (methodInjections == null) {
methodInjections = mutableSetOf()
injectionsByClass[enclosingElement] = methodInjections
}
val methodName = executableElement.simpleName.toString()
val methodType = executableElement.parameters.firstOrNull()?.asType()?.toString()
element.getAnnotation(OnClick::class.java).value.forEach { id ->
methodInjections.add(InjectionPoint(id, methodName = methodName, methodType = methodType))
}
}
// Default generated dir path: app\build\generated\source\kaptKotlin\debug
val generatedDir = processingEnv.options["kapt.kotlin.generated"]
val filePath = "$generatedDir/com/example/butterknife"
// Generate every injection file
for (injection in injectionsByClass) {
val type = injection.key
val targetClass = type.qualifiedName
val lastDot = targetClass.lastIndexOf(".")
val packageName = targetClass.substring(0, lastDot)
val activityType = targetClass.substring(lastDot + 1)
val className = activityType + SUFFIX
val injections = StringBuilder()
injection.value.forEach {
if (it.fieldName != null) {
injections.appendLine(String.format(FIELD_INJECTION, it.fieldName, it.id))
} else if (it.methodName != null) {
injections.appendLine(String.format(METHOD_INJECTION, it.id, it.methodName, if (it.methodType == null) "" else "it as ${it.methodType}"))
}
}
val file = File(filePath, "$className.kt")
file.parentFile.mkdirs()
// Write file
val writer = BufferedWriter(FileWriter(file))
writer.use {
it.write(String.format(TEMPLATE, packageName, className, activityType, injections.toString()))
}
}
return false
}
override fun getSupportedAnnotationTypes(): MutableSet<String> {
return mutableSetOf(BindView::class.java.canonicalName)
}
override fun getSupportedSourceVersion(): SourceVersion {
return SourceVersion.latestSupported()
}
}
解析 BindView 注解的过程和之前是一样的,在 BindView 解析完成后,我们继续解析 OnClick 注解,通过 (element as ExecutableElement).simpleName.toString()
拿到被注解的函数名,通过 (element as ExecutableElement).parameters.firstOrNull()?.asType()?.toString()
拿到参数类型,将其保存到 InjectionPoint 中。
最后生成辅助类时,根据 InjectionPoint 类中保存的参数生成对应的字符串,再写入文件即可。
接下来我们可以测试一下,在布局文件中添加一个 id 为 btnToast1 的 Button 和一个 id 为 btnToast2 的 Button,修改 MainActivity 中的代码如下:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
KButterKnife.inject(this)
}
@OnClick([R.id.btnToast1, R.id.btnToast2])
fun toast(button: Button) {
when (button.id) {
R.id.btnToast1 -> Toast.makeText(this, "Toast 1", Toast.LENGTH_SHORT).show()
R.id.btnToast2 -> Toast.makeText(this, "Toast 2", Toast.LENGTH_SHORT).show()
}
}
运行程序,点击对应按钮就可以看到对应的 Toast 信息弹出,说明我们的 OnClick 注解已经正常工作了。
源码已上传 github:https://github.com/wkxjc/KButterKnife
网友评论