Android Studio模板之文件组

文件组模板是基于FreeMarker模板语言的一个功能很强大的Android开发模板,可以这样说,代码片段模板和文件模板是一种提高编码效率的工具,而文件组模板可以算是一种模板引擎。


效果图展示



已有工程中使用模板效果图



创建工程时使用模板



示例场景


在进行Android开发时,我们经常会创建一个Demo工程,目的可能有很多种,可能是为了验证一个问题,可能是为了学习一个框架的使用,可能为了测试自己写的一个lib库等等。这个时候我们可能会创建一个Activity,然后再在xml写一些按钮,再在Activity里写该按钮的事件监听逻辑,也就是说为了执行一段代码我们要做这么多操作。为了简化这段重复操作,我这边写了一个DebugActivity类,然后支持我们只需要写个子类来继承它,然后像下面这样写几个方法即可,运行的时候会根据方法动态创建按钮,并在点击按钮时执行该方法的代码逻辑。


public void _test() {

T("弹出Toast");

}


由于本文主要介绍模板相关的,所以该场景相关的具体代码技术细节就不多说了,有兴趣的可以看下,DebugActivity的代码,这里提出来只是为模板开发简单的做个铺垫。


模板位置


Android Studio Template中有系统预设的一些模板,我们可以直接修改,也可以另行添加新的模板。打开Android Studio安装目录/Contents/plugins/android/lib/templates这个文件夹我们能看到下面的目录结构,这里便是AS中模板存放的位置。


我们接下来的工作也就在这里,保险起见我们在这里新建一个目录,我们自己写的模板都放在自己新建的目录里,例如我这里就创建了一个叫pk的目录。


模板规范


在上面的基础上,我们可以直接打开/activies/EmptyActivity目录,如下图



我们可以看到上面红色区域便是Template的文件结构,大致说下各个文件(夹)的含义


  • globals.xml.ftl 模板中参数配置的地方(可选)

  • recipe.xml.ftl 模板行为执行处,引入这个模板之后,接下来要做什么事情,就是它说的算(可选,但是不选就没有意义了,因为模板引入是要要行为驱动的)

  • root 存放模板文件及引入资源的目录,模板文件可以是.xml、.java、.gradle等任何一个文本格式的文件,资源一般是我们引入的.png资源文件(可选,不选同上)

  • template_blank_activity.png 引入模板时的引导图(可选)

  • template.xml 面向模板引擎的配置文件(必选)


我们可以看到,真正核心的部分就是root、recipe.xml.ftl和template.xml,接下来这重点说明这三部分。


我们可以打开root目录,能够看到里面的文件除了图片资源文件都是以.ftl结尾的,而.ftl是标准的FreeMarker的文件。FreeMarker是类似于Velocity的一种模板框架,据说对于多文件处理时它具有更好的性能,大概也是Android Studio选择Velocity作为单文件模板,选择FreeMarker作为文件组模板的原因吧。有兴趣的可以去FreeMarker官网学习一下,它的自定义标签功能还是很强大的,个人感觉比Velocity的更加接地气。


接下来我们看一下recipe.xml.ftl 的内容,打开如下



这里以<#开头的都是FreeMarker的语法,基本上比葫芦画瓢就能看明白,就不多说了。其实对于这个文件最重要的部分是下面四个标签:


  • copy 就是简单的copy,把模板root目录下的某个文件copy到目标工程的某个目录下

  • instantiate 跟copy很类似,唯一多的一点功能就是并不只简单的走IO流进行copy,而是通过FreeMarker框架按照模板中的FreeMarker能识别的逻辑判断和数据引入来生成最终的目标文件

  • merge 目标项目中有了某文件,而我们还要想该文件合并一些我们的模板的部分时,就选用merge,例如我们添加一个Activity时需要mergeAndroidManifest.xml的配置。目前支持的merge格式有.xml和.gradle,但是对.gradle支持的不怎么好,不过不影响该模板的开发,对于这套模板引擎的开发者来说,这可能是最麻烦的部分了,但是对于我们使用者就不用考那么多了,直接使用吧

  • open 这个很简单,就是指定模板引入之后要IDE打开的文件


然后看下template.xml内容


<?xml version="1.0"?>

<template

    format="5"

    revision="5"

    name="Empty Activity"

    minApi="7"

    minBuildApi="14"

    description="Creates a new empty activity">

    <category value="Activity" />

    <formfactor value="Mobile" />

    <parameter

        id="activityClass"

        name="Activity Name"

        type="string"

        constraints="class|unique|nonempty"

        suggest="${layoutToActivity(layoutName)}"

        default="MainActivity"

        help="The name of the activity class to create" />

    <parameter

        id="generateLayout"

        name="Generate Layout File"

        type="boolean"

        default="true"

        help="If true, a layout file will be generated" />

    <parameter

        id="layoutName"

        name="Layout Name"

        type="string"

        constraints="layout|unique|nonempty"

        suggest="${activityToLayout(activityClass)}"

        default="activity_main"

        visibility="generateLayout"

        help="The name of the layout to create for the activity" />

    <parameter

        id="isLauncher"

        name="Launcher Activity"

        type="boolean"

        default="false"

        help="If true, this activity will have a CATEGORY_LAUNCHER intent filter, making it visible in the launcher" />

    

    <parameter

        id="packageName"

        name="Package name"

        type="string"

        constraints="package"

        default="com.mycompany.myapp" />

    <!-- 128x128 thumbnails relative to template.xml -->

    <thumbs>

        <!-- default thumbnail is required -->

        <thumb>template_blank_activity.png</thumb>

    </thumbs>

    <globals file="globals.xml.ftl" />

    <execute file="recipe.xml.ftl" />

