用Kotlin擼一個圖片壓縮外掛-實戰篇(三)

極客熊貓發表於2018-08-18

簡述: 由於個人原因,已經有很長一段時間沒有寫過文章,有句話是那麼說的只要開始就不會太晚,所以我們開始《用Kotlin擼一個圖片壓縮外掛》系列文章最後一篇實戰篇。實際上我已經把原始碼釋出到了GitHub,程式碼很簡單。有了前兩篇文章的基礎,這篇文章將會使用Kotlin從零開始帶你擼個圖片壓縮外掛。

一、開發前期準備工作

  • 1、訪問TinyPng官網註冊TinyPng開發者賬號,拿到TinyPng ApiKey,整個過程只需簡單註冊驗證即可。

用Kotlin擼一個圖片壓縮外掛-實戰篇(三)

  • 2、由於本專案圖片壓縮框架是基於TinyPng的圖片壓縮API來實現的,所以需要在TinyPng官網提供了develop開發庫,可以找到相應Java的jar,為了方便下載這裡就直接貼出地址了:TinyPng依賴包下載

用Kotlin擼一個圖片壓縮外掛-實戰篇(三)

  • 3、由於圖片外掛使用到GUI,外掛GUI採用的是Java中的Swing框架搭建,具體可以去複習相關Swing的知識點,當然只需要大概瞭解即可,畢竟這個不是重點。

  • 4、需要去掌握外掛開發的基礎知識,由於本篇文章是實戰篇就不去細講外掛基礎知識,具體詳情可參照該系列的第二篇文章用Kotlin擼一個圖片壓縮外掛-外掛基礎篇(二)

  • 5、需要有Kotlin的基本開發知識,比如Kotlin中擴充套件函式的封裝,Lambda表示式,函式式API,IO流API的使用

二、圖片壓縮外掛基本功能點

圖片壓縮外掛主要支援如下兩大功能:

  • 1、支援指定圖片源輸入目錄批量壓縮到一個指定的輸出目錄。

用Kotlin擼一個圖片壓縮外掛-實戰篇(三)

  • 2、支援在AndroidStudio專案中直接選中指定的一個或多個圖片,右鍵點選直接壓縮。

用Kotlin擼一個圖片壓縮外掛-實戰篇(三)

三、實現思路分析

實現的整體思路:首先我們需要找到實現關鍵點,然後從關鍵點一步步向外擴充套件延伸,那麼實現圖片壓縮的外掛的關鍵點在哪裡,肯定毫無疑問是圖片壓縮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方法呼叫放在非同步任務中

四、程式碼結構和實現

用Kotlin擼一個圖片壓縮外掛-實戰篇(三)

  • action包:主要定義外掛中的兩個action,我們都知道在外掛開發中Action是功能執行的入口,ImageSlimmingAction是前面說到第一個功能點批量壓縮指定輸入和輸出目錄的,RightSelectedAction是前面說過的第二個功能點在專案選中圖中檔案直接右鍵壓縮的, 最後這兩個Action都需要在plugin.xml中註冊。
  <actions>
        <action class="com.mikyou.plugins.image.slimming.action.ImageSlimmingAction" text="ImageSlimming"
                id="com.mikyou.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.mikyou.plugins.image.action.rightselectedaction"
                class="com.mikyou.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的實現和互動。

四、實現的關鍵技術點

  • 關鍵點一: 外掛開發中如何執行一個非同步任務

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)//右鍵獲取選中多個檔案,擴充套件函式
    }
複製程式碼
  • 關鍵點三: Swing中JFileChooser元件的使用

關於JFileChooser元件的使用就比較簡單了,這裡就不去詳細介紹,程式碼也很簡單

  private void openFileAndSetPath(JComboBox<String> cBoxPath, int selectedMode, Boolean isSupportMultiSelect) {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setFileSelectionMode(selectedMode);
        fileChooser.setMultiSelectionEnabled(isSupportMultiSelect);
        //設定檔案擴充套件過濾器
        if (selectedMode != JFileChooser.DIRECTORIES_ONLY) {
            fileChooser.addChoosableFileFilter(new FileNameExtensionFilter(".png", "png"));
            fileChooser.addChoosableFileFilter(new FileNameExtensionFilter(".jpg", "jpg"));
            fileChooser.addChoosableFileFilter(new FileNameExtensionFilter(".jpeg", "jpeg"));
        }

        fileChooser.showOpenDialog(null);


        if (selectedMode == JFileChooser.DIRECTORIES_ONLY) {//僅僅選擇目錄情況,不存在多檔案選中
            File selectedDir = fileChooser.getSelectedFile();
            if (selectedDir != null) {
                cBoxPath.insertItemAt(selectedDir.getAbsolutePath(), 0);
                cBoxPath.setSelectedIndex(0);
            }
        } else {//選擇含有檔案情況,包括僅僅 選擇檔案 和 同時選擇檔案和目錄,
            File[] selectedFiles = fileChooser.getSelectedFiles();
            if (selectedFiles != null && selectedFiles.length > 0) {
                cBoxPath.insertItemAt(getSelectedFilePath(selectedFiles), 0);
                cBoxPath.setSelectedIndex(0);
            }
        }

    }
複製程式碼
  • 關鍵點四: 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驗證失敗,請重新輸入")
    })
}
複製程式碼

然後就是利用非同步任務進行圖片壓縮操作。

fun slimImage(
        project: Project?,
        inputFiles: List<File>,
        model: ImageSlimmingModel = ImageSlimmingModel("", "", "", ""),
        successAction: (() -> Unit)? = null,
        outputSameFile: Boolean = false,
        failAction: ((String) -> Unit)? = null
) {
    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存在異常,請重新輸入")
    })
}
複製程式碼

五、總結

到這裡《用Kotlin擼一個圖片壓縮外掛》系列文章就結束了,其實實現起來挺簡單的,其中主要的關鍵點就是需要更加熟悉使用Intellij open api, 然後其他就是運用好Kotlin的一些語法特性,其餘的都很簡單。而且個人覺得把圖片壓縮做成一個外掛會變得很高效,不然傳統的模式得需要把圖片拖到瀏覽器中然後一個一個下載下來,還有的人問我不就是一個指令碼能解決的嗎?指令碼個人覺得不夠靈活不能像外掛一樣任意在專案中選中一張或多張圖片直接右鍵壓縮。如有什麼問題歡迎下方留言,謝謝。

外掛專案原始碼地址

用Kotlin擼一個圖片壓縮外掛-實戰篇(三)

歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~

相關文章