基于IntelliJ IDEA实现AndroidStudio自定义插件:创建模板工程(包含:文件IO处理、弹框定制等技术点)

需求

AndroidStudio 有很多插件,可供开发者集成、使用。
像Flutter、Cordova、mPaas等众多插件,都拥有一个共同的功能,就是创建“模板工程”。也就是使用这些插件创建的Android工程已集成好了相关的依赖、配置,开发者们也不需要再从零开始集成,直接开发就可以了,非常方便。

我们这边也需要对外提供SDK,也想参考这种方式,提供给调用方使用。
在网上,自定义插件的资料倒是有一些,不过使用自定义插件创建工程的资料却是 零 !!!

没办法,只能通过AndroidStudio插件反推吧:
我这边是在AndroidStudio -> Setting-> Plugins ->Marketplace找了几款会创建工程的插件,去其官网,下载插件,然后反编译得到源码的(下了很多插件,反编译源码,没有任何混淆的,哈哈,省时间了~)

通过分析其源码,实现插件创建模板工程的方式主要有两种:

1、在插件中放入模板工程文件,创建工程时,直接执行IO操作,使用模板文件创建工程;(稍简单一些)
2、在插件代码中通过IO创建工程文件,执行代码逻辑向文件中写内容;(逻辑复杂一些)

我这边选择使用方案1,简单高效,维护也方便。

设计

不啰嗦了,直接上具体实施方案:

1、将纯净版模板工程(删除build的工程)压缩成一个文件,将来作为模板文件放入插件
2、插件安装后,插件菜单显示在AndroidStudio -> File菜单的顶部(new菜单的上面,看着舒服些,哈哈~)
3、用户点击插件菜单,创建模板工程,会弹出提示框,让用户选择目标位置
4、用户选择目标位置后,点击【创建】,将插件中的模板文件(模板工程.zip)拷贝至指定位置
5、解压缩模板工程.zip,得到完整的模板工程(解压后,也可删除压缩包文件)

开搞

1、安装 IntelliJ IDEA

Java编程语言开发的集成环境,IntelliJ在业界被公认为最好的java开发工具。
我们的插件也要使用该工具实现,不懂如何使用的同学,可以先去做做功课哈~

下载地址:http://www.jetbrains.com/idea/
我选择的是Community(社区版)
然后下一步......即可

2、创建插件工程

file->new->Intellij Platform Plugin

新建完成的目录,其中 plugin.xml 相当于我们的 AndroidManifest.xml,对一些Actions(类似于我们的Activity)进行注册,逻辑代码同样写在 src 中,资源文件(比如说icon)放在 resources 中

3、插件工程结构

template:存放模板文件的目录

-- readme.txt  让用户读的文本信息,例如插件版本、日期、变更内容、联系人...
-- TestTemplate.zip 模板工程压缩包文件

com.qxc.testplugin:存放插件代码逻辑的目录

-- AddFileActionByTemp  action动作类,监听用户点击菜单动作
-- OutFolderChooser  自定义文件目录选择器类
-- UnZipUtils  解压缩工具类

4、plugin.xml

定义插件信息、action信息(菜单项),源码:

<idea-plugin version="2">
  <id>com.qixingchao.createufp</id>
  <name>CreateUFPProject</name>
  <version>1.0</version>
  <vendor email="970188529@qq.com" url="http://baidu.com">qxc</vendor>
  <description><![CDATA[
      Create a ufp project.<br>
    ]]></description>
  <change-notes><![CDATA[
    ]]>
  </change-notes>
  <idea-version since-build="141.0"/>
  <extensions defaultExtensionNs="com.intellij">
  </extensions>
  <actions>
    <action id="CreateTemplateProject" class="com.qxc.testplugin.AddFileActionByTemp" text="CreateTemplateProject"
            description="CreateTemplateProject">
      <add-to-group group-id="FileMenu" anchor="first"/>
    </action>
  </actions>
</idea-plugin>

插件菜单显示在AndroidStudio -> File菜单的顶部

5、AddFileActionByTemp 类

大家可能需要对intellij的API,多做做功课,不然可能看不明白

package com.qxc.testplugin;

import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.project.Project;
import java.io.*;

/**
 * 根据模板创建文件/工程
 * 齐行超
 * 2020年3月3日
 */
public class AddFileActionByTemp extends AnAction
{
    public Project project;

    /**
     * action
     * @param e 事件
     */
    public void actionPerformed(AnActionEvent e)
    {
        this.project = ((Project)e.getData(PlatformDataKeys.PROJECT));
        init();
        refreshProject(e);
    }

