用Kotlin撸一个图片压缩插件-实战篇(三)

简述: 由于个人原因,已经有很长一段时间没有写过文章,有句话是那么说的只要开始就不会太晚,所以我们开始《用Kotlin撸一个图片压缩插件》系列文章最后一篇实战篇。实际上我已经把源码发布到了GitHub,代码很简单。有了前两篇文章的基础,这篇文章将会使用Kotlin从零开始带你撸个图片压缩插件。

一、开发前期准备工作

  • 1、访问TinyPng官网注册TinyPng开发者账号,拿到TinyPng
    ApiKey,整个过程只需简单注册验证即可。

用Kotlin撸一个图片压缩插件-实战篇(三)_第1张图片

  • 2、由于本项目图片压缩框架是基于TinyPng的图片压缩API来实现的,所以需要在TinyPng官网提供了develop开发库,可以找到相应Java的jar,为了方便下载这里就直接贴出地址了:TinyPng依赖包下载

用Kotlin撸一个图片压缩插件-实战篇(三)_第2张图片

  • 3、由于图片插件使用到GUI,插件GUI采用的是Java中的Swing框架搭建,具体可以去复习相关Swing的知识点,当然只需要大概了解即可,毕竟这个不是重点。

  • 4、需要去掌握插件开发的基础知识,由于本篇文章是实战篇就不去细讲插件基础知识,具体详情可参照该系列的第二篇文章用Kotlin撸一个图片压缩插件-插件基础篇(二)

  • 5、需要有Kotlin的基本开发知识,比如Kotlin中扩展函数的封装,Lambda表达式,函数式API,IO流API的使用

二、图片压缩插件基本功能点

图片压缩插件主要支持如下两大功能:

  • 1、支持指定图片源输入目录批量压缩到一个指定的输出目录。

用Kotlin撸一个图片压缩插件-实战篇(三)_第3张图片

  • 2、支持在AndroidStudio项目中直接选中指定的一个或多个图片,右键点击直接压缩。

用Kotlin撸一个图片压缩插件-实战篇(三)_第4张图片

三、实现思路分析

实现的整体思路:首先我们需要找到实现关键点,然后从关键点一步步向外扩展延伸,那么实现图片压缩的插件的关键点在哪里,肯定毫无疑问是图片压缩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撸一个图片压缩插件-实战篇(三)_第5张图片

  • 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,通过传入当前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系列文章,欢迎查看:

Kotlin邂逅设计模式系列:

  • 当Kotlin完美邂逅设计模式之单例模式(一)

数据结构与算法系列:

  • 每周一算法之二分查找(Kotlin描述)

翻译系列:

  • [译] Kotlin中关于Companion Object的那些事
  • [译]记一次Kotlin官方文档翻译的PR(内联类)
  • [译]Kotlin中内联类的自动装箱和高性能探索(二)
  • [译]Kotlin中内联类(inline class)完全解析(一)
  • [译]Kotlin的独门秘籍Reified实化类型参数(上篇)
  • [译]Kotlin泛型中何时该用类型形参约束?
  • [译] 一个简单方式教你记住Kotlin的形参和实参
  • [译]Kotlin中是应该定义函数还是定义属性?
  • [译]如何在你的Kotlin代码中移除所有的!!(非空断言)
  • [译]掌握Kotlin中的标准库函数: run、with、let、also和apply
  • [译]有关Kotlin类型别名(typealias)你需要知道的一切
  • [译]Kotlin中是应该使用序列(Sequences)还是集合(Lists)?
  • [译]Kotlin中的龟(List)兔(Sequence)赛跑

原创系列:

  • 教你如何完全解析Kotlin中的类型系统
  • 如何让你的回调更具Kotlin风味
  • Jetbrains开发者日见闻(三)之Kotlin1.3新特性(inline class篇)
  • JetBrains开发者日见闻(二)之Kotlin1.3的新特性(Contract契约与协程篇)
  • JetBrains开发者日见闻(一)之Kotlin/Native 尝鲜篇
  • 教你如何攻克Kotlin中泛型型变的难点(实践篇)
  • 教你如何攻克Kotlin中泛型型变的难点(下篇)
  • 教你如何攻克Kotlin中泛型型变的难点(上篇)
  • Kotlin的独门秘籍Reified实化类型参数(下篇)
  • 有关Kotlin属性代理你需要知道的一切
  • 浅谈Kotlin中的Sequences源码解析
  • 浅谈Kotlin中集合和函数式API完全解析-上篇
  • 浅谈Kotlin语法篇之lambda编译成字节码过程完全解析
  • 浅谈Kotlin语法篇之Lambda表达式完全解析
  • 浅谈Kotlin语法篇之扩展函数
  • 浅谈Kotlin语法篇之顶层函数、中缀调用、解构声明
  • 浅谈Kotlin语法篇之如何让函数更好地调用
  • 浅谈Kotlin语法篇之变量和常量
  • 浅谈Kotlin语法篇之基础语法

Effective Kotlin翻译系列

  • [译]Effective Kotlin系列之考虑使用原始类型的数组优化性能(五)
  • [译]Effective Kotlin系列之使用Sequence来优化集合的操作(四)
  • [译]Effective Kotlin系列之探索高阶函数中inline修饰符(三)
  • [译]Effective Kotlin系列之遇到多个构造器参数要考虑使用构建器(二)
  • [译]Effective Kotlin系列之考虑使用静态工厂方法替代构造器(一)

实战系列:

  • 用Kotlin撸一个图片压缩插件ImageSlimming-导学篇(一)
  • 用Kotlin撸一个图片压缩插件-插件基础篇(二)
  • 用Kotlin撸一个图片压缩插件-实战篇(三)
  • 浅谈Kotlin实战篇之自定义View图片圆角简单应用

你可能感兴趣的:(Kotlin,Kotlin,Android,Intellij,IDEA插件,图片压缩插件,java)