首先爲什麼我要實現Databinding這個小插件,主要是在平常開發中,發現每次經過Android Studio的Layout resource file來建立xml佈局文件時,佈局文件的格式都沒有包含Databinding所要的標籤。致使的問題就是每次都要重複手動修改佈局文件,添加layout標籤等。html
因此爲了可以偷懶,就有個這個一步生成符合Databinding的佈局文件。java
這篇文章不會詳細講每個代碼的實現,由於這樣太浪費你們的時間,我會經過幾個要點與關鍵代碼來梳理實現過程,並且感興趣的以後再去看源碼也會很容易理解。android
源碼地址(歡迎來這點擊start😁):git
github.com/idisfkj/dat…github
廢話很少說,先來看下這個插件的效果api
實現上面的插件,我這裏概括爲三步,只要你掌握了這三步,你也可以實現本身的插件,提升平常開發,減小沒必要要的重複操做。bash
至於如何使用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方法便可。
在實現方法以前,咱們須要在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等菜單欄,同時也能夠建立新的頂部菜單欄。
這個方法主要是用來更新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。更多信息能夠查看下面的連接: www.jetbrains.org/intellij/sd…
PsiManager:項目結構管理器,這裏經過它來找到layout文件目錄,後續還會使用它來實現自動添加文件。更多信息能夠查看下面的連接: www.jetbrains.org/intellij/sd…
如今咱們已經控制了Action的顯隱,接下來咱們要作的就是實現它的點擊事件。
邏輯很簡單,就是一個簡單的點擊事件,彈出一個編輯框。
override fun actionPerformed(e: AnActionEvent) {
// AnActionEvent的擴展方法,目的是找到當前操做的虛擬文件
e.handleVirtualFile { project, virtualFile ->
NewLayoutDialog(project, virtualFile).show()
}
}
複製代碼
重點是NewLayoutDialog的內部處理邏輯,那麼咱們繼續。
如今咱們要作的是
對於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)
)
複製代碼
三個參數值分別爲
接下來就是將文件添加到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文件。
其實到這裏基本已經能夠正常使用了,但爲了該插件能更靈活點,我仍是增長了配置功能。
這是插件的設置頁面,我在這裏提供了Default Root Element的設置,它是建立xml文件的佈局根節點標籤,默認是LinearLayout,因此你能夠經過修改它來改變每次彈窗的默認根佈局節點標籤。
固然這只是一個小功能,在這裏提出是爲了讓你們瞭解設置頁的實現。
以前我還實現了能夠自定義xml的內容模板,但後來想意義並不大就刪除掉了,由於咱們平常開發中佈局的內容都是多變的,惟一能稍微固定的也就是佈局的根節點了。
對於設置頁的佈局,其實也是一個label與JTextField,因此我這裏就很少說了,具體能夠查看源碼
設置頁須要實現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的判斷邏輯,引入對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接口。它會暴露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補給站