简述: 因为我的缘由,已经有很长一段时间没有写过文章,有句话是那么说的只要开始就不会太晚,因此咱们开始《用Kotlin撸一个图片压缩插件》系列文章最后一篇实战篇。实际上我已经把源码发布到了GitHub,代码很简单。有了前两篇文章的基础,这篇文章将会使用Kotlin从零开始带你撸个图片压缩插件。java
三、因为图片插件使用到GUI,插件GUI采用的是Java中的Swing框架搭建,具体能够去复习相关Swing的知识点,固然只须要大概了解便可,毕竟这个不是重点。git
四、须要去掌握插件开发的基础知识,因为本篇文章是实战篇就不去细讲插件基础知识,具体详情可参照该系列的第二篇文章用Kotlin撸一个图片压缩插件-插件基础篇(二)github
五、须要有Kotlin的基本开发知识,好比Kotlin中扩展函数的封装,Lambda表达式,函数式API,IO流API的使用api
图片压缩插件主要支持以下两大功能:浏览器
实现的总体思路:首先咱们须要找到实现关键点,而后从关键点一步步向外扩展延伸,那么实现图片压缩的插件的关键点在哪里,确定毫无疑问是图片压缩API,也就是TinyPng API函数调用实现。网络
Tinify.fromFile(inputFile).toFile(inputFile)
复制代码
经过以上的TinyPng API就能够找到关键点,一个是输入文件另外一个则是输出文件,那么咱们这个图片压缩插件的全部实现都是围绕着如何经过一个简单的方式指定一个输入文件或目录和一个输出文件或目录。app
没错就是这么简单,那么咱们一块儿来分析下上面两大功能实现思路其实也很简单:框架
功能点一: 就是经过Swing框架中的JFileChooser组件,打开并指定一个图片输入文件或目录和一个图片压缩后的输出文件或目录便可。异步
功能点二: 经过Intellij Idea open api中的 DataKeys.VIRTUAL_FILE_ARRAY.getData(this)
拿到当前选中的Virtual Files,也就是当前选中的文件把选中的文件当作输入文件,而后图片压缩后文件直接输出到源文件中便可。async
注意: 因为Tiny.fromFile().toFile()内部源码实际上经过OkHttp发送图片压缩的网络请求,并且内部采用的方式是同步请求的,可是在IDEA Plugin开发中主线程是不能执行耗时任务的,因此须要将该API方法调用放在异步任务中
<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>
复制代码
//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)//右键获取选中多个文件,扩展函数
}
复制代码
关于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);
}
}
}
复制代码
在进行图片压缩前就是须要去验证一下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,欢迎加入咱们~~~