</template>


当我们进行模板引入时,AS会弹出一个如下图的UI界面,要我们来填入或选择一些数据,例如输入Activity的的名称,选择SDK的版本之类的。而这个界面就是根据由该文件而来的。



内容比较多,为减少篇幅我挑些重要的说


  • template标签

  • name 引入模板时的模板名称,就死根据他选择哪个模板的

  • description 弹出Dialog的标题,对应上去的区域1

  • category 表示该模板属于哪种分类,在引入的时候会有个分类的选择

  • parameter 每个该标签就对应Dialog界面的一个输入项

  • id 该参数的唯一标识符,也是我们在.ftl中引入的值,例如定义的id为username,引用时就是$username

  • name 对应Dialog上面该输入项的名称

  • type 对应该参数的类型,Dialog就是根据这个来决定对应输入是选择框、输入框还是下拉框等等

  • constraints 对应该参数的约束,如果有多个要用|分割开

  • suggest 建议值,这个输入部分是由级联效应的,可能你改了A参数,B参数也会跟着改变,就是根据这个参数决定的

  • default 参数的默认值

  • visibility 可见性,要配置一个boolean类型的参数,一般指向另一个输入源

  • help 当焦点在某个输入源上面时,上图的区域3的就限制这儿的内容


操刀实战


了解了模板规范之后,我们编写模板时就不会那么被动了,下面我们来自己动手编写文章开始部分展示的模板。


首先在刚才提到的自定义的模板下创建如下图所示的目录结构



然后将下面的代码对应贴进去(图片部分随便找一张代替好了…)


globals.xml.ftl




recipe.xml.ftl



template.xml


<?xml version="1.0"?>

<template

    format="5"

    revision="5"

    name="Debug Activity"

    minApi="7"

    minBuildApi="14"

    description="创建一个Debug的Activity">

    <category value="Activity" />

    <formfactor value="Mobile" />

    <parameter

        id="activityClass"

        name="Activity名称"

        type="string"

        constraints="class|unique|nonempty"

        default="SetupActivity"

        help="创建Activity的名称" />

    <parameter

        id="addExample"

        name="是否添加按钮使用示例"

        type="boolean"

        default="false"

        help="选择时会自动生成测试按钮;否则不生成" />

    

    <parameter

        id="addJumpActivity"

        name="是否添加跳转Activity示例"

        type="boolean"

        default="false"

        help="选择时会自动生成跳转Activity相关逻辑;否则不生成" />

    <parameter

        id="isLauncher"

        name="设为启动页面"

        type="boolean"

        default="true"

        help="选择时设置该页面为启动页面;否则不设" />

    

    <parameter

        id="packageName"

        name="包名"

        type="string"

        constraints="package"

        default="com.mycompany.myapp"

        help="输入Application包名" />

    <!-- 128x128 thumbnails relative to template.xml -->

    <thumbs>

        <!-- default thumbnail is required -->

        <thumb>template_debug_activity.png</thumb>

    </thumbs>

    <globals file="globals.xml.ftl" />

    <execute file="recipe.xml.ftl" />

</template>


AndroidManifest.xml.ftl



DebugActivity.java.ftl


package ${packageName};

import android.app.Activity;

import android.content.Context;

import android.content.Intent;

import android.os.Bundle;

import android.util.Log;

import android.view.View;

import android.widget.Button;

import android.widget.LinearLayout;

import android.widget.ScrollView;

import android.widget.Toast;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

import java.lang.reflect.Method;

import java.util.ArrayList;

import java.util.List;

/**

 * Debug测试类,快速调试Demo工程<hr />

 * 使用姿势:<br />

 * 1. 新建一个子类继承该类<br />

 * 2. 跳转Activity: 在子类配置{@link Jump}注解, 然后在注解中配置跳转Activity的类型<br />

 * 3. 点击按钮触发方法: 在子类声明一个名称以"_"开头的方法(支持任意修饰符),最终生成按钮的文字便是改方法截去"_"<br />

 * 4. 方法参数支持缺省参数和单个参数<br />

 * 5. 如果是单个参数,参数类型必须是Button或Button的父类类型,当方法执行时,该参数会被赋值为该Buttom对象<br />

 * https://github.com/puke3615/DebugActivity<br />

 * <p>

 *

 * @author zijiao

 * @version 16/10/16

 */

