Kotlin 如何開發 IDEA 圖片壓縮插件
這篇文章將會帶大家使用 Kotlin 開發一個非常實用的工具,一款基于 Intellij IDEA 并且可以適用于所有 jetBrains 全家桶 IDE 的圖片壓縮插件,可以直接用于平時開發中。
我們經常會遇到一些圖片需要壓縮的需求,特別是 Android 開發一些打入包內的圖片,為了不增加包體積大小需要手動壓縮一遍圖片,這時候一般會把圖片拖到具體壓縮網站上,在線壓縮然后下載。
如果說只需要在 AndroidStudio 或 IDEA 中直接選中要壓縮的圖片即可,是不是很方便呢。開發該插件目的有兩個:一個是學習 Intellij IDEA 插件的開發流程,另一個是練習 Kotlin 的開發實戰技能。
1. 插件開發導學篇
1.1 為什么需要開發一款圖片壓縮插件
我們在項目開發過程中常會使用圖片,一般開發者都不會直接把設計切的圖片放入到項目中,而是會去壓縮一下,那么一般會去 TinyPng 網頁端壓縮一遍,你一般會先把要壓縮的圖片拖進去,然后又一張張把圖片點擊下載下來,是不是感覺特別的浪費時間,是不是需要把浪費的時間省下來,然后就愉快地早點下班啦。如果你還沒有使用過 TinyPng,那么這個插件也許適合你。
然后這段時間正研究插件,所以決定試試,其實很簡單的。因為 TinyPng 提供 develop api,可以方便實現圖片壓縮。這次插件也就是利用了它的 API 開發的。
1.2 插件基本介紹
本插件是一款基于 TinyPng API 開發的圖片壓縮的 IDEA 工具插件,采用的是 Kotlin 語言開發以及 Java Swing 框架設計 UI 界面。可運行在 AndroidStudio,Intellij IDEA,WebStorm 等 JetBrains 全家桶系列 IDE 中。主要支持以下功能:
- 1、支持整個目錄中的圖片批量壓縮,只需要指定圖片源目錄和壓縮的輸出目錄即可;
- 2、支持單張或者選定多張圖片文件進行壓縮;
- 3、支持 png,jpg 格式圖片;
- 4、支持輸入目錄和輸出目錄二次選擇功能,減少繁瑣指定相同的目錄;
- 5、支持指定輸入文件的前綴,也即是批量文件添加前綴名,以及前綴名二次選擇功能;
- 6、圖片壓縮過程中,仍然繼續 coding, 工作并行執行。
1.3 需要使用的技術點
- Intellij Idea 插件開發基礎知識;
- 插件開發中執行一個后臺線程任務 Task.Backgroundable 的使用;
- Intellij Idea open api 的使用;
- Kotlin 開發基礎知識;
- Kotlin 中擴展函數的封裝;
- Kotlin 中 Lambda 表達式的使用;
- Kotlin 中函數式 API 的使用;
- Kotlin 中 IO 操作 API 的使用;
- Java 中 Swing UI 框架的基本使用;
- TinyPng API 基本使用。
1.4 實現后基本效果
2. IntelliJ IDEA 插件開發基礎篇
2.1 什么是 IntelliJ IDEA 插件
IDE 插件利用 jetBrains 公司開源的 IntelliJ Platform SDK (java 語言) 來開發一個獨立功能可以安裝在 IDEA 之類的編輯器的功能組件。 IDE 插件是基于 IntelliJ IDEA 開發工具開發,里面集成了插件的項目的構建。采用的是 Java 語言開發和 IntelliJ 的 SDK 相結合開發。
并且在開發出來的插件不僅在 AndroidStudio 上可以使用,可以通用于 jetBrains 的編輯器的全家桶工具。通過源碼可以發現 Intellij Idea 內置了大量的插件,可以這么說 Intellij Idea 開發工具大部分功能是由插件組合而成的。
2.2 開始編寫第一個插件
構建插件項目的方式主要分為兩種:一種是直接創建 IDEA 內置的插件項目。
另一種則是先通過構建一個 gradle 項目,然后加入 plugin.xml 配置以及 加入 IDEA ERP 的依賴,然后來構建一個插件項目 (整個開發過程就和開發一個 Android 項目一樣),當然這個構建過程可參考官方給出的 gradle-intellij-plugin 項目來實現。
(這里我們以第一種為例) 打開已經安裝好的 IntelliJ IDEA,然后 create New Project. 選擇一個 IntelliJ Platform Plugin 項目。注意需要引入 IntelliJ IDEA 的 SDK
選擇好 SDK 后,然后只需要一步一步把項目創建完畢即可,創建好的項目結構如下:
正如你所看到,生成了一個 plugin.xml,這個文件是插件項目的配置文件,它記錄了插件相關的版本擴展等基本信息,還記錄了插件事件與具體實現類綁定過程,下面就一一介紹每個標簽的含義。
<idea-plugin>
<id>com.your.company.unique.plugin.id</id>
<name>Plugin display name here</name>
<version>1.0</version>
<vendor email="[email protected]" url="http://www.yourcompany.com">YourCompany</vendor>
<description><![CDATA[
Enter short description for your plugin here.<br>
<em>most HTML tags may be used</em>
]]></description>
<change-notes><![CDATA[
Add change notes here.<br>
<em>most HTML tags may be used</em>
]]>
</change-notes>
<!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/build_number_ranges.html for description -->
<idea-version since-build="173.0"/>
<!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html
on how to target different products -->
<!-- uncomment to enable plugin in all products
<depends>com.intellij.modules.lang</depends>
-->
<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
</extensions>
<actions>
<!-- Add your actions here -->
</actions>
</idea-plugin>
標簽 | 含義 | 解釋說明 |
---|---|---|
plugin 插件項目的標識 | 和 Android 項目中的 package 功能類似,唯一標識一個插件項目 | |
插件名字 | 發布到 jetBrains plugin 倉庫中會用這個 | |
插件版本號 | 這個用于標識插件版本,一般用于更新 jetbrains plugins 倉庫中插件版本標識 | |
開發者信息,郵箱和個人主頁,公司名字或個人開發者姓名 | 用于插件倉庫中插件信息介紹顯示 | |
<description> | 插件的描述信息 | 主要是描述插件有什么功能,支持標簽內部內嵌 HTML 標簽 |
<changNote> | 插件版本變更信息 | 一般用于插件版本變更的信息,支持標簽內部內嵌 HTML 標簽 |
<idea-version> | 插件支持的 idea 版本 | 這個版本標簽需要注意下,它決定了該插件能夠運行在最低版本的 IDEA 中,一旦配置不當,會導致插件安裝不成功,有點類似 Android 中 AndroidManifest.xml 中配置最低兼容 Android 版本意思 |
<depends> | 當前的插件項目依賴哪些內置或者外部的插件庫依賴 | 例如你需要實現類似 git 功能插件,你就可以通過 depends 標簽引入 Git4Idea 即可,Git4Idea, 如果看過 IDEA 源碼的話,實際上內置 GitHub 插件就是通過 depends 依賴內部 Git4Idea 插件實現的,還有現在的碼云 git 工具插件也是通過依賴 Git4Idea 內置插件來實現的 |
<extension> | 插件與其他插件或與 IDE 本身交互 | (默認是 IDEA) 如果您希望插件擴展其他插件或 IntelliJ Platform 的功能,則必須聲明一個或多個擴展名 |
<action> | 決定了你的插件在 IDE 上顯示的位置和順序 | 這個標簽非常重要,它決定了你的插件在 IDE 上顯示的位置和順序,以及這個插件的點擊事件和插件項目 Action 實現類的綁定。 |
創建一個 Action 類,在 IDEA 插件項目中,IDEA 點擊 Item 或者按鈕或者一個圖標對應是觸發了插件中一個 Action,創建 Action 主要有兩種方式:
第 1 種:通過 IDEA 提供的一個入口,直接去創建 Action,然后它自動幫你實現 plugin.xml 中的事件綁定的注冊:
第 2 種: 手動創建一個 Action 類,然后繼承 AnAction 類或者 DumbAwareAction 類,然后在 plugin.xml 中的 action 標簽去注冊 action 類與事件綁定:
//創建Action類
package com.imooc.plugins.demo
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.ui.Messages//注意import,是com.intellij.openapi包下
class DemoAction: AnAction() {
override fun actionPerformed(p0: AnActionEvent?) {
Messages.showInfoMessage("Just a Test ", "來自DemoAction提示")
}
}
在 plugin.xml 中注冊 action 類的綁定:
<actions>
<!-- Add your actions here -->
<action id="com.imooc.plugins.demo.DemoAction" class="com.imooc.plugins.demo.DemoAction" text="DemoAction"
description="just a test demo">
<add-to-group group-id="ToolbarRunGroup" anchor="last"/><!--加入到ToolbarRunGroup內置組-->
</action>
</actions>
在 plugin.xml 中配置插件圖標,先在插件項目中 resource 目錄下創建一個 image 目錄或者直接把圖標拷貝目錄下即可 然后 action 標簽中指定 icon 屬性:
<actions>
<!-- Add your actions here -->
<action id="com.imooc.plugins.demo.DemoAction" class="com.imooc.plugins.demo.DemoAction" text="DemoAction"
description="just a test demo" icon="/image/icon_pic_demo.png"><!--指定圖標-->
<add-to-group group-id="ToolbarRunGroup" anchor="last"/><!--加入到ToolbarRunGroup內置組-->
</action>
</actions>
在 plugin.xml 中配置自定義組,并把自定義的組加入內置的組中:
<group id="com.imooc.plugins.group.demo" text="Demo" description="just a demo group"><!--group標簽實現自定義組,id:組的唯一標識,text:組顯示名稱,description:組的描述名-->
<add-to-group group-id="MainMenu" anchor="last"/><!--把組加入到內置的組中-->
<action id="com.imooc.plugins.demo.DemoAction" class="com.imooc.plugins.demo.DemoAction" text="DemoAction" description="just a test demo" icon="/image/icon_pic_demo.png"><!--指定圖標-->
<add-to-group group-id="ToolbarRunGroup" anchor="last"/><!--加入到ToolbarRunGroup內置組-->
</action>
</group>
配置 OK 后,現在就可以運行插件了,運行成功后會新啟動一個 Intellij IDEA,這個 IDE 就是安裝了開發的插件,然后就可以在里面去調試你的插件功能:
點擊運行,進行測試,此外還支持斷點調試:
最后一步,打包插件,并發布。選擇頂部工具欄 Build, 點擊 "Prepare Plugin Module ‘Demo’ For Deployment", 就會在當前工作目錄下生成一個 jar 或 zip 的包。然后發布插件,只需要在 jetBrains Plugins Repository 上傳你的包,等待 jetBrains 官方的審核通過了,就能通過 ide 中的 plugins 倉庫中搜索找到。
3. 圖片插件開發篇
3.1 開發的前期準備
訪問 TinyPng 官網注冊 TinyPng 開發者賬號,拿到 TinyPng ApiKey, 整個過程只需簡單注冊驗證即可:
由于本項目圖片壓縮框架是基于 TinyPng 的圖片壓縮 API 來實現的,所以需要在 TinyPng 官網提供了 develop 開發庫,可以找到相應 Java 的 jar,為了方便下載這里就直接貼出地址了:TinyPng 依賴包下載
由于圖片插件使用到 GUI,插件 GUI 采用的是 Java 中的 Swing 框架搭建,具體可以去復習相關 Swing 的知識點,當然只需要大概了解即可。此外你還需要掌握插件開發的基礎知識,Kotlin 的基本開發知識,比如 Kotlin 中擴展函數的封裝,Lambda 表達式,函數式 API,IO 流 API 的使用。
3.2 插件實現原理分析
實現的整體思路:首先我們需要找到實現關鍵點,然后從關鍵點一步步向外擴展延伸,那么實現圖片壓縮的插件的關鍵點在哪里,肯定毫無疑問是圖片壓縮 API,也就是 TinyPng API 函數調用實現。
Tinify.fromFile(inputFile).toFile(inputFile)
通過以上的 TinyPng API 就可以找到關鍵點,一個是輸入文件另一個則是輸出文件,那么我們這個圖片壓縮插件的所有實現都是圍繞著如何通過一個簡單的方式指定一個輸入文件或目錄和一個輸出文件或目錄。
沒錯就是這么簡單,那么我們一起來分析下上面兩大功能實現思路其實也很簡單:
-
功能點一:就是通過 Swing 框架中的 JFileChooser 組件,打開并指定一個圖片輸入文件或目錄和一個圖片壓縮后的輸出文件或目錄即可。
-
功能點二:通過 Intellij Idea open api 中的
DataKeys.VIRTUAL_FILE_ARRAY.getData(this)
拿到當前選中的 Virtual Files,也就是當前選中的文件把選中的文件當做輸入文件,然后圖片壓縮后文件直接輸出到源文件中即可。
注意:由于 Tiny.fromFile ().toFile () 內部源碼實際上通過 OkHttp 發送圖片壓縮的網絡請求,而且內部采用的方式是同步請求的,但是在 IDEA Plugin 開發中主線程是不能執行耗時任務的,所以需要將該 API 方法調用放在異步任務中。
3.3 項目代碼結構
action 包主要定義插件中的兩個 action,我們都知道在插件開發中 Action 是功能執行的入口,ImageSlimmingAction 是前面說到第一個功能點批量壓縮指定輸入和輸出目錄的,RightSelectedAction 是前面說過的第二個功能點在項目選中圖中文件直接右鍵壓縮的,最后這兩個 Action 都需要在 plugin.xml 中注冊。
<actions>
<action class="com.imooc.plugins.image.slimming.action.ImageSlimmingAction" text="ImageSlimming"
id="com.imooc.plugins.image.slimming.action.ImageSlimmingAction"
description="compress picture plugin" icon="/img/icon_image_slimming.png">
<add-to-group group-id="MainToolBar" anchor="after" relative-to-action="Android.MainToolBarSdkGroup"/>
</action>
<action id="com.imooc.plugins.image.action.rightselectedaction"
class="com.imooc.plugins.image.slimming.action.RightSelectedAction" text="Quick Slim Images"
description="Quick Slim Images">
<add-to-group group-id="ProjectViewPopupMenu" anchor="after" relative-to-action="ReplaceInPath"/>
</action>
</actions>
extension 包主要是定義了 Kotlin 中的擴展函數,一個是 Boolean 的擴展可以類似鏈式調用來替代 if-else 判斷,另一個則是 Dialog 使用的擴展:
//Boolean 擴展
sealed class BooleanExt<out T>
object Otherwise : BooleanExt<Nothing>()//Nothing是所有類的子類,協變的類繼承關系和泛型參數類型繼承關系一致
class TransferData<T>(val data: T) : BooleanExt<T>()
inline fun <T> Boolean.yes(block: () -> T): BooleanExt<T> = when {
this -> TransferData(block.invoke())
else -> Otherwise
}
inline fun <T> Boolean.no(block: () -> T): BooleanExt<T> = when {
this -> Otherwise
else -> TransferData(block.invoke())
}
inline fun <T> BooleanExt<T>.otherwise(block: () -> T): T = when (this) {
is Otherwise ->
block()
is TransferData ->
this.data
}
//Dialog擴展
fun Dialog.showDialog(width: Int = 550, height: Int = 400, isInCenter: Boolean = true, isResizable: Boolean = false) {
pack()
this.isResizable = isResizable
setSize(width, height)
if (isInCenter) {
setLocation(Toolkit.getDefaultToolkit().screenSize.width / 2 - width / 2, Toolkit.getDefaultToolkit().screenSize.height / 2 - height / 2)
}
isVisible = true
}
fun Project.showWarnDialog(icon: Icon = UIUtil.getWarningIcon(), title: String, msg: String, positiveText: String = "確定", negativeText: String = "取消", positiveAction: (() -> Unit)? = null, negativeAction: (() -> Unit)? = null) {
Messages.showDialog(this, msg, title, arrayOf(positiveText, negativeText), 0, icon, object : DialogWrapper.DoNotAskOption.Adapter() {
override fun rememberChoice(p0: Boolean, p1: Int) {
if (p1 == 0) {
positiveAction?.invoke()
} else if (p1 == 1) {
negativeAction?.invoke()
}
}
})
}
helper 包主要是用文件 IO 操作,由于兩個 Action 都存在圖片壓縮操作,為了復用就直接把圖片壓縮 API 調用的實現操作抽出封裝在 ImageSlimmingHelper 中,ui 包主要就是 Swing 框架中一些界面 GUI 的實現和交互。
3.4 插件實現核心點分析
插件開發中如何執行一個異步任務:
IDEA Plugin 開發和 Android 開發很類似,一些耗時的任務是不能直接在主線程執行的,需要在特定后臺線程執行,否則會阻塞主線程。在 intellij open api 中有個 Task.Backgroundable 抽象類就是處理異步任務的。Backgroundable 繼承了 Task 類以及實現了 PerformInBackgroundOption 接口。具體使用很簡單傳入兩個參數一個是 Project 對象和一個執行異步中 hint 提示文本,有四個回調函數分別為 run (progress: ProgressIndicator)、onSuccess、onThrowable、onFinished. 最后通過 queue 方法加入到異步任務隊列中。為了方便調用將其封裝成一個擴展函數來使用。
//創建后臺異步任務的Project的擴展函數asyncTask
private fun Project.asyncTask(
hintText: String,
runAction: (ProgressIndicator) -> Unit,
successAction: (() -> Unit)? = null,
failAction: ((Throwable) -> Unit)? = null,
finishAction: (() -> Unit)? = null
) {
object : Task.Backgroundable(this, hintText) {
override fun run(p0: ProgressIndicator) {
runAction.invoke(p0)
}
override fun onSuccess() {
successAction?.invoke()
}
override fun onThrowable(error: Throwable) {
failAction?.invoke(error)
}
override fun onFinished() {
finishAction?.invoke()
}
}.queue()
}
//asyncTask的使用
project?.asyncTask(hintText = "正在壓縮", runAction = {
//執行圖片壓縮操作
outputSameFile.yes {
//針對右鍵選定圖片情況,直接壓縮當前目錄選中圖片,輸出目錄包括文件也是原來的
inputFiles.forEach { inputFile -> Tinify.fromFile(inputFile.absolutePath).toFile(inputFile.absolutePath) }
}.otherwise {
inputFiles.forEach { inputFile -> Tinify.fromFile(inputFile.absolutePath).toFile(getDestFilePath(model, inputFile.name)) }
}
}, successAction = {
successAction?.invoke()
}, failAction = {
failAction?.invoke("TinyPng key存在異常,請重新輸入")
})
插件開發中如何獲取當前選中的文件或目錄
在插件開發中如何獲得當前選中文件,實際上 open api 提供了類似 DataContext 數據上下文環境,我們需要去拿到文件集合對象就需要先找到文件管理的窗口對象,還記得上篇博客中說到的 AnActionEvent 對象是插件與 IDEA 交互通信的一個媒介,通過 AnActionEvent 內部的 dataContext 的 getData 方法,傳入對應的 DataKey 對象獲得相應的窗口對象。在 CommonDataKey 中有一個 DataKey<VirtualFile []>,通過傳入當前 event 中的 dataContext 對象即可獲得當前選中的文件對象集合。
private fun DataContext.getSelectedFiles(): Array<VirtualFile>? {
return DataKeys.VIRTUAL_FILE_ARRAY.getData(this)//右鍵獲取選中多個文件,擴展函數
}
api key 的驗證和圖片壓縮的實現
在進行圖片壓縮前就是需要去驗證一下 TingPng ApiKey 的合法性,如果第一次驗證合法就需要把該 ApiKey 存儲在本地,下次壓縮就直接使用本地的 key 進行壓縮,一旦本地 key 失效后,需要重新彈出 TinyPng apikey 的驗證提示框,進行重新認證。當然需要注意的是驗證 api key 的合法性也是進行一次同步的網絡請求所以它也要放在異步任務執行。
fun checkApiKeyValid(
project: Project?,
apiKey: String,
validAction: (() -> Unit)? = null,
invalidAction: ((String) -> Unit)? = null
) {
if (apiKey.isBlank()) {
invalidAction?.invoke("TinyPng key為空,請重新輸入")
}
project?.asyncTask(hintText = "正在檢查key是否合法", runAction = {
try {
Tinify.setKey(apiKey)
Tinify.validate()
} catch (exception: Exception) {
throw exception
}
}, successAction = {
validAction?.invoke()
}, failAction = {
println("驗證Key失敗!!${it.message}")
invalidAction?.invoke("TinyPng key驗證失敗,請重新輸入")
})
}
然后就是利用異步任務進行圖片壓縮操作。
4. 總結
到這里有關 Kotlin 實現圖片插件實戰篇就結束了,本篇文章篇幅比較多,主要介紹了如何去開發一個 IDEA 插件以及使用 Kotlin 去開發一個圖片壓縮插件具體實現方案和技術重點和難點。下篇文章我們將繼續 Kotlin 的實戰篇系列。