只需三步实现Databinding插件化

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

因此为了可以偷懒,就有个这个一步生成符合Databinding的布局文件。java

这篇文章不会详细讲每个代码的实现,由于这样太浪费你们的时间,我会经过几个要点与关键代码来梳理实现过程,并且感兴趣的以后再去看源码也会很容易理解。android

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

github.com/idisfkj/dat…github

废话很少说,先来看下这个插件的效果api

三步走

实现上面的插件,我这里概括为三步,只要你掌握了这三步,你也可以实现本身的插件,提升平常开发,减小没必要要的重复操做。bash

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

建立Actions

至于如何使用Gradle来建立plugin项目,这不是今天的主题,因此就很少介绍了。我这里提供一个连接,能够帮助你快速使用Gradle建立plugin项目app

www.jetbrains.org/intellij/sd…ide

就如上面的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。更多信息能够查看下面的连接: www.jetbrains.org/intellij/sd…

  2. PsiManager:项目结构管理器,这里经过它来找到layout文件目录,后续还会使用它来实现自动添加文件。更多信息能够查看下面的连接: www.jetbrains.org/intellij/sd…

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,若是你不了解的话能够查看下面的连接。

www.jetbrains.org/intellij/sd…

要实现上述的布局效果,须要继承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,我也会第一时间进行优化。

自荐

我的主页: www.rousetime.com

技术公众号: Android补给站

相关文章
相关标签/搜索