public abstract class DebugActivity extends Activity {

    protected static final String FIXED_PREFIX = "_";

    private final String TAG = getClass().getName();

    private final List<ButtonItem> buttonItems = new ArrayList<>();

    protected LinearLayout linearLayout;

    protected Context context;

    @Target(ElementType.TYPE)

    @Retention(RetentionPolicy.RUNTIME)

    public @interface Jump {

        Class<? extends Activity>[] value() default {};

    }

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        this.context = this;

        ScrollView scrollView = new ScrollView(this);

        setContentView(scrollView);

        this.linearLayout = new LinearLayout(this);

        this.linearLayout.setOrientation(LinearLayout.VERTICAL);

        scrollView.addView(linearLayout);

        try {

            resolveConfig();

            createButton();

        } catch (Throwable e) {

            error(e.getMessage());

        }

    }

    private void createButton() {

        for (ButtonItem buttonItem : buttonItems) {

            linearLayout.addView(buildButton(buttonItem));

        }

    }

    protected View buildButton(final ButtonItem buttonItem) {

        final Button button = new Button(this);

        button.setText(buttonItem.name);

        button.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View v) {

                if (buttonItem.target != null) {

                    to(buttonItem.target);

                } else {

                    Method method = buttonItem.method;

                    method.setAccessible(true);

                    Class<?>[] parameterTypes = method.getParameterTypes();

                    int paramSize = parameterTypes.length;

                    switch (paramSize) {

                        case 0:

                            try {

                                method.invoke(DebugActivity.this);

                            } catch (Throwable e) {

                                e.printStackTrace();

                                error(e.getMessage());

                            }

                            break;

                        case 1:

                            if (parameterTypes[0].isAssignableFrom(Button.class)) {

                                try {

                                    method.invoke(DebugActivity.this, button);

                                } catch (Throwable e) {

                                    e.printStackTrace();

                                    error(e.getMessage());

                                }

                                break;

                            }

                        default:

                            error(method.getName() + "方法参数配置错误.");

                            break;

                    }

                }

            }

        });

        return button;

    }

    private void resolveConfig() {

        Class<?> cls = getClass();

        //读取跳转配置

        if (cls.isAnnotationPresent(Jump.class)) {

            Jump annotation = cls.getAnnotation(Jump.class);

            for (Class<? extends Activity> activityClass : annotation.value()) {

                buttonItems.add(buildJumpActivityItem(activityClass));

            }

        }

        //读取方法

        for (Method method : cls.getDeclaredMethods()) {

            handleMethod(method);

        }

    }

    protected void handleMethod(Method method) {

        String methodName = method.getName();

        if (methodName.startsWith(FIXED_PREFIX)) {

            methodName = methodName.replaceFirst(FIXED_PREFIX, "");

            ButtonItem buttonItem = new ButtonItem();

            buttonItem.method = method;

            buttonItem.name = methodName;

            buttonItems.add(buttonItem);

        }

    }

    protected ButtonItem buildJumpActivityItem(Class<? extends Activity> activityClass) {

        ButtonItem buttonItem = new ButtonItem();

        buttonItem.name = "跳转到" + activityClass.getSimpleName();

        buttonItem.target = activityClass;

        return buttonItem;

    }

    public void L(Object s) {

        Log.i(TAG, s + "");

    }

    public void error(String errorMessage) {

        T("[错误信息]\n" + errorMessage);

    }

    public void T(Object message) {

        Toast.makeText(context, String.valueOf(message), Toast.LENGTH_SHORT).show();

    }

    public void to(Class<? extends Activity> target) {

        try {

            startActivity(new Intent(this, target));

        } catch (Exception e) {

            e.printStackTrace();

            error(e.getMessage());

        }

    }

    public void T(String format, Object... values) {

        T(String.format(format, values));

    }

    protected static class ButtonItem {

        public String name;

        public Method method;

        public Class<? extends Activity> target;

    }

}


JumpActivity.java.ftl



SimpleActivity.java.ftl


package ${packageName};

@DebugActivity.Jump({

<#if addJumpActivity>

    JumpActivity.class,

<#else>

</#if>

})

public class ${activityClass} extends DebugActivity {

<#if addExample>

    private int number = 0;

    public void _无参方法调用() {

    T("无参方法调用");

    }

    public void _有参方法调用(Button button) {

        button.setText("number is " + number++);

    }

    //代码执行不到,直接弹出toast提示报错

    public void _错误参数调用(String msg) {

        T("test");

    }

    //方法名没有以"_"开头,按钮无法创建成功

    public void 无效调用() {

        T("test");

    }

    //crash会被会被catch住,以toast方式弹出

    public void _Crash测试() {

        int a = 1 / 0;

    }

</#if>

}


ok,到此对于该模板的编写过程就结束了,接下来重启下Android Studio,然后New Project一路next下去,直到这个界面,这里就是我们自定义的DebugActivity模板了


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值