美文网首页
手写 KButterKnife 框架(二) —— @OnClic

手写 KButterKnife 框架(二) —— @OnClic

作者: Vic_wkx | 来源:发表于2021-07-22 22:37 被阅读0次

在上一篇文章中,我们使用编译期注解处理技术成功生成了自动绑定 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

相关文章

网友评论

      本文标题:手写 KButterKnife 框架(二) —— @OnClic

      本文链接:https://www.haomeiwen.com/subject/tsgymltx.html