首先爲什麼我要實現Databinding這個小插件,主要是在平常開發中,發現每次經過Android Studio的Layout resource file來建立xml佈局文件時,佈局文件的格式都沒有包含Databinding所要的標籤<layout>。致使的問題就是每次都要重複手動修改佈局文件,添加<layout>標籤等。html
因此爲了可以偷懶,就有個這個一步生成符合Databinding的佈局文件。java
這篇文章不會詳細講每個代碼的實現,由於這樣太浪費你們的時間,我會經過幾個要點與關鍵代碼來梳理實現過程,並且感興趣的以後再去看源碼也會很容易理解。android
源碼地址(歡迎來這點擊start😁):git
https://github.com/idisfkj/da...github
廢話很少說,先來看下這個插件的效果web
實現上面的插件,我這裏概括爲三步,只要你掌握了這三步,你也可以實現本身的插件,提升平常開發,減小沒必要要的重複操做。api
至於如何使用Gradle來建立plugin項目,這不是今天的主題,因此就很少介紹了。我這裏提供一個連接,能夠幫助你快速使用Gradle建立plugin項目app
http://www.jetbrains.org/inte...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 } } } } }
這裏有兩個知識點
http://www.jetbrains.org/inte...
http://www.jetbrains.org/inte...
如今咱們已經控制了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,若是你不瞭解的話能夠查看下面的連接。
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) )
三個參數值分別爲
接下來就是將文件添加到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,我也會第一時間進行優化。
私人獨家博客: https://www.rousetime.com
技術公衆號:Android補給站