基於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="[email protected]" 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等,相信大家也都能看得懂。
如果有疑問,也歡迎大家留言諮詢,就是不一定有時間解答,哈哈~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章