阅读 725

用Kotlin撸一个图片压缩插件-插件基础篇(二)

简述: 前两天写了篇用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.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仓库中会用这个。

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

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实现类的绑定。

  • 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.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>
复制代码
  • 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对象获得相应的窗口对象

 @Nullable
    public <T> T getData(@NotNull DataKey<T> key) {
        if (key == null) {
            $$$reportNull$$$0(28);
        }

        return this.getDataContext().getData(key);//委托给DataContext对象getData方法实现
}
复制代码
  • 4、AnActionEvent获得当前Project对象,引出CommonDataKeys
    @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() {
    }
}
复制代码
  • 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");
  @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");
}
复制代码

四、插件开发一些建议

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

五、插件开发一些资源

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

欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不定期翻译一篇Kotlin国外技术文章。如果你也喜欢Kotlin,欢迎加入我们~~~

关注下面的标签,发现更多相似文章
评论