美文网首页
手写 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