简述: 前两天写了篇用Kotlin撸一个图片压缩插件-导学篇,现在迎来了插件基础篇,没错这篇文章就是教你如何一步一步从零开始写一个插件,包括插件项目构建,运行,调试到最后的上线发布整个流程。如果你是插件零基础的小白,那么这篇文章适合你,而且这篇文章也是下面实战篇的基础.

插播一条消息(有人提需求了)

ImageSlimming图片压缩插件开发完成后,马上就把它推荐给团队内部人员使用,在周会上就有同事提出了一个需求,就是在AndroidStudio项目中,可以任意选中res目录下一张或多张图片,然后直接右键选择,就可以实现图片压缩。然后思考了一波,这个需求挺好的,心里大概想了下,今晚就去把它实现了。实现效果大概如下:

实现这个功能后,把V1.1版本的代码做了很大的结构上调整,抽离出一些公共的顶层函数和扩展函数,目前这个功能代码已经更新到GitHub上了,请认准feature-image-slimming-v1.2分支。

一、什么是IDE(JetBrains全家桶)插件

IDE插件利用jetBrains公司开源的IntelliJ Platform SDK(java语言)来开发一个独立功能可以安装在IDEA之类的编辑器的功能组件。 IDE插件是基于IntelliJ IDEA开发工具开发,里面集成了插件的项目的构建。采用的是Java语言开发和IntelliJ的SDK相结合开发。并且在开发出来的插件不仅在AndroidStudio上可以使用,可以通用于jetBrains的编辑器的全家桶工具。通过源码可以发现Intellij Idea内置了大量的插件,可以这么说Intellij Idea开发工具大部分功能是由插件组合而成的。

二、开始构建你的第一个插件项目

注意: 构建插件项目的方式主要有两种:

一种是直接创建IDEA内置的插件项目.

另一种则是先通过构建一个gradle项目,然后加入plugin.xml配置以及 加入IDEA ERP的依赖,然后来构建一个插件项目(整个开发过程就和开发一个Android项目一样),当然这个构建过程可参考官方给出的gradle-intellij-plugin项目来实现。不过在最新2018.1.1之后版本中,IDEA内部也提供了构建grale插件项目入口,具体可下载新版本Intellij Idea。

  • 1、(这里我们以第一种为例)打开已经安装好的IntelliJ IDEA,然后create New Project. 选择一个IntelliJ Platform Plugin项目。注意需要引入IntelliJ IDEA的SDK

  • 2、选择好SDK后,然后只需要一步一步把项目创建完毕即可,创建好的项目结构如下:

  • 3、正如你所看到,生成了一个plugin.xml,这个文件是插件项目的配置文件,它记录了插件相关的版本扩展等基本信息,还记录了插件事件与具体实现类绑定过程,下面就一一介绍每个标签的含义。
<idea-plugin><id>com.your.company.unique.plugin.id</id><name>Plugin display name here</name><version>1.0</version><vendor email="support@yourcompany.com" 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.htmlon 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>

id标签: plugin插件项目的标识,和Android项目中的package功能类似。唯一标识一个插件项目。

name标签: 插件名字,发布到jetBrains plugin仓库中会用这个。

version标签: 插件版本号,这个用于标识插件版本,一般用于更新jetbrains plugins仓库中插件版本标识。

vendor标签: 开发者信息,邮箱和个人主页,公司名字或个人开发者姓名,用于插件仓库中插件信息介绍显示。

description标签: 插件的描述信息,主要是描述插件有什么功能。支持<![CDATA[...]]>标签内部内嵌HTML标签。

changNote标签: 一般用于插件版本变更的信息。支持<![CDATA[...]]>标签内部内嵌HTML标签。

idea-version标签: 这个版本标签需要注意下,它决定了该插件能够运行在最低版本的IDEA中,一旦配置不当,会导致插件安装不成功,有点类似Android中AndroidManifest.xml中配置最低兼容Android版本意思。

