需求
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等,相信大家也都能看得懂。
如果有疑问,也欢迎大家留言咨询,就是不一定有时间解答,哈哈~