只需三步实现Databinding插件化

11次阅读

共计 7594 个字符,预计需要花费 19 分钟才能阅读完成。

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

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

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

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

https://github.com/idisfkj/da…

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

三步走

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

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

创建 Actions

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

http://www.jetbrains.org/inte…

就如上面的 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/inte…

  1. PsiManager:项目结构管理器,这里通过它来找到 layout 文件目录,后续还会使用它来实现自动添加文件。更多信息可以查看下面的链接:

http://www.jetbrains.org/inte…

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/inte…

要实现上述的布局效果,需要继承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 补给站

正文完
 0