首先爲何我要實現Databinding這個小插件,主要是在日常開發中,發現每次通過Android Studio的Layout resource file來創建xml佈局文件時,佈局文件的格式都沒有包含Databinding所要的標籤<layout>。導致的問題就是每次都要重複手動修改佈局文件,添加<layout>標籤等。
所以爲了能夠偷懶,就有個這個一步生成符合Databinding的佈局文件。
這篇文章不會詳細講每一個代碼的實現,因爲這樣太浪費大家的時間,我會通過幾個要點與關鍵代碼來梳理實現過程,而且感興趣的之後再去看源碼也會很容易理解。
源碼地址(歡迎來這點擊start😁):
https://github.com/idisfkj/da...
廢話不多說,先來看下這個插件的效果
三步走
實現上面的插件,我這裏歸納爲三步,只要你掌握了這三步,你也能夠實現自己的插件,提高日常開發,減少不必要的重複操作。
- 創建Actions
- 生成Panel佈局
- 配置持久化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
}
}
}
}
}
這裏有兩個知識點
- VirtualFile: 簡單的來說可以理解爲項目中的文件與文件夾。 這裏通過它來定位當前所處的module。更多信息可以查看下面的鏈接:
http://www.jetbrains.org/inte...
- 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佈局
現在我們要做的是
- 創建Dialog彈窗
- 繪製彈窗佈局
- 實現點擊事件
- 創建資源佈局文件
創建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補給站