简述: 前两天写了篇用Kotlin撸一个图片压缩插件-导学篇,如今迎来了插件基础篇,没错这篇文章就是教你如何一步一步从零开始写一个插件,包括插件项目构建,运行,调试到最后的上线发布整个流程。若是你是插件零基础的小白,那么这篇文章适合你,并且这篇文章也是下面实战篇的基础.html
ImageSlimming图片压缩插件开发完成后,立刻就把它推荐给团队内部人员使用,在周会上就有同事提出了一个需求,就是在AndroidStudio项目中,能够任意选中res目录下一张或多张图片,而后直接右键选择,就能够实现图片压缩。而后思考了一波,这个需求挺好的,内心大概想了下,今晚就去把它实现了。实现效果大概以下:java
实现这个功能后,把V1.1版本的代码作了很大的结构上调整,抽离出一些公共的顶层函数和扩展函数,目前这个功能代码已经更新到GitHub上了,请认准feature-image-slimming-v1.2分支。android
IDE插件利用jetBrains公司开源的IntelliJ Platform SDK(java语言)来开发一个独立功能能够安装在IDEA之类的编辑器的功能组件。 IDE插件是基于IntelliJ IDEA开发工具开发,里面集成了插件的项目的构建。采用的是Java语言开发和IntelliJ的SDK相结合开发。而且在开发出来的插件不只在AndroidStudio上可使用,能够通用于jetBrains的编辑器的全家桶工具。经过源码能够发现Intellij Idea内置了大量的插件,能够这么说Intellij Idea开发工具大部分功能是由插件组合而成的。git
注意: 构建插件项目的方式主要有两种:github
一种是直接建立IDEA内置的插件项目.api
另外一种则是先经过构建一个gradle项目,而后加入plugin.xml配置以及 加入IDEA ERP的依赖,而后来构建一个插件项目(整个开发过程就和开发一个Android项目同样),固然这个构建过程可参考官方给出的gradle-intellij-plugin项目来实现。不过在最新2018.1.1以后版本中,IDEA内部也提供了构建grale插件项目入口,具体可下载新版本Intellij Idea。架构
<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.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>
复制代码
id标签: plugin插件项目的标识,和Android项目中的package功能相似。惟一标识一个插件项目。并发
name标签: 插件名字,发布到jetBrains plugin仓库中会用这个。app
version标签: 插件版本号,这个用于标识插件版本,通常用于更新jetbrains plugins仓库中插件版本标识。dom
vendor标签: 开发者信息,邮箱和我的主页,公司名字或我的开发者姓名,用于插件仓库中插件信息介绍显示。
description标签: 插件的描述信息,主要是描述插件有什么功能。支持标签内部内嵌HTML标签。
changNote标签: 通常用于插件版本变动的信息。支持标签内部内嵌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实现类的绑定。
第一种:就是经过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.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.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>
复制代码
七、你能够打断点,点击debug,而后就能够断点调试代码。
八、最后一步,打包插件,并发布。选择顶部工具栏Build, 点击"Prepare Plugin Module 'Demo' For Deployment",就会在当前工做目录下生成一个jar或zip的包。而后发布插件,只须要在jetBrains Plugins Repository上传你的包,等待jetBrains官方的审核经过了,就能经过ide中的plugins仓库中搜索找到。
插件开发最为重要之一的就是Action类了,能够说它是插件功能的一个入口,编写一个Action类,通常会去继承AnAction类,AnAction是一个抽象类,必需要去实现actionPerformed方法,这个方法是在用户触发插件的点击事件后回调的,因此相似于打开对话框,执行某个功能的逻辑能够写在里面等等。单从插件开发角度(插件的生命周期除外)来讲,能够把当它当作程序中的main函数。
首先建立一个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持久化文件对象等等,绝不夸张的说后续插件功能开发都是围绕它来展开的,下面会详细描述。
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方法中执行的。
AnActionEvent对象,actionPerformed和update方法都会携带一个AnActionEvent对象,能够说它是插件与IDEA交互通讯的一个媒介,经过AnActionEvent内部的dataContext的getData方法,传入对应的DataKey对象得到相应的窗口对象
@Nullable
public <T> T getData(@NotNull DataKey<T> key) {
if (key == null) {
$$$reportNull$$$0(28);
}
return this.getDataContext().getData(key);//委托给DataContext对象getData方法实现
}
复制代码
@Nullable
public 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() {
}
}
复制代码
经过以上图示操做,会发现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");
@Deprecated
public 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");
}
复制代码
最后到这里,插件开发基础篇就结束,下一篇就是本系列完结实战开发篇,欢迎继续关注~~~
欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不按期翻译一篇Kotlin国外技术文章。若是你也喜欢Kotlin,欢迎加入咱们~~~