depends标签: 表示当前的插件项目依赖哪些内置或者外部的插件库依赖,例如你需要实现类似git功能插件,你就可以通过depends标签引入Git4Idea即可,<depends>Git4Idea</depends>,如果看过IDEA源码的话,实际上内置GitHub插件就是通过depends依赖内部Git4Idea插件实现的,还有现在的码云git工具插件也是通过依赖Git4Idea内置插件来实现的

extension标签: 插件与其他插件或与IDE本身交互。(默认是IDEA)如果您希望插件扩展其他插件或IntelliJ Platform的功能,则必须声明一个或多个扩展名。

  <extensions defaultExtensionNs="com.intellij"><appStarter implementation="MyTestPackage.MyTestExtension1" /><applicationConfigurable implementation="MyTestPackage.MyTestExtension2" /></extensions>

action标签: 这个标签非常重要,它决定了你的插件在IDE上显示的位置和顺序,以及这个插件的点击事件和插件项目Action实现类的绑定。

  • 4、创建一个Action类,在IDEA插件项目中,IDEA点击Item或者按钮或者一个图标对应是触发了插件中一个Action,创建Action主要有两种方式:

第一种:就是通过IDEA提供的一个入口,直接去创建Action,然后它自动帮你实现plugin.xml中的事件绑定的注册

注意点一: 定义的Action最好要加入到一个IDE中内置组中,这样才能容易在对应组中找到插件,并运行插件。可能会有人问了,列举出来那么多z在我哪知道对应运行起来IDEA哪个地方,有小技巧看下对应组中小括号中的描述内容,然后就是选中一个组,看看里面都有哪些组,大概就能猜到对应IDEA哪个地方,最笨办法就是测试运行下即可,建议把测试结果记录下来,后续就方便了。

注意点二: 除了把定义的action加入到内置的组中,还可以加入自定义组中,如何自定义组下面第二种方法会讲述,但是还是需要自定义组加入内置的组中,所以一般都是需要把action直接或间接加入到内置的组中。

注意点三: Action还可以配置icon,也就是常见点击icon图标就执行插件,如何配置图标在下面第二种方法会有介绍。

第二种:手动创建一个Action类,然后继承AnAction类或者DumbAwareAction类,然后在plugin.xml中的action标签去注册action类与事件绑定

创建Action类:

package com.mikyou.plugins.demoimport 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.mikyou.plugins.demo.DemoAction" class="com.mikyou.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.mikyou.plugins.demo.DemoAction" class="com.mikyou.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.mikyou.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.mikyou.plugins.demo.DemoAction" class="com.mikyou.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>
  • 5、配置OK后,现在就可以运行插件了,运行成功后会新启动一个Intellij IDEA,这个IDE就是安装了开发的插件,然后就可以在里面去调试你的插件功能。

  • 6、点击运行,进行测试

  • 7、你可以打断点,点击debug,然后就可以断点调试代码。

  • 8、最后一步,打包插件,并发布。选择顶部工具栏Build, 点击"Prepare Plugin Module ‘Demo’ For Deployment",就会在当前工作目录下生成一个jar或zip的包。然后发布插件,只需要在jetBrains Plugins Repository上传你的包,等待jetBrains官方的审核通过了,就能通过ide中的plugins仓库中搜索找到。

三、从源码分析插件中AnAction

  • 1、插件中的AnAction类

插件开发最为重要之一的就是Action类了,可以说它是插件功能的一个入口,编写一个Action类,一般会去继承AnAction类,AnAction是一个抽象类,必须要去实现actionPerformed方法,这个方法是在用户触发插件的点击事件后回调的,所以类似于打开对话框,执行某个功能的逻辑可以写在里面等等。单从插件开发角度(插件的生命周期除外)来说,可以把当它当做程序中的main函数。

  • 2、插件中的AnAction类中的actionPerformed方法

首先创建一个DemoAction继承AnAction

class DemoAction: AnAction() {override fun actionPerformed(p0: AnActionEvent?) {Messages.showInfoMessage("Just a Test ", "来自DemoAction提示")}
}

然后看下AnAction重载的第三个构造器,会去拿到Presentation类的对象,准确来说这个对象保存了插件是否可见、是否可用、插件的Icon以及插件显示在IDE中的外观控制信息,可以说是插件外观信息和控制的实体。

