Android Studio模板之文件組
文件組模板是基于FreeMarker模板語言的一個(gè)功能很強(qiáng)大的Android開發(fā)模板,可以這樣說,代碼片段模板和文件模板是一種提高編碼效率的工具,而文件組模板可以算是一種模板引擎。
效果圖展示
已有工程中使用模板效果圖
創(chuàng)建工程時(shí)使用模板
示例場景
在進(jìn)行Android開發(fā)時(shí),我們經(jīng)常會創(chuàng)建一個(gè)Demo工程,目的可能有很多種,可能是為了驗(yàn)證一個(gè)問題,可能是為了學(xué)習(xí)一個(gè)框架的使用,可能為了測試自己寫的一個(gè)lib庫等等。這個(gè)時(shí)候我們可能會創(chuàng)建一個(gè)Activity,然后再在xml寫一些按鈕,再在Activity里寫該按鈕的事件監(jiān)聽邏輯,也就是說為了執(zhí)行一段代碼我們要做這么多操作。為了簡化這段重復(fù)操作,我這邊寫了一個(gè)DebugActivity類,然后支持我們只需要寫個(gè)子類來繼承它,然后像下面這樣寫幾個(gè)方法即可,運(yùn)行的時(shí)候會根據(jù)方法動態(tài)創(chuàng)建按鈕,并在點(diǎn)擊按鈕時(shí)執(zhí)行該方法的代碼邏輯。
- public void _test() {
- T("彈出Toast");
- }
由于本文主要介紹模板相關(guān)的,所以該場景相關(guān)的具體代碼技術(shù)細(xì)節(jié)就不多說了,有興趣的可以看下,DebugActivity的代碼,這里提出來只是為模板開發(fā)簡單的做個(gè)鋪墊。
模板位置
Android Studio Template中有系統(tǒng)預(yù)設(shè)的一些模板,我們可以直接修改,也可以另行添加新的模板。打開Android Studio安裝目錄/Contents/plugins/android/lib/templates這個(gè)文件夾我們能看到下面的目錄結(jié)構(gòu),這里便是AS中模板存放的位置。
我們接下來的工作也就在這里,保險(xiǎn)起見我們在這里新建一個(gè)目錄,我們自己寫的模板都放在自己新建的目錄里,例如我這里就創(chuàng)建了一個(gè)叫pk的目錄。
模板規(guī)范
在上面的基礎(chǔ)上,我們可以直接打開/activies/EmptyActivity目錄,如下圖
我們可以看到上面紅色區(qū)域便是Template的文件結(jié)構(gòu),大致說下各個(gè)文件(夾)的含義
- globals.xml.ftl 模板中參數(shù)配置的地方(可選)
- recipe.xml.ftl 模板行為執(zhí)行處,引入這個(gè)模板之后,接下來要做什么事情,就是它說的算(可選,但是不選就沒有意義了,因?yàn)槟0逡胧且袨轵?qū)動的)
- root 存放模板文件及引入資源的目錄,模板文件可以是.xml、.java、.gradle等任何一個(gè)文本格式的文件,資源一般是我們引入的.png資源文件(可選,不選同上)
- template_blank_activity.png 引入模板時(shí)的引導(dǎo)圖(可選)
- template.xml 面向模板引擎的配置文件(必選)
我們可以看到,真正核心的部分就是root、recipe.xml.ftl和template.xml,接下來這重點(diǎn)說明這三部分。
我們可以打開root目錄,能夠看到里面的文件除了圖片資源文件都是以.ftl結(jié)尾的,而.ftl是標(biāo)準(zhǔn)的FreeMarker的文件。FreeMarker是類似于Velocity的一種模板框架,據(jù)說對于多文件處理時(shí)它具有更好的性能,大概也是Android Studio選擇Velocity作為單文件模板,選擇FreeMarker作為文件組模板的原因吧。有興趣的可以去FreeMarker官網(wǎng)學(xué)習(xí)一下,它的自定義標(biāo)簽功能還是很強(qiáng)大的,個(gè)人感覺比Velocity的更加接地氣。
接下來我們看一下recipe.xml.ftl 的內(nèi)容,打開如下
這里以<#開頭的都是FreeMarker的語法,基本上比葫蘆畫瓢就能看明白,就不多說了。其實(shí)對于這個(gè)文件最重要的部分是下面四個(gè)標(biāo)簽:
- copy 就是簡單的copy,把模板root目錄下的某個(gè)文件copy到目標(biāo)工程的某個(gè)目錄下
- instantiate 跟copy很類似,***多的一點(diǎn)功能就是并不只簡單的走IO流進(jìn)行copy,而是通過FreeMarker框架按照模板中的FreeMarker能識別的邏輯判斷和數(shù)據(jù)引入來生成最終的目標(biāo)文件
- merge 目標(biāo)項(xiàng)目中有了某文件,而我們還要想該文件合并一些我們的模板的部分時(shí),就選用merge,例如我們添加一個(gè)Activity時(shí)需要mergeAndroidManifest.xml的配置。目前支持的merge格式有.xml和.gradle,但是對.gradle支持的不怎么好,不過不影響該模板的開發(fā),對于這套模板引擎的開發(fā)者來說,這可能是最麻煩的部分了,但是對于我們使用者就不用考那么多了,直接使用吧
- open 這個(gè)很簡單,就是指定模板引入之后要IDE打開的文件
然后看下template.xml內(nèi)容
- <?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>
當(dāng)我們進(jìn)行模板引入時(shí),AS會彈出一個(gè)如下圖的UI界面,要我們來填入或選擇一些數(shù)據(jù),例如輸入Activity的的名稱,選擇SDK的版本之類的。而這個(gè)界面就是根據(jù)由該文件而來的。
內(nèi)容比較多,為減少篇幅我挑些重要的說
- template標(biāo)簽
- name 引入模板時(shí)的模板名稱,就死根據(jù)他選擇哪個(gè)模板的
- description 彈出Dialog的標(biāo)題,對應(yīng)上去的區(qū)域1
- category 表示該模板屬于哪種分類,在引入的時(shí)候會有個(gè)分類的選擇
- parameter 每個(gè)該標(biāo)簽就對應(yīng)Dialog界面的一個(gè)輸入項(xiàng)
- id 該參數(shù)的***標(biāo)識符,也是我們在.ftl中引入的值,例如定義的id為username,引用時(shí)就是$username
- name 對應(yīng)Dialog上面該輸入項(xiàng)的名稱
- type 對應(yīng)該參數(shù)的類型,Dialog就是根據(jù)這個(gè)來決定對應(yīng)輸入是選擇框、輸入框還是下拉框等等
- constraints 對應(yīng)該參數(shù)的約束,如果有多個(gè)要用|分割開
- suggest 建議值,這個(gè)輸入部分是由級聯(lián)效應(yīng)的,可能你改了A參數(shù),B參數(shù)也會跟著改變,就是根據(jù)這個(gè)參數(shù)決定的
- default 參數(shù)的默認(rèn)值
- visibility 可見性,要配置一個(gè)boolean類型的參數(shù),一般指向另一個(gè)輸入源
- help 當(dāng)焦點(diǎn)在某個(gè)輸入源上面時(shí),上圖的區(qū)域3的就限制這兒的內(nèi)容
操刀實(shí)戰(zhàn)
了解了模板規(guī)范之后,我們編寫模板時(shí)就不會那么被動了,下面我們來自己動手編寫文章開始部分展示的模板。
首先在剛才提到的自定義的模板下創(chuàng)建如下圖所示的目錄結(jié)構(gòu)
然后將下面的代碼對應(yīng)貼進(jìn)去(圖片部分隨便找一張代替好了…)
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="創(chuàng)建一個(gè)Debug的Activity">
- <category value="Activity" />
- <formfactor value="Mobile" />
- <parameter
- id="activityClass"
- name="Activity名稱"
- type="string"
- constraints="class|unique|nonempty"
- default="SetupActivity"
- help="創(chuàng)建Activity的名稱" />
- <parameter
- id="addExample"
- name="是否添加按鈕使用示例"
- type="boolean"
- default="false"
- help="選擇時(shí)會自動生成測試按鈕;否則不生成" />
- <parameter
- id="addJumpActivity"
- name="是否添加跳轉(zhuǎn)Activity示例"
- type="boolean"
- default="false"
- help="選擇時(shí)會自動生成跳轉(zhuǎn)Activity相關(guān)邏輯;否則不生成" />
- <parameter
- id="isLauncher"
- name="設(shè)為啟動頁面"
- type="boolean"
- default="true"
- help="選擇時(shí)設(shè)置該頁面為啟動頁面;否則不設(shè)" />
- <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測試類,快速調(diào)試Demo工程<hr />
- * 使用姿勢:<br />
- * 1. 新建一個(gè)子類繼承該類<br />
- * 2. 跳轉(zhuǎn)Activity: 在子類配置{@link Jump}注解, 然后在注解中配置跳轉(zhuǎn)Activity的類型<br />
- * 3. 點(diǎn)擊按鈕觸發(fā)方法: 在子類聲明一個(gè)名稱以"_"開頭的方法(支持任意修飾符),最終生成按鈕的文字便是改方法截去"_"<br />
- * 4. 方法參數(shù)支持缺省參數(shù)和單個(gè)參數(shù)<br />
- * 5. 如果是單個(gè)參數(shù),參數(shù)類型必須是Button或Button的父類類型,當(dāng)方法執(zhí)行時(shí),該參數(shù)會被賦值為該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() + "方法參數(shù)配置錯(cuò)誤.");
- break;
- }
- }
- }
- });
- return button;
- }
- private void resolveConfig() {
- Class<?> cls = getClass();
- //讀取跳轉(zhuǎn)配置
- 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 = "跳轉(zhuǎn)到" + activityClass.getSimpleName();
- buttonItem.target = activityClass;
- return buttonItem;
- }
- public void L(Object s) {
- Log.i(TAG, s + "");
- }
- public void error(String errorMessage) {
- T("[錯(cuò)誤信息]\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 _無參方法調(diào)用() {
- T("無參方法調(diào)用");
- }
- public void _有參方法調(diào)用(Button button) {
- button.setText("number is " + number++);
- }
- //代碼執(zhí)行不到,直接彈出toast提示報(bào)錯(cuò)
- public void _錯(cuò)誤參數(shù)調(diào)用(String msg) {
- T("test");
- }
- //方法名沒有以"_"開頭,按鈕無法創(chuàng)建成功
- public void 無效調(diào)用() {
- T("test");
- }
- //crash會被會被catch住,以toast方式彈出
- public void _Crash測試() {
- int a = 1 / 0;
- }
- </#if>
- }
ok,到此對于該模板的編寫過程就結(jié)束了,接下來重啟下Android Studio,然后New Project一路next下去,直到這個(gè)界面,這里就是我們自定義的DebugActivity模板了