只需三步實現Databinding插件化

0?wx_fmt=jpeg

首先爲何我要實現Databinding這個小插件,主要是在日常開發中,發現每次通過Android Studio的Layout resource file來創建xml佈局文件時,佈局文件的格式都沒有包含Databinding所要的標籤<layout>。導致的問題就是每次都要重複手動修改佈局文件,添加<layout>標籤等。

所以爲了能夠偷懶,就有個這個一步生成符合Databinding的佈局文件。

這篇文章不會詳細講每一個代碼的實現,因爲這樣太浪費大家的時間,我會通過幾個要點與關鍵代碼來梳理實現過程,而且感興趣的之後再去看源碼也會很容易理解。

源碼地址(歡迎來這點擊start😁):

https://github.com/idisfkj/da...

廢話不多說,先來看下這個插件的效果

640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1

三步走

實現上面的插件,我這裏歸納爲三步,只要你掌握了這三步,你也能夠實現自己的插件,提高日常開發,減少不必要的重複操作。

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

創建Actions

至於如何使用Gradle來創建plugin項目,這不是今天的主題,所以就不多介紹了。我這裏提供一個鏈接,可以幫助你快速使用Gradle創建plugin項目

http://www.jetbrains.org/inte...

就如上面的gif效果圖一樣,首先第一步是通過layout文件節點,彈出菜單列表,最後在New選項子列表中呈現Databinding layout resource file選項。如下圖所示

clipboard.png

上面的這整個步驟,可以歸納爲一點,就是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等菜單欄,同時也可以創建新的頂部菜單欄。

clipboard.png

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。更多信息可以查看下面的鏈接:

http://www.jetbrains.org/inte...

  1. 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佈局

現在我們要做的是

  1. 創建Dialog彈窗
  2. 繪製彈窗佈局
  3. 實現點擊事件
  4. 創建資源佈局文件

clipboard.png

創建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()
            }
        }
    }
}

現在,如果你將要創建的文件存在重名,將會彈出如下提示

clipboard.png

當然如果成功,文件就已經創建在layout目錄下,同時是Databinding模式的xml文件。

配置持久化Component

其實到這裏基本已經可以正常使用了,但爲了該插件能更靈活點,我還是增加了配置功能。

clipboard.png

這是插件的設置頁面,我在這裏提供了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補給站

clipboard.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章