只需三步实现Databinding插件化

作者: 午后一小憩 | 来源:发表于2019-07-17 08:31 被阅读21次

    首先为何我要实现Databinding这个小插件,主要是在日常开发中,发现每次通过Android Studio的Layout resource file来创建xml布局文件时,布局文件的格式都没有包含Databinding所要的标签<layout>。导致的问题就是每次都要重复手动修改布局文件,添加layout标签等。

    所以为了能够偷懒,就有个这个一步生成符合Databinding的布局文件。

    这篇文章不会详细讲每一个代码的实现,因为这样太浪费大家的时间,我会通过几个要点与关键代码来梳理实现过程,而且感兴趣的之后再去看源码也会很容易理解。

    源码地址(欢迎来这点击start😁):

    https://github.com/idisfkj/databinding_autorun

    废话不多说,先来看下这个插件的效果

    三步走

    实现上面的插件,我这里归纳为三步,只要你掌握了这三步,你也能够实现自己的插件,提高日常开发,减少不必要的重复操作。

    1. 创建Actions
    2. 生成Panel布局
    3. 配置持久化Component

    创建Actions

    至于如何使用Gradle来创建plugin项目,这不是今天的主题,所以就不多介绍了。我这里提供一个链接,可以帮助你快速使用Gradle创建plugin项目

    http://www.jetbrains.org/intellij/sdk/docs/tutorials/build_system.html

    就如上面的gif效果图一样,首先第一步是通过layout文件节点,弹出菜单列表,最后在New选项子列表中呈现Databinding layout resource file选项。如下图所示

    上面的这整个步骤,可以归纳为一点,就是Action,所以我们接下来需要自定义Action。

    但所幸的是intellij openapi已经为我们提供了AnAction类,我们要做的只需继承它,来实现具体的update与actionPerformed方法即可。

    config

    在实现方法之前,我们需要在resources/META-INF/plugin.xml文件中进行配置。

        <actions>
            <!-- Add your actions here -->
            <action class="com.idisfkj.databinding.autorun.actions.DataBindingAutorunAction"
                    id="DataBindingAutorunAction"
                    text="_DataBinding layout resource file"
                    description="Create DataBinding Resource File">
                <add-to-group group-id="NewGroup" anchor="first"/>
            </action>
        </actions>
    

    该配置最重要的是最后一条add-to-group,这里我们需要将当前Action添加到NewGroup的系统列表中,这样我们才能在上图中的New的扩展列表中看到Databinding layout resources file选项。

    原则上我们在AS能够看到的列表,都能够进行插入。例如顶部的File、Edit、View等菜单栏,同时也可以创建新的顶部菜单栏。

    update

    这个方法主要是用来更新Action的状态,它的回调会非常频繁与迅速。通过这个回调方法来控制Databinding layout resource file这个选项的显隐。

    为什么要控制显隐呢?很简单,一方面我们创建.xml资源文件只能在layout文件夹下,所以我们要控制它的创建位置;另一方面也是为了与原生的Layout resource file选项保持一致,不至于违和。

    而Action的显隐是可以通过presentation.isVisible来控制。

    那么最终效果与控制量都知道了,最后我们要做的就是逻辑判断。我们直接来Look at the code

        override fun update(e: AnActionEvent) {
            with(e) {
                // 默认不显示
                presentation.isVisible = false
                // AnActionEvent的扩展方法,目的是找到当前操作的虚拟文件
                handleVirtualFile { project, virtualFile ->
                    // 找到当前module,并且定位到layout文件目录
                    ModuleUtil.findModuleForFile(virtualFile, project)?.sourceRoots?.map {
                        val layout = PsiManager.getInstance(project)
                            .findDirectory(it)
                            ?.findSubdirectory("layout")
     
                        // 当前操作范围在layout节点下
                        if (layout != null && virtualFile.path.contains(layout.virtualFile.path)) {
                            // 显示
                            presentation.isVisible = true
                            return@map
                        }
                    }
                }
            }
        }
    

    这里有两个知识点

    1. VirtualFile: 简单的来说可以理解为项目中的文件与文件夹。 这里通过它来定位当前所处的module。更多信息可以查看下面的链接:
      http://www.jetbrains.org/intellij/sdk/docs/basics/virtual_file_system.html

    2. PsiManager:项目结构管理器,这里通过它来找到layout文件目录,后续还会使用它来实现自动添加文件。更多信息可以查看下面的链接:
      http://www.jetbrains.org/intellij/sdk/docs/basics/architectural_overview/psi.html

    actionPerformed

    现在我们已经控制了Action的显隐,接下来我们要做的就是实现它的点击事件。

    逻辑很简单,就是一个简单的点击事件,弹出一个编辑框。

        override fun actionPerformed(e: AnActionEvent) {
            // AnActionEvent的扩展方法,目的是找到当前操作的虚拟文件
            e.handleVirtualFile { project, virtualFile ->
                NewLayoutDialog(project, virtualFile).show()
            }
        }
    

    重点是NewLayoutDialog的内部处理逻辑,那么我们继续。

    生成Panel布局

    现在我们要做的是

    1. 创建Dialog弹窗
    2. 绘制弹窗布局
    3. 实现点击事件
    4. 创建资源布局文件

    创建Dialog弹窗

    对于Dialog弹窗的创建也是非常方便的,只需继承DialogWrapper。在初始化时调用它的init方法,之后就是实现具体的布局createCenterPanel与点击事件doOKAction方法。

        init {
            title = "New DataBinding Layout Resource File"
            init()
        }
     
        override fun createCenterPanel(): JComponent? = panel
     
        override fun doOKAction() {}
    

    绘制弹窗布局

    如果使用传统的GUI布局,个人感觉非常麻烦。因为项目使用的是kotlin,所以我这里使用了Kotlin UI DSL,如果你不了解的话可以查看下面的链接。

    http://www.jetbrains.org/intellij/sdk/docs/user_interface_components/kotlin_ui_dsl.html

    要实现上述的布局效果,需要继承JPanel,然后添加两个文本label与输入框JTextField。具体如下

    class NewLayoutPanel(project: Project) : JPanel() {
     
        val fileName = JTextField()
        val rootElement = JTextField()
     
        init {
            layout = BorderLayout()
            val panel = panel(LCFlags.fill) {
                row("File name:") { fileName() }
                row("Root element:") { rootElement() }
            }
            rootElement.text = SettingsComponent.getInstance(project).defaultRootElement
     
            add(panel, BorderLayout.CENTER)
        }
     
        override fun getPreferredSize(): Dimension = Dimension(300, 40)
    }
    

    代码中的SettingsComponent是用来保存持久化配置的,而这里是获取设置页面配置的数据,后续会提及到。

    现在已经有了布局,再将自定义的布局添加到createCenterPanel方法中。接下来要做的是实现弹窗的OK点击

    实现点击事件

    点击的逻辑是,首先查看当前将要创建的文件名称是否已经存在,其次才是创建文件,添加到目录中。

    对于文件名称是否重名,开始我是通过查找该目录下的所有文件来进行判断的,但后来发现无需这么麻烦。因为在添加文件的时候会进行自动判断,如果有重名会抛出异常,所以可以通过捕获异常来进行弹窗提示。

    文件的创建通过PsiFileFactory的createFileFromText方法

    val file = PsiFileFactory.getInstance(project)
        .createFileFromText(
            (panel.fileName.text
                ?: TemplateUtils.TEMPLATE_DATABINDING_FILE_NAME) + TemplateUtils.TEMPLATE_LAYOUT_SUFFIX,
            XMLLanguage.INSTANCE,
            TemplateUtils.getTemplateContent(panel.rootElement.text)
        )
    

    三个参数值分别为

    • 文件名: 通过布局panel获取text
    • 语言: 因为是.xml布局文件,所用是xml语言
    • 内容: 这里使用了预先定制的模板(可任意修改)

    接下来就是将文件添加到layout下,这里还是要使用之前的PsiManager来定位到layout目录下

    // 通过Swing dispatch thread来进行写操作
    ApplicationManager.getApplication().runWriteAction {
        // module的扩展方法,目的是通过PsiManager定位到layout目录下
        getModule()?.handleVirtualFile {
            // 判断该操作是否在可接受的范围内
            if (actionVirtualFile.path.contains(it.virtualFile.path)) {
                try {
                    // 添加文件
                    it.add(file)
                    // 关闭弹窗
                    close(OK_EXIT_CODE)
                } catch (e: IncorrectOperationException) {
                    // 异常弹窗提醒
                    NotificationUtils.showMessage(
                        project, "error",
                        e.localizedMessage
                    )
                    e.printStackTrace()
                }
            }
        }
    }
    

    现在,如果你将要创建的文件存在重名,将会弹出如下提示

    当然如果成功,文件就已经创建在layout目录下,同时是Databinding模式的xml文件。

    配置持久化Component

    其实到这里基本已经可以正常使用了,但为了该插件能更灵活点,我还是增加了配置功能。

    这是插件的设置页面,我在这里提供了Default Root Element的设置,它是创建xml文件的布局根节点标签,默认是LinearLayout,所以你可以通过修改它来改变每次弹窗的默认根布局节点标签。

    当然这只是一个小功能,在这里提出是为了让大家了解设置页的实现。

    之前我还实现了可以自定义xml的内容模板,但后来想意义并不大就删除掉了,因为我们日常开发中布局的内容都是多变的,唯一能稍微固定的也就是布局的根节点了。

    Setting布局

    对于设置页的布局,其实也是一个label与JTextField,所以我这里就不多说了,具体可以查看源码

    Configurable

    设置页需要实现Configurable接口,它会提供是4个方法

        override fun isModified(): Boolean = modified
     
        override fun getDisplayName(): String = "DataBinding Autorun"
     
        override fun apply() {
            SettingsComponent.getInstance(project).defaultRootElement = settingsPanel.defaultRootElement.text
            modified = false
        }
     
        override fun createComponent(): JComponent? = settingsPanel.apply {
            defaultRootElement.text = SettingsComponent.getInstance(project).defaultRootElement
            defaultRootElement.document.addDocumentListener(this@SettingsConfigurable)
        }
    
    • isModified: 是否进行了修改,为true的话设置页的Apply就会变成可点击
    • getDisplayName: 在Android Studio的OtherSettings中展示的名称
    • apply: Apply的点击回调
    • createComponent: 布局

    对于isModified的判断逻辑,引入对document的监听DocumentListener

        override fun changedUpdate(e: DocumentEvent?) {
            modified = true
        }
     
        override fun insertUpdate(e: DocumentEvent?) {
            modified = true
        }
     
        override fun removeUpdate(e: DocumentEvent?) {
            modified = true
        }
    

    它提供的三个方法只要发生了回调,就认为是编辑了该设置页。

    最后在apply与createComponent中都用到了SettingsComponent,它是用来保存数据的,保证设置的defaultRootElement能够实时保存,类似于Android的sharedpreferences

    PersistentStateComponent

    要实现数据的持久话,需要实现PersistentStateComponent接口。它会暴露getState与loadState两个方法,让我们来获取与保存状态。

    它的保存方式也是通过.xml的文件方式进行保存,所以需要使用@state来进行配置,具体如下

    @State(
        name = "SettingsConfiguration",
        storages = [Storage(value = "settingsConfiguration.xml")]
    )
    class SettingsComponent : PersistentStateComponent<SettingsComponent> {
     
        var defaultRootElement = "LinearLayout"
     
        companion object {
            fun getInstance(project: Project): SettingsComponent =
                ServiceManager.getService(project, SettingsComponent::class.java)
        }
     
        override fun getState(): SettingsComponent? = this
     
        override fun loadState(state: SettingsComponent) {
            XmlSerializerUtil.copyBean(state, this)
        }
    }
    

    该状态名为SettingConfiguration,保存在settingConfiguration.xml文件中。保存方式会借助XmlSerializerUtil来实现。

    当然为了保存该实例的单例模式,这里使用ServiceManager的getService方法来获取它的实例。所以在上面的Configurable中,使用的就是这个方式。

    配置

    自定义的SettingsConfigurable与SettingsComponent都需要到plugin.xml中进行配置,这与之前的Action类似。你可以理解为Android的四大组件。

        <extensions defaultExtensionNs="com.intellij">
            <!-- Add your extensions here -->
            <defaultProjectTypeProvider type="Android"/>
            <projectConfigurable instance="com.idisfkj.databinding.autorun.ui.settings.SettingsConfigurable"/>
            <projectService serviceInterface="com.idisfkj.databinding.autorun.component.SettingsComponent"
                            serviceImplementation="com.idisfkj.databinding.autorun.component.SettingsComponent"/>
        </extensions>
     
        <project-components>
            <component>
                <implementation-class>
                    com.idisfkj.databinding.autorun.component.SettingsComponent
                </implementation-class>
            </component>
        </project-components>
    

    由于SettingsComponent是project级别的,所以这里包含在project-components标签中;另一方面SettingsConfigurable在配置中统一归于extensions标签,至于为什么,这就涉及到扩展了,简单的说就是别人可以在你的插件基础上进行不同程度的扩展,就是基于这个的。由于这又是另外一个话题,所以就不多说了,感兴趣的可以自己去了解。

    结语

    关于Databinding插件化的定制就到这里了,源码已经在文章开头给出。

    或者你也可以通过Android精华录获取

    如果你对该插件有别的建议,欢迎@我;亦或者你在使用的过程中有什么不便的地方也可以在github中提issue,我也会第一时间进行优化。

    自荐

    个人主页: https://www.rousetime.com
    技术公众号: Android补给站

    Android补给站

    相关文章

      网友评论

        本文标题:只需三步实现Databinding插件化

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