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的标题,对应上去的区域1category 表示该模板属于哪种分类,在引入的时候会有个分类的选择 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模板了
Android C++系列:Linux文件IO操作(二) 注意这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写 位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置。比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一 个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是 1。
Android C++系列:Linux文件IO操作(一) 事实上Unbuffered I/O这个名词是有些误导的,虽然write系统调用位于C标准库I/O缓 冲区的底层,但在write的底层也可以分配一个内核I/O缓冲区,所以write也不一定是直接 写到文件的,也可能写到内核I/O缓冲区中,至于究竟写到了文件中还是内核缓冲区中对于 进程来说是没有差别的,如果进程A和进程B打开同一文件,进程A写到内核I/O缓冲区中的数 据从进程B也能读到,而C标准库的I/O缓冲区则不具有这一特性(想一想为什么)
相关文章
- 2021年上半年最接地气的Android面经,附大厂真题面经
- 5年老安卓面试竟然被这3道Android基础题难倒了?系列篇
- 【Android Studio安装部署系列】二十、Android studio如何将so文件添加到svn中
- android 读取根目录下的文件或文件夹
- android中的常见对话框
- 如何用Android studio生成正式签名的APK文件
- Android Studio 2.3.1导出jar文件不能生成release解决办法
- Android学习--Assets资源文件读取及AssetManager介绍
- Android自定义崩溃收集器捕获java层和native层崩溃异常日志
- Android之文件搜索工具类
- Android Studio实现前后台分离的选课系统
- Android 绘制背景标签图片,文件防伪背景图详解
- Android 10 读写文件权限
- 【Android OpenCV】Visual Studio 创建支持 OpenCV 库的 CMake 工程 ③ ( CMake 工程中配置 OpenCV 库文件 | 拷贝 OpenCV 函数库文件 )
- Android 蓝牙开发(三) -- 低功耗蓝牙开发
- Android代码实现APK文件的安装与卸载
- Android 创建与解析XML(六)—— 比较与使用
- android在xml文件中定义drawable数组、id数组等
- 【Android 安装包优化】Android 应用中 7zr 可执行程序准备 ( Android Studio 导入可执行 7zr 程序 | 从 Assets 资源文件拷贝 7zr 到内置存储 )
- 【我的Android进阶之旅】Android 混淆文件资源分类整理
- 【我的Android进阶之旅】Android 如何防止 so库文件被未知应用盗用?
- android和ios流媒体库推荐
- 我的Android进阶之旅------>Android中使用HTML作布局文件以及调用Javascript