只需三步實現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補給站

相關文章
相關標籤/搜索