盒子
盒子
文章目录
  1. 三步走
  2. 创建Actions
    1. config
    2. update
    3. actionPerformed
  3. 生成Panel布局
    1. 创建Dialog弹窗
    2. 绘制弹窗布局
    3. 实现点击事件
  4. 配置持久化Component
    1. Setting布局
    2. Configurable
    3. PersistentStateComponent
    4. 配置
  5. 结语

只需三步实现Databinding插件化

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

1
2
3
4
5
6
7
8
9
<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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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的显隐,接下来我们要做的就是实现它的点击事件。

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

1
2
3
4
5
6
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方法。

1
2
3
4
5
6
7
8
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。具体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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方法

1
2
3
4
5
6
7
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目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 通过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个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
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

1
2
3
4
5
6
7
8
9
10
11
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来进行配置,具体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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的四大组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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插件化的定制就到这里了,源码已经在文章开头给出。

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

支持一下
赞赏是一门艺术