    /**
     * 刷新工程
     * @param e
     */
    private void refreshProject(AnActionEvent e)
    {
        e.getProject().getBaseDir().refresh(false, true);
    }

    /**
     * 初始化
     */
    private void init()
    {
        OutFolderChooser outFolderChooser = new OutFolderChooser();
        outFolderChooser.InitUI(this);
    }

    /**
     * 创建文件
     * @param basePath 路径
     * @return true、false
     */
    public boolean createClassFiles(String basePath)
    {
        try {
            createFile(basePath, "readme.txt");
            createProject(basePath, "TestTemplate.zip");
        }catch (Exception ex){
            ex.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 创建文件(读写字符串)
     * @param basePath 路径
     * @param fileName 文件名
     * @throws Exception 异常
     */
    private void createFile(String basePath, String fileName) throws Exception
    {
        String content = "";
        if (!new File(basePath + fileName).exists())
        {
            content = ReadTemplateFile(fileName);
            writeToFile(content, basePath, fileName);
        }
    }

    /**
     * 创建工程(zip文件,通过io流处理)
     * @param basePath 路径
     * @param fileName 文件名称
     * @throws Exception 异常
     */
    public void createProject(String basePath, String fileName) throws Exception
    {
        InputStream in = null;
        in = getClass().getResourceAsStream("/template/" + fileName);
        FileInputStream fi=null;
        FileOutputStream fo=null;
        try {
            fo=new FileOutputStream(basePath+fileName);
            byte[] b=new byte[1024];
            int len=0;
            while((len=in.read(b))!=-1) {
                fo.write(b, 0, len);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(fi!=null) {
                try {
                    fi.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(fo!=null) {
                try {
                    fo.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        UnZipUtils.unZip(basePath+fileName, basePath);
    }

    /**
     * 读取模板文件
     * @param fileName 文件名称
     * @return 文件内容(字符串)
     */
    private String ReadTemplateFile(String fileName)
    {
        InputStream in = null;
        in = getClass().getResourceAsStream("/template/" + fileName);
        String content = "";
        try
        {
            content = new String(readStream(in));
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return content;
    }

    /**
     * 读取io流
     * @param inputStream 文件io流
     * @return byte数组
     * @throws IOException io异常
     */
    private byte[] readStream(InputStream inputStream)
            throws IOException
    {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte['?'];
        int len = -1;
        try
        {
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            outputStream.close();
            inputStream.close();
        }
        return outputStream.toByteArray();
    }

    /**
     * 写文件
     * @param content 内容
     * @param classPath 路径(文件)
     * @param className 名称(文件)
     */
    private void writeToFile(String content, String classPath, String className)
    {
        try
        {
            File floder = new File(classPath);
            if (!floder.exists()) {
                floder.mkdirs();
            }
            File file = new File(classPath + "/" + className);
            if (!file.exists()) {
                file.createNewFile();
            }
            FileWriter fw = new FileWriter(file.getAbsoluteFile());
            BufferedWriter bw = new BufferedWriter(fw);
            bw.write(content);
            bw.close();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }
}

6、OutFolderChooser

package com.qxc.testplugin;

import com.intellij.openapi.ui.Messages;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JTextField;

/**
 * 输出目录选择器
 * 齐行超
 * 2020年3月3日
 */
public class OutFolderChooser extends JFrame {
    private AddFileActionByTemp addFileActionByTemp;
    private JTextField textField;

    private String textBtn1 = "创建工程";
    private String textBtn2 = "选择目录";

    /**
     * 初始化输出目录选择器
     * @param addFileActionByTemp 类实例
     */
    public void InitUI(AddFileActionByTemp addFileActionByTemp)
    {
        this.addFileActionByTemp = addFileActionByTemp;

        this.setTitle("新建模板工程");
        this.setSize(500, 200);
        this.setDefaultCloseOperation(3);
        this.setResizable(false);
        this.setLocationRelativeTo(null);
        this.setLayout(null);//关闭流式布局

        Font f=new Font("宋体",Font.PLAIN,12);//根据指定字体名称、样式和磅值大小,创建一个新 Font。

        JButton button1 = new JButton(textBtn1);
        button1.setBounds(405,55, 80, 50);
        button1.setContentAreaFilled(false);  //消除按钮背景颜色
        button1.setOpaque(false); //除去边框
        button1.setFocusPainted(false);//出去突起
        button1.setFont(f);
        this.add(button1);

        JButton button2 = new JButton(textBtn2);
        button2.setBounds(320,55, 80, 50);
        button2.setContentAreaFilled(false);  //消除按钮背景颜色
        button2.setOpaque(false); //除去边框
        button2.setFocusPainted(false);//出去突起
        button2.setFont(f);
        this.add(button2);

        textField = new JTextField();
        textField.setBounds(10, 55, 300, 50);
        this.add(textField);

        ButtonListener BL = new ButtonListener(textField);
        button1.addActionListener(BL);
        button2.addActionListener(BL);
        this.setVisible(true);//设置窗体可见
    }

    /**
     * 弹框(diaolog事件处理)
     * @param parent
     * @param msgTextArea
     */
    public void showFileSaveDialog(Component parent, JTextField msgTextArea) {
        // 创建一个默认的文件选取器
        JFileChooser fileChooser = new JFileChooser();
        // 设置打开文件选择框后默认输入的文件名
        fileChooser.setSelectedFile(new File("UFPProject"));
        // 打开文件选择框(线程将被阻塞, 直到选择框被关闭)
        int result = fileChooser.showSaveDialog(parent);
        if (result == JFileChooser.APPROVE_OPTION) {
            // 如果点击了"保存", 则获取选择的保存路径
            File file = fileChooser.getSelectedFile();
            msgTextArea.setText(file.getAbsolutePath().trim());
        }
    }

    /**
     * 创建工程(diaolog事件处理)
     */
    public void createProject(){
      boolean result = addFileActionByTemp.createClassFiles(textField.getText().trim());
      if(result){
          Messages.showInfoMessage(addFileActionByTemp.project, "创建成功!", "提示");
          this.setVisible(false);//设置窗体可见
      }else{
          Messages.showInfoMessage(addFileActionByTemp.project, "创建失败,请联系框架组!", "提示");
      }
    }

    class ButtonListener implements ActionListener {
        private JTextField textField;
        public ButtonListener(JTextField textField) {
            super();
            this.textField = textField;
        }
        public void actionPerformed(ActionEvent e) {
            if(e.getActionCommand().equals(textBtn1))
            {
                OutFolderChooser.this.createProject();
            }
            else if(e.getActionCommand().equals(textBtn2))
            {
                OutFolderChooser.this.showFileSaveDialog(OutFolderChooser.this, textField);
            }
        }
    }
}

7、UnZipUtils

package com.qxc.testplugin;

import java.io.*;
import java.nio.charset.Charset;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * 解压缩工具类
 * 齐行超
 * 2020年3月3日
 */
public class UnZipUtils {
    /**
     * 解压缩zip文件
     * @param filePath 文件路径
     * @param outFolder 输出路径
     */
    public static void unZip(String filePath, String outFolder) {
        // 判断文件夹是否存在
        File folder = new File(outFolder);
        if(!folder.exists()){
            folder.mkdir();
        }

        // 创建buffer
        byte[] buffer = new byte[1024];
        ZipInputStream zipls = null;

        try {
            zipls = new ZipInputStream(new FileInputStream(filePath), Charset.forName("GBK"));
            ZipEntry entry = null;
            while ((entry=zipls.getNextEntry())!=null){
                String entryName = entry.getName();
                String outFileName = outFolder + File.separator + entryName;
                System.out.println("create: " + outFileName);
                if(entry.isDirectory()){
                    new File(outFileName).mkdirs();
                }else{
                    FileOutputStream fos = new FileOutputStream(outFileName);
                    int len;
                    while ((len = zipls.read(buffer))>0){
                        fos.write(buffer,0,len);
                    }
                    fos.close();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                zipls.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

8、生成插件

编码完成,点击Build -> Prepare Plugin Moudle “xxx” Deployment

生成后的插件,是Jar包形式,包含资源信息,反编译可以看到咱们的模板文件等内容。

9、AndroidStudio安装插件

插件后安装成功后,重启AnroidStudio

10、测试插件

搞定,测试了下新创建的模板工程,运行正常,nice~

总结

插件工程源码:https://pan.baidu.com/s/1GBAQC7d_bnvLno1NSIMh4A
提取码:3xh5

IntelliJ IDEA还是非常好用的,除了支持安卓插件的开发,也支持调试。
插件工程中的技术点不难,无非就是IO操作、自定义UI等,相信大家也都能看得懂。
如果有疑问,也欢迎大家留言咨询,就是不一定有时间解答,哈哈~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容