Android Studio模板之文件组

简介:

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

效果图展示 

效果图展示

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

创建工程时使用模板

示例场景

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

 
 
  1. public void _test() { 
  2.  
  3. T("弹出Toast"); 
  4.  
  5.  

由于本文主要介绍模板相关的,所以该场景相关的具体代码技术细节就不多说了,有兴趣的可以看下,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内容

 
 
  1. <?xml version="1.0"?> 
  2.  
  3. <template 
  4.  
  5.     format="5" 
  6.  
  7.     revision="5" 
  8.  
  9.     name="Empty Activity" 
  10.  
  11.     minApi="7" 
  12.  
  13.     minBuildApi="14" 
  14.  
  15.     description="Creates a new empty activity"
  16.  
  17.     <category value="Activity" /> 
  18.  
  19.     <formfactor value="Mobile" /> 
  20.  
  21.     <parameter 
  22.  
  23.         id="activityClass" 
  24.  
  25.         name="Activity Name" 
  26.  
  27.         type="string" 
  28.  
  29.         constraints="class|unique|nonempty" 
  30.  
  31.         suggest="${layoutToActivity(layoutName)}" 
  32.  
  33.         default="MainActivity" 
  34.  
  35.         help="The name of the activity class to create" /> 
  36.  
  37.     <parameter 
  38.  
  39.         id="generateLayout" 
  40.  
  41.         name="Generate Layout File" 
  42.  
  43.         type="boolean" 
  44.  
  45.         default="true" 
  46.  
  47.         help="If true, a layout file will be generated" /> 
  48.  
  49.     <parameter 
  50.  
  51.         id="layoutName" 
  52.  
  53.         name="Layout Name" 
  54.  
  55.         type="string" 
  56.  
  57.         constraints="layout|unique|nonempty" 
  58.  
  59.         suggest="${activityToLayout(activityClass)}" 
  60.  
  61.         default="activity_main" 
  62.  
  63.         visibility="generateLayout" 
  64.  
  65.         help="The name of the layout to create for the activity" /> 
  66.  
  67.     <parameter 
  68.  
  69.         id="isLauncher" 
  70.  
  71.         name="Launcher Activity" 
  72.  
  73.         type="boolean" 
  74.  
  75.         default="false" 
  76.  
  77.         help="If true, this activity will have a CATEGORY_LAUNCHER intent filter, making it visible in the launcher" /> 
  78.  
  79.      
  80.  
  81.     <parameter 
  82.  
  83.         id="packageName" 
  84.  
  85.         name="Package name" 
  86.  
  87.         type="string" 
  88.  
  89.         constraints="package" 
  90.  
  91.         default="com.mycompany.myapp" /> 
  92.  
  93.     <!-- 128x128 thumbnails relative to template.xml --> 
  94.  
  95.     <thumbs> 
  96.  
  97.         <!-- default thumbnail is required --> 
  98.  
  99.         <thumb>template_blank_activity.png</thumb> 
  100.  
  101.     </thumbs> 
  102.  
  103.     <globals file="globals.xml.ftl" /> 
  104.  
  105.     <execute file="recipe.xml.ftl" /> 
  106.  
  107. </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

 
 
  1. <?xml version="1.0"?> 
  2.  
  3. <template 
  4.  
  5.     format="5" 
  6.  
  7.     revision="5" 
  8.  
  9.     name="Debug Activity" 
  10.  
  11.     minApi="7" 
  12.  
  13.     minBuildApi="14" 
  14.  
  15.     description="创建一个Debug的Activity"
  16.  
  17.     <category value="Activity" /> 
  18.  
  19.     <formfactor value="Mobile" /> 
  20.  
  21.     <parameter 
  22.  
  23.         id="activityClass" 
  24.  
  25.         name="Activity名称" 
  26.  
  27.         type="string" 
  28.  
  29.         constraints="class|unique|nonempty" 
  30.  
  31.         default="SetupActivity" 
  32.  
  33.         help="创建Activity的名称" /> 
  34.  
  35.     <parameter 
  36.  
  37.         id="addExample" 
  38.  
  39.         name="是否添加按钮使用示例" 
  40.  
  41.         type="boolean" 
  42.  
  43.         default="false" 
  44.  
  45.         help="选择时会自动生成测试按钮;否则不生成" /> 
  46.  
  47.      
  48.  
  49.     <parameter 
  50.  
  51.         id="addJumpActivity" 
  52.  
  53.         name="是否添加跳转Activity示例" 
  54.  
  55.         type="boolean" 
  56.  
  57.         default="false" 
  58.  
  59.         help="选择时会自动生成跳转Activity相关逻辑;否则不生成" /> 
  60.  
  61.     <parameter 
  62.  
  63.         id="isLauncher" 
  64.  
  65.         name="设为启动页面" 
  66.  
  67.         type="boolean" 
  68.  
  69.         default="true" 
  70.  
  71.         help="选择时设置该页面为启动页面;否则不设" /> 
  72.  
  73.      
  74.  
  75.     <parameter 
  76.  
  77.         id="packageName" 
  78.  
  79.         name="包名" 
  80.  
  81.         type="string" 
  82.  
  83.         constraints="package" 
  84.  
  85.         default="com.mycompany.myapp" 
  86.  
  87.         help="输入Application包名" /> 
  88.  
  89.     <!-- 128x128 thumbnails relative to template.xml --> 
  90.  
  91.     <thumbs> 
  92.  
  93.         <!-- default thumbnail is required --> 
  94.  
  95.         <thumb>template_debug_activity.png</thumb> 
  96.  
  97.     </thumbs> 
  98.  
  99.     <globals file="globals.xml.ftl" /> 
  100.  
  101.     <execute file="recipe.xml.ftl" /> 
  102.  
  103. </template>  

AndroidManifest.xml.ftl

DebugActivity.java.ftl

 
 
  1. package ${packageName}; 
  2.  
  3. import android.app.Activity; 
  4.  
  5. import android.content.Context; 
  6.  
  7. import android.content.Intent; 
  8.  
  9. import android.os.Bundle; 
  10.  
  11. import android.util.Log; 
  12.  
  13. import android.view.View
  14.  
  15. import android.widget.Button; 
  16.  
  17. import android.widget.LinearLayout; 
  18.  
  19. import android.widget.ScrollView; 
  20.  
  21. import android.widget.Toast; 
  22.  
  23. import java.lang.annotation.ElementType; 
  24.  
  25. import java.lang.annotation.Retention; 
  26.  
  27. import java.lang.annotation.RetentionPolicy; 
  28.  
  29. import java.lang.annotation.Target; 
  30.  
  31. import java.lang.reflect.Method; 
  32.  
  33. import java.util.ArrayList; 
  34.  
  35. import java.util.List; 
  36.  
  37. /** 
  38.  
  39.  * Debug测试类,快速调试Demo工程<hr /> 
  40.  
  41.  * 使用姿势:<br /> 
  42.  
  43.  * 1. 新建一个子类继承该类<br /> 
  44.  
  45.  * 2. 跳转Activity: 在子类配置{@link Jump}注解, 然后在注解中配置跳转Activity的类型<br /> 
  46.  
  47.  * 3. 点击按钮触发方法: 在子类声明一个名称以"_"开头的方法(支持任意修饰符),最终生成按钮的文字便是改方法截去"_"<br /> 
  48.  
  49.  * 4. 方法参数支持缺省参数和单个参数<br /> 
  50.  
  51.  * 5. 如果是单个参数,参数类型必须是Button或Button的父类类型,当方法执行时,该参数会被赋值为该Buttom对象<br /> 
  52.  
  53.  * https://github.com/puke3615/DebugActivity<br /> 
  54.  
  55.  * <p> 
  56.  
  57.  * 
  58.  
  59.  * @author zijiao 
  60.  
  61.  * @version 16/10/16 
  62.  
  63.  */ 
  64.  
  65. public abstract class DebugActivity extends Activity { 
  66.  
  67.     protected static final String FIXED_PREFIX = "_"
  68.  
  69.     private final String TAG = getClass().getName(); 
  70.  
  71.     private final List<ButtonItem> buttonItems = new ArrayList<>(); 
  72.  
  73.     protected LinearLayout linearLayout; 
  74.  
  75.     protected Context context; 
  76.  
  77.     @Target(ElementType.TYPE) 
  78.  
  79.     @Retention(RetentionPolicy.RUNTIME) 
  80.  
  81.     public @interface Jump { 
  82.  
  83.         Class<? extends Activity>[] value() default {}; 
  84.  
  85.     } 
  86.  
  87.     @Override 
  88.  
  89.     protected void onCreate(Bundle savedInstanceState) { 
  90.  
  91.         super.onCreate(savedInstanceState); 
  92.  
  93.         this.context = this; 
  94.  
  95.         ScrollView scrollView = new ScrollView(this); 
  96.  
  97.         setContentView(scrollView); 
  98.  
  99.         this.linearLayout = new LinearLayout(this); 
  100.  
  101.         this.linearLayout.setOrientation(LinearLayout.VERTICAL); 
  102.  
  103.         scrollView.addView(linearLayout); 
  104.  
  105.         try { 
  106.  
  107.             resolveConfig(); 
  108.  
  109.             createButton(); 
  110.  
  111.         } catch (Throwable e) { 
  112.  
  113.             error(e.getMessage()); 
  114.  
  115.         } 
  116.  
  117.     } 
  118.  
  119.     private void createButton() { 
  120.  
  121.         for (ButtonItem buttonItem : buttonItems) { 
  122.  
  123.             linearLayout.addView(buildButton(buttonItem)); 
  124.  
  125.         } 
  126.  
  127.     } 
  128.  
  129.     protected View buildButton(final ButtonItem buttonItem) { 
  130.  
  131.         final Button button = new Button(this); 
  132.  
  133.         button.setText(buttonItem.name); 
  134.  
  135.         button.setOnClickListener(new View.OnClickListener() { 
  136.  
  137.             @Override 
  138.  
  139.             public void onClick(View v) { 
  140.  
  141.                 if (buttonItem.target != null) { 
  142.  
  143.                     to(buttonItem.target); 
  144.  
  145.                 } else { 
  146.  
  147.                     Method method = buttonItem.method; 
  148.  
  149.                     method.setAccessible(true); 
  150.  
  151.                     Class<?>[] parameterTypes = method.getParameterTypes(); 
  152.  
  153.                     int paramSize = parameterTypes.length; 
  154.  
  155.                     switch (paramSize) { 
  156.  
  157.                         case 0: 
  158.  
  159.                             try { 
  160.  
  161.                                 method.invoke(DebugActivity.this); 
  162.  
  163.                             } catch (Throwable e) { 
  164.  
  165.                                 e.printStackTrace(); 
  166.  
  167.                                 error(e.getMessage()); 
  168.  
  169.                             } 
  170.  
  171.                             break; 
  172.  
  173.                         case 1: 
  174.  
  175.                             if (parameterTypes[0].isAssignableFrom(Button.class)) { 
  176.  
  177.                                 try { 
  178.  
  179.                                     method.invoke(DebugActivity.this, button); 
  180.  
  181.                                 } catch (Throwable e) { 
  182.  
  183.                                     e.printStackTrace(); 
  184.  
  185.                                     error(e.getMessage()); 
  186.  
  187.                                 } 
  188.  
  189.                                 break; 
  190.  
  191.                             } 
  192.  
  193.                         default
  194.  
  195.                             error(method.getName() + "方法参数配置错误."); 
  196.  
  197.                             break; 
  198.  
  199.                     } 
  200.  
  201.                 } 
  202.  
  203.             } 
  204.  
  205.         }); 
  206.  
  207.         return button; 
  208.  
  209.     } 
  210.  
  211.     private void resolveConfig() { 
  212.  
  213.         Class<?> cls = getClass(); 
  214.  
  215.         //读取跳转配置 
  216.  
  217.         if (cls.isAnnotationPresent(Jump.class)) { 
  218.  
  219.             Jump annotation = cls.getAnnotation(Jump.class); 
  220.  
  221.             for (Class<? extends Activity> activityClass : annotation.value()) { 
  222.  
  223.                 buttonItems.add(buildJumpActivityItem(activityClass)); 
  224.  
  225.             } 
  226.  
  227.         } 
  228.  
  229.         //读取方法 
  230.  
  231.         for (Method method : cls.getDeclaredMethods()) { 
  232.  
  233.             handleMethod(method); 
  234.  
  235.         } 
  236.  
  237.     } 
  238.  
  239.     protected void handleMethod(Method method) { 
  240.  
  241.         String methodName = method.getName(); 
  242.  
  243.         if (methodName.startsWith(FIXED_PREFIX)) { 
  244.  
  245.             methodName = methodName.replaceFirst(FIXED_PREFIX, ""); 
  246.  
  247.             ButtonItem buttonItem = new ButtonItem(); 
  248.  
  249.             buttonItem.method = method; 
  250.  
  251.             buttonItem.name = methodName; 
  252.  
  253.             buttonItems.add(buttonItem); 
  254.  
  255.         } 
  256.  
  257.     } 
  258.  
  259.     protected ButtonItem buildJumpActivityItem(Class<? extends Activity> activityClass) { 
  260.  
  261.         ButtonItem buttonItem = new ButtonItem(); 
  262.  
  263.         buttonItem.name = "跳转到" + activityClass.getSimpleName(); 
  264.  
  265.         buttonItem.target = activityClass; 
  266.  
  267.         return buttonItem; 
  268.  
  269.     } 
  270.  
  271.     public void L(Object s) { 
  272.  
  273.         Log.i(TAG, s + ""); 
  274.  
  275.     } 
  276.  
  277.     public void error(String errorMessage) { 
  278.  
  279.         T("[错误信息]\n" + errorMessage); 
  280.  
  281.     } 
  282.  
  283.     public void T(Object message) { 
  284.  
  285.         Toast.makeText(context, String.valueOf(message), Toast.LENGTH_SHORT).show(); 
  286.  
  287.     } 
  288.  
  289.     public void to(Class<? extends Activity> target) { 
  290.  
  291.         try { 
  292.  
  293.             startActivity(new Intent(this, target)); 
  294.  
  295.         } catch (Exception e) { 
  296.  
  297.             e.printStackTrace(); 
  298.  
  299.             error(e.getMessage()); 
  300.  
  301.         } 
  302.  
  303.     } 
  304.  
  305.     public void T(String format, Object... values) { 
  306.  
  307.         T(String.format(format, values)); 
  308.  
  309.     } 
  310.  
  311.     protected static class ButtonItem { 
  312.  
  313.         public String name
  314.  
  315.         public Method method; 
  316.  
  317.         public Class<? extends Activity> target; 
  318.  
  319.     } 
  320.  
  321.  

JumpActivity.java.ftl

SimpleActivity.java.ftl

 
 
  1. package ${packageName}; 
  2.  
  3. @DebugActivity.Jump({ 
  4.  
  5. <#if addJumpActivity> 
  6.  
  7.     JumpActivity.class, 
  8.  
  9. <#else
  10.  
  11. </#if> 
  12.  
  13. }) 
  14.  
  15. public class ${activityClass} extends DebugActivity { 
  16.  
  17. <#if addExample> 
  18.  
  19.     private int number = 0; 
  20.  
  21.     public void _无参方法调用() { 
  22.  
  23.     T("无参方法调用"); 
  24.  
  25.     } 
  26.  
  27.     public void _有参方法调用(Button button) { 
  28.  
  29.         button.setText("number is " + number++); 
  30.  
  31.     } 
  32.  
  33.     //代码执行不到,直接弹出toast提示报错 
  34.  
  35.     public void _错误参数调用(String msg) { 
  36.  
  37.         T("test"); 
  38.  
  39.     } 
  40.  
  41.     //方法名没有以"_"开头,按钮无法创建成功 
  42.  
  43.     public void 无效调用() { 
  44.  
  45.         T("test"); 
  46.  
  47.     } 
  48.  
  49.     //crash会被会被catch住,以toast方式弹出 
  50.  
  51.     public void _Crash测试() { 
  52.  
  53.         int a = 1 / 0; 
  54.  
  55.     } 
  56.  
  57. </#if> 
  58.  
  59.  

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






作者:Puke
来源:51CTO
目录
相关文章
|
2月前
|
Android开发
安卓SO层开发 -- 编译指定平台的SO文件
安卓SO层开发 -- 编译指定平台的SO文件
30 0
|
3月前
|
人工智能 IDE 开发工具
Studio Bot - 让 AI 帮我写 Android 代码
Studio Bot - 让 AI 帮我写 Android 代码
152 1
|
2月前
|
JSON Java Go
|
2月前
|
数据库 Android开发 数据库管理
【Android】使用android studio查看内置数据库信息
【Android】使用android studio查看内置数据库信息
62 0
|
2月前
|
算法 Java Android开发
安卓逆向 -- 调用其他APK的SO文件
安卓逆向 -- 调用其他APK的SO文件
17 0
|
2月前
|
编译器 开发工具 Android开发
|
2月前
|
Android开发
安卓逆向 -- Hook多个dex文件
安卓逆向 -- Hook多个dex文件
18 1
|
2月前
|
Android开发
【Android Studio】小游戏 | 实现两个小动物随手指移动
【Android Studio】小游戏 | 实现两个小动物随手指移动
|
2月前
|
Android开发 数据安全/隐私保护
【Android Studio】简单的QQ登录界面
【Android Studio】简单的QQ登录界面
|
3月前
|
IDE 开发工具 Android开发
Android Studio 下发布项目成APK文件
Android Studio 下发布项目成APK文件
117 1