public AnAction() {this.myShortcutSet = CustomShortcutSet.EMPTY;this.myIsDefaultIcon = true;}public AnAction(Icon icon) {this((String)null, (String)null, icon);}public AnAction(@Nullable String text) {this(text, (String)null, (Icon)null);}public AnAction(@Nullable String text, @Nullable String description, @Nullable Icon icon) {this.myShortcutSet = CustomShortcutSet.EMPTY;this.myIsDefaultIcon = true;Presentation presentation = this.getTemplatePresentation();presentation.setText(text);//设置插件显示文本presentation.setDescription(description);//设置插件描述文件信息presentation.setIcon(icon);//设置插件的图标}

构建好自定义Action实体,外部调用方会触发actionPerformed方法,请注意actionPerformed方法带了一个AnActionEvent对象,它有个getData方法可以拿到IDEA很多窗口对象,但是实际上内部通过委托它的dataContext成员对象的getData方式实现的,它很重要代表上下文环境,相当于Android开发中的Context,可以通过它内部的dataContext中的getData方法可以得到IDEA界面各个窗口对象以及各个窗口为实现某些特定功能的对象。例如Project对象,VirtualFile对象、Editor对象、PsiFile持久化文件对象等等,毫不夸张的说后续插件功能开发都是围绕它来展开的,下面会详细描述。

  • 2、插件中的AnAction类中的update方法
class DemoAction: AnAction() {override fun actionPerformed(p0: AnActionEvent?) {Messages.showInfoMessage("Just a Test ", "来自DemoAction提示")}override fun update(e: AnActionEvent?) {super.update(e)}
}

update方法是在Action状态发生变化的时被回调,当Action状态更新时,update函数被IDEA回调,并且传递AnActionEvent对象参数,AnAction对象中封装了当前Action对应的上下文环境。 也就是说我们前面所讲的需要把action加入到组,才有可能得到显示,因为在action组显示的时候,该组内部的所有action中的update方法都会被回调,所以一个插件的update方法会比actionPerformed先执行,而且是有可能多次执行,也就是一个插件最开始得先显示出来并且可操作,然后才是点击触发action事件。所以也就产生一个场景的应用就是细心小伙伴会发现有时候右侧菜单中item是灰色的点不动,有时候可以,有时候不显示,有时候又是可以显示的。这些判断的逻辑一般是在update方法中执行的。

  • 3、插件中的AnAction类中的AnActionEvent

AnActionEvent对象,actionPerformed和update方法都会携带一个AnActionEvent对象,可以说它是插件与IDEA交互通信的一个媒介,通过AnActionEvent内部的dataContext的getData方法,传入对应的DataKey对象获得相应的窗口对象

 @Nullablepublic <T> T getData(@NotNull DataKey<T> key) {if (key == null) {$$$reportNull$$$0(28);}return this.getDataContext().getData(key);//委托给DataContext对象getData方法实现
}
  • 4、AnActionEvent获得当前Project对象,引出CommonDataKeys
    @Nullablepublic Project getProject() {return (Project)this.getData(CommonDataKeys.PROJECT);}

可以看到是通过AnActionEvent.getData方法传入一个CommonDataKeys.PROJECT参数,拿到Project对象,那么CommonDataKeys是不是一个key的集合呢?接着看会发现有很多对象key,例如Editor、VirtualFile、PsiFile对象等等。

public class CommonDataKeys {public static final DataKey<Project> PROJECT = DataKey.create("project");public static final DataKey<Editor> EDITOR = DataKey.create("editor");public static final DataKey<Editor> HOST_EDITOR = DataKey.create("host.editor");public static final DataKey<Caret> CARET = DataKey.create("caret");public static final DataKey<Editor> EDITOR_EVEN_IF_INACTIVE = DataKey.create("editor.even.if.inactive");public static final DataKey<Navigatable> NAVIGATABLE = DataKey.create("Navigatable");public static final DataKey<Navigatable[]> NAVIGATABLE_ARRAY = DataKey.create("NavigatableArray");public static final DataKey<VirtualFile> VIRTUAL_FILE = DataKey.create("virtualFile");public static final DataKey<VirtualFile[]> VIRTUAL_FILE_ARRAY = DataKey.create("virtualFileArray");public static final DataKey<PsiElement> PSI_ELEMENT = DataKey.create("psi.Element");public static final DataKey<PsiFile> PSI_FILE = DataKey.create("psi.File");public static final DataKey<Boolean> EDITOR_VIRTUAL_SPACE = DataKey.create("editor.virtual.space");public CommonDataKeys() {}
}
  • 5、继续深入CommonDataKeys挖掘它是否有什么子类,或许能够发现更多key集合,拿到更多对象对应key,意味着你开发IDEA插件使用的API会更广,也会更快更好实现需求开发。这里我会教你如何去使用upsource在线查看IDEA的源码,去查看CommonDataKeys的子类。

通过以上图示操作,会发现CommonDataKeys还有个子类PlatformDataKeys,PlatformDataKeys又有个子类LangDataKeys,所以这里列举下获取相关对象的key,以后开发需要哪个对象,直接查阅也很方便。

public class PlatformDataKeys extends CommonDataKeys {public static final DataKey<FileEditor> FILE_EDITOR = DataKey.create("fileEditor");public static final DataKey<String> FILE_TEXT = DataKey.create("fileText");public static final DataKey<Boolean> IS_MODAL_CONTEXT = DataKey.create("isModalContext");public static final DataKey<DiffViewer> DIFF_VIEWER = DataKey.create("diffViewer");public static final DataKey<DiffViewer> COMPOSITE_DIFF_VIEWER = DataKey.create("compositeDiffViewer");public static final DataKey<String> HELP_ID = DataKey.create("helpId");public static final DataKey<Project> PROJECT_CONTEXT = DataKey.create("context.Project");public static final DataKey<Component> CONTEXT_COMPONENT = DataKey.create("contextComponent");public static final DataKey<CopyProvider> COPY_PROVIDER = DataKey.create("copyProvider");public static final DataKey<CutProvider> CUT_PROVIDER = DataKey.create("cutProvider");public static final DataKey<PasteProvider> PASTE_PROVIDER = DataKey.create("pasteProvider");public static final DataKey<DeleteProvider> DELETE_ELEMENT_PROVIDER = DataKey.create("deleteElementProvider");public static final DataKey<Object> SELECTED_ITEM = DataKey.create("selectedItem");public static final DataKey<Object[]> SELECTED_ITEMS = DataKey.create("selectedItems");public static final DataKey<Rectangle> DOMINANT_HINT_AREA_RECTANGLE = DataKey.create("dominant.hint.rectangle");public static final DataKey<ContentManager> CONTENT_MANAGER = DataKey.create("contentManager");public static final DataKey<ToolWindow> TOOL_WINDOW = DataKey.create("TOOL_WINDOW");public static final DataKey<TreeExpander> TREE_EXPANDER = DataKey.create("treeExpander");public static final DataKey<ExporterToTextFile> EXPORTER_TO_TEXT_FILE = DataKey.create("exporterToTextFile");public static final DataKey<VirtualFile> PROJECT_FILE_DIRECTORY = DataKey.create("context.ProjectFileDirectory");public static final DataKey<Disposable> UI_DISPOSABLE = DataKey.create("ui.disposable");public static final DataKey<ContentManager> NONEMPTY_CONTENT_MANAGER = DataKey.create("nonemptyContentManager");public static final DataKey<ModalityState> MODALITY_STATE = DataKey.create("ModalityState");public static final DataKey<Boolean> SOURCE_NAVIGATION_LOCKED = DataKey.create("sourceNavigationLocked");public static final DataKey<String> PREDEFINED_TEXT = DataKey.create("predefined.text.value");public static final DataKey<String> SEARCH_INPUT_TEXT = DataKey.create("search.input.text.value");public static final DataKey<Object> SPEED_SEARCH_COMPONENT = DataKey.create("speed.search.component.value");public static final DataKey<Point> CONTEXT_MENU_POINT = DataKey.create("contextMenuPoint");@Deprecatedpublic static final DataKey<Comparator<? super AnAction>> ACTIONS_SORTER = DataKey.create("actionsSorter");
}public class LangDataKeys extends PlatformDataKeys {public static final DataKey<Module> MODULE = DataKey.create("module");public static final DataKey<Module> MODULE_CONTEXT = DataKey.create("context.Module");public static final DataKey<Module[]> MODULE_CONTEXT_ARRAY = DataKey.create("context.Module.Array");public static final DataKey<ModifiableModuleModel> MODIFIABLE_MODULE_MODEL = DataKey.create("modifiable.module.model");public static final DataKey<Language> LANGUAGE = DataKey.create("Language");public static final DataKey<Language[]> CONTEXT_LANGUAGES = DataKey.create("context.Languages");public static final DataKey<PsiElement[]> PSI_ELEMENT_ARRAY = DataKey.create("psi.Element.array");public static final DataKey<IdeView> IDE_VIEW = DataKey.create("IDEView");public static final DataKey<Boolean> NO_NEW_ACTION = DataKey.create("IDEview.no.create.element.action");public static final DataKey<Condition<AnAction>> PRESELECT_NEW_ACTION_CONDITION = DataKey.create("newElementAction.preselect.id");public static final DataKey<PsiElement> TARGET_PSI_ELEMENT = DataKey.create("psi.TargetElement");public static final DataKey<Module> TARGET_MODULE = DataKey.create("module.TargetModule");public static final DataKey<PsiElement> PASTE_TARGET_PSI_ELEMENT = DataKey.create("psi.pasteTargetElement");public static final DataKey<ConsoleView> CONSOLE_VIEW = DataKey.create("consoleView");public static final DataKey<JBPopup> POSITION_ADJUSTER_POPUP = DataKey.create("chooseByNameDropDown");public static final DataKey<JBPopup> PARENT_POPUP = DataKey.create("chooseByNamePopup");public static final DataKey<Library> LIBRARY = DataKey.create("project.model.library");public static final DataKey<RunProfile> RUN_PROFILE = DataKey.create("runProfile");public static final DataKey<ExecutionEnvironment> EXECUTION_ENVIRONMENT = DataKey.create("executionEnvironment");public static final DataKey<RunContentDescriptor> RUN_CONTENT_DESCRIPTOR = DataKey.create("RUN_CONTENT_DESCRIPTOR");
}

四、插件开发一些建议

  • 1、建议多查看官方API文档,尽管我认为官方文档写得不是很好,但是这是一条深入学习插件开发比较快的途径。
  • 2、建议多查看一下IDE内置插件的源码,这是我认为深入学习插件开发最好方法,例如Git4Idea内置的git插件,深入它的源码,你会发现IDE中pull,push,checkout,branch每个功能具体实现是怎样的。而且还有个好处,你会模仿使用一些内置插件使用过的API,比如如何执行后台的线程任务,如何操作文件系统(插件内部文件)。
  • 3、最后一个,也是最重要的一点就是你的idea想法,插件开发只是个工具,最关键是想法,如何把一个比较繁杂操作简化成使用插件来实现

五、插件开发一些资源

  • 官方文档
  • 插件资源集合
  • IntelliJ IDEA 架构概述(面向插件开发者)
  • 码云Intellij Idea Git插件工具源码

最后到这里,插件开发基础篇就结束,下一篇就是本系列完结实战开发篇,欢迎继续关注~~~

欢迎关注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撸一个图片压缩插件-插件基础篇(二)相关推荐

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

    简述: 由于个人原因,已经有很长一段时间没有写过文章,有句话是那么说的只要开始就不会太晚,所以我们开始<用Kotlin撸一个图片压缩插件>系列文章最后一篇实战篇.实际上我已经把源码发布到了 ...

  2. 用Kotlin撸一个图片压缩插件ImageSlimming-导学篇(一)

    简述: 很久没有发布Kotlin的实战相关的内容,这段时间在折腾Intellij IDEA的插件开发,折腾出了几个小插件,因为最近公司业务分离,原来堆在基础业务那边模块,都以模块的形式抽离出来,独立仓 ...

  3. vue3 开发一个图片预览插件

    vue3 的插件开发和vue2思路类似但是写法却迥异.主要变化在vue3没有了extend构造器. 这次我们通过一个图片预览插件,贯通整个vue3插件开发的过程. 1 在src下新建lplugins文 ...

  4. 插件化基础(二)——加载插件资源

    系列文章目录: 插件化基础(一)--加载插件的类 插件化基础(二)--加载插件资源 插件化基础(三)--启动插件组件 一.了解 Asset 和 Resources 我们加载的资源通常来自 res 和 ...

  5. 小程序工程化实践(上篇)-- 手把手教你撸一个小程序 webpack 插件,一个例子带你熟悉 webpack 工作流程...

    本文基于 webpack 4 和 babel 7,Mac OS,VS Code 小程序开发现状: 小程序开发者工具不好用,官方对 npm 的支持有限,缺少对 webpack, babel 等前端常用工 ...

  6. fwslider--属于自己的一个图片轮播插件

    一直在学习,一直在提升,一直在想什么时候自己也能写个简单的图片轮播啊? 终于,我写出来了,虽然没有别人写的那么牛逼,我也没有办法和大牛比,所以跟自己比就好了,自己有提升就很开心,这个图片轮播写出来一段 ...

  7. 【转载】企业级服务器设计与实现经验之插件系统基础篇

    最初之所以要采用插件的形式进行开发,主要是为了解决功能服务的"热插拔"问题,在决定采用"框架+插件"的方式进行设计后,我们就更进一步,打算将一个个可以分割开来的 ...

  8. 在线地图插件forarcmap_QGIS基础篇插件安装(在线地图纠偏)

    QGIS基础篇,可以关注微信公众号,发送关键字获取相关文章. 本文主要介绍,QGIS插件安装,QGIS插件是基于Python和Qt开发的,通过其插件平台,可以安装很多有用的插件. 1. 插件安装,主要 ...

  9. golang游戏开发学习笔记-开发一个简单的2D游戏(基础篇)

    此文写在golang游戏开发学习笔记-创建一个能自由探索的3D世界之后,感兴趣可以先去那篇文章了解一些基础知识,在这篇文章里我们要创建一个简单的2D游戏场景以及配套的人物,并实现人物运动和碰撞检测功能 ...

最新文章

  1. 项目三(2)——抽象类
  2. Button的使用(七):RadioGroup、RadioButton
  3. 2019区块链广泛应用于能源领域 ,DMA基金会,服务于区块链产业
  4. js中关于new Object时传参的一些细节分析
  5. 【扫盲】小白基础-SDN详解
  6. 四川大学计算机学院录取,四川大学计算机学院2018年硕士研究生招生拟录取名单及成绩公示...
  7. 深入剖析.NETCORE中CORS(跨站资源共享)
  8. 7月Chrome谷歌浏览器份额以68.60%位居榜首
  9. 【数值分析】数值分析的微积分学基础
  10. 【Linux】在Linux环境下使用VSCode调试C/C++程序
  11. 【雕爷学编程】Arduino动手做(16)---数字触摸传感器
  12. TensorFlow 学习(六) —— TensorFlow 与 numpy 的交互
  13. 干货|内网渗透之端口转发端口映射
  14. 小米手机切换应用--完美实现步骤
  15. 在CAD里怎么测量面积?
  16. 计算机丢失opencv_world300.dll文件
  17. Win7系统安装教程【附Win7/64位系统下载地址】
  18. MySQL探秘(八):InnoDB的事务
  19. 《黑客帝国》中的代码雨让人身临其境 利用Python轻松实现
  20. [003]python数据类型一__python_全栈基础

热门文章

  1. 博主:遇见未知的自己 (http://www.cnblogs.com)
  2. 重复的字符串——移动匹配+KMP
  3. 字库生成记录-20210311
  4. http常见状态码有哪些?
  5. word2013~2016多级标题自动编号
  6. 服务器怎么设置2个账号密码忘记了怎么办啊,腾讯云服务器管理员账号密码如何重置...
  7. 【LeetCode】636. 函数的独占时间
  8. unity 透明度算法_unity3D之透明度
  9. 如何让我的电脑监控我手机的请求
  10. C语言 全字母句,全字母句 SDUT