1. 程式人生 > >自己編寫Android Studio外掛

自己編寫Android Studio外掛

前言

這篇部落格是根據輸入或者選中佈局檔案(如R.layout.activity_main,只需要選中activity_main或者輸入activity_main),來自動生成欄位,和獲取值(findViewById())。

適用Activity和Fragment


演示

編寫外掛

環境

Android Studio本身是不支援開發外掛的,所以需要下載IntelliJ IDEA來編寫,但是Android Studio是基於IntelliJ IDEA的,用IntelliJ IDEA不會感到陌生,官網下載https://www.jetbrains.com/idea/

建立專案


建立專案

目錄結構


目錄結構
plugin.xml

plugin.xml是類似Android專案裡面的AndroidMenifest檔案,用來配置資訊的註冊和宣告。


plugin.xml
  • id:(com.example.plugin.Name)外掛的ID,保證外掛上傳倉庫後的唯一性。
  • name:外掛名稱。
  • version:版本號。
  • description:外掛的簡介。
  • change-notes:版本更新資訊。
  • extensions:擴充套件元件註冊 。

開始編寫

建立一個Action,是繼承AnAction類的

右鍵src目錄->New->Action


New->Action

填寫內容


填寫內容

填寫ActionID,ClassName,Name,Description;選擇放在哪個選單,Anchor選擇First或者Last;設定快捷鍵KeyBoard Shortcuts;

ActionID:代表該Action的唯一的ID,一般的格式為:pluginName.ID
ClassName:類名
Name:就是最終外掛在選單上的名稱
Description:對這個Action的描述資訊
Groups:定義這個選單選項出現的位置,這裡選中CodeMenu(Code),在Code選單裡面。

可以在plugin.xml裡面修改對應的Action屬性


Action

編寫Action


action


點選ok之後會生成相應的Action,在Action裡面的actionPerformed

方法會在點選選單或者快捷鍵的是否觸發。

思路

在獲取佈局檔案內容後自動解析佈局檔案並生成欄位和findViewById程式碼。

1.獲取佈局檔案
2.解析佈局檔案,獲取屬性
3.將程式碼寫入action

獲取佈局檔案

查詢檔案需要用到PsiFile類,通過FilenameIndex.getFilesByName(project, name, scope)來查詢佈局檔案。
先獲取使用者選中內容,如果沒選中,則彈出dialog讓使用者輸入內容;

        // 獲取project
        Project project = e.getProject();
        // 獲取選中內容
        final Editor mEditor = e.getData(PlatformDataKeys.EDITOR);
        if (null == mEditor) {
            return;
        }
        SelectionModel model = mEditor.getSelectionModel();
        String selectedText = model.getSelectedText();
        if (TextUtils.isEmpty(selectedText)) {
            // 未選中佈局內容,顯示dialog
            selectedText = Messages.showInputDialog(project, "layout(不需要輸入R.layout.):" , "未選中佈局內容,請輸入layout檔名", Messages.getInformationIcon());
            if (TextUtils.isEmpty(selectedText)) {
                Utils.showPopupBalloon(mEditor, "未輸入layout檔名");
                return;
            }
        }

然後根據輸入的內容查詢xml檔案;

        // 獲取佈局檔案,通過FilenameIndex.getFilesByName獲取
        // GlobalSearchScope.allScope(project)搜尋整個專案
        PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, selectedText + ".xml", GlobalSearchScope.allScope(project));
        if (psiFiles.length <= 0) {
            Utils.showPopupBalloon(mEditor, "未找到選中的佈局檔案");
            return;
        }
        XmlFile xmlFile = (XmlFile) psiFiles[0];

解析佈局檔案,獲取屬性

通過psiFile.accept(new PsiRecursiveElementWalkingVisitor()…);去遍歷一個檔案的所有元素

    /**
     * 獲取所有id
     *
     * @param file
     * @param elements
     * @return
     */
    public static java.util.List<Element> getIDsFromLayout(final PsiFile file, final java.util.List<Element> elements) {
        // To iterate over the elements in a file
        // 遍歷一個檔案的所有元素
        file.accept(new XmlRecursiveElementVisitor() {

            @Override
            public void visitElement(PsiElement element) {
                super.visitElement(element);
                // 解析Xml標籤
                if (element instanceof XmlTag) {
                    XmlTag tag = (XmlTag) element;
                    // 獲取Tag的名字(TextView)或者自定義
                    String name = tag.getName();
                    // 如果有include
                    if (name.equalsIgnoreCase("include")) {
                        // 獲取佈局
                        XmlAttribute layout = tag.getAttribute("layout", null);
                        // 獲取project
                        Project project = file.getProject();
                        // 佈局檔案
                        XmlFile include = null;
                        PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, getLayoutName(layout.getValue()) + ".xml", GlobalSearchScope.allScope(project));
                        if (psiFiles.length > 0) {
                            include = (XmlFile) psiFiles[0];
                        }
                        if (include != null) {
                            // 遞迴
                            getIDsFromLayout(include, elements);
                            return;
                        }
                    }
                    // 獲取id欄位屬性
                    XmlAttribute id = tag.getAttribute("android:id", null);
                    if (id == null) {
                        return;
                    }
                    // 獲取id的值
                    String idValue = id.getValue();
                    if (idValue == null) {
                        return;
                    }
                    XmlAttribute aClass = tag.getAttribute("class", null);
                    if (aClass != null) {
                        name = aClass.getValue();
                    }
                    // 新增到list
                    try {
                        Element e = new Element(name, idValue, tag);
                        elements.add(e);
                    } catch (IllegalArgumentException e) {

                    }
                }
            }
        });


        return elements;
    }

    /**
     * layout.getValue()返回的值為@layout/layout_view
     *
     * @param layout
     * @return
     */
    public static String getLayoutName(String layout) {
        if (layout == null || !layout.startsWith("@") || !layout.contains("/")) {
            return null;
        }

        // @layout layout_view
        String[] parts = layout.split("/");
        if (parts.length != 2) {
            return null;
        }
        // layout_view
        return parts[1];
    }

對應的實體類Element,裡面包含獲取id的值,獲取型別如(TextView或者com.example.CustomView),根據id設定變數名。

    // 判斷id正則
    private static final Pattern sIdPattern = Pattern.compile("@\\+?(android:)?id/([^$]+)$", Pattern.CASE_INSENSITIVE);
    // id
    public String id;
    // 名字如TextView
    public String name;
    // 命名1 aa_bb_cc; 2 aaBbCc 3 mAaBbCc
    public int fieldNameType = 3;
    public XmlTag xml;

    /**
     * 建構函式
     *
     * @param name View的名字
     * @param id   android:id屬性
     * @throws IllegalArgumentException When the arguments are invalid
     */
    public Element(String name, String id, XmlTag xml) {
        // id
        final Matcher matcher = sIdPattern.matcher(id);
        if (matcher.find() && matcher.groupCount() > 1) {
            this.id = matcher.group(2);
        }

        if (this.id == null) {
            throw new IllegalArgumentException("Invalid format of view id");
        }

        String[] packages = name.split("\\.");
        if (packages.length > 1) {
            // com.example.CustomView
            this.name = packages[packages.length - 1];
        } else {
            this.name = name;
        }

        this.xml = xml;
    }

    /**
     * 獲取id,R.id.id
     *
     * @return
     */
    public String getFullID() {
        StringBuilder fullID = new StringBuilder();
        String rPrefix = "R.id.";
        fullID.append(rPrefix);
        fullID.append(id);
        return fullID.toString();
    }

    /**
     * 獲取變數名
     *
     * @return
     */
    public String getFieldName() {
        String fieldName = id;
        String[] names = id.split("_");
        if (fieldNameType == 2) {
            // aaBbCc
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < names.length; i++) {
                if (i == 0) {
                    sb.append(names[i]);
                } else {
                    sb.append(firstToUpperCase(names[i]));
                }
            }
            fieldName = sb.toString();
        } else if (fieldNameType == 3) {
            // mAaBbCc
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < names.length; i++) {
                if (i == 0) {
                    sb.append("m");
                }
                sb.append(firstToUpperCase(names[i]));
            }
            fieldName = sb.toString();
        }
        return fieldName;
    }

    // 第一個字母大寫
    public static String firstToUpperCase(String key) {
        return key.substring(0, 1).toUpperCase(Locale.CHINA) + key.substring(1);
    }

將程式碼寫入action

Intellij Platform不允許在主執行緒中進行實時的檔案寫入,需通過非同步任務來進行,可以通過繼承WriteCommandAction.Simple,然後在run方法裡面進行寫檔案操作。

    @Override
    protected void run() throws Throwable {

    }
主要用到的方法
    /**
     * 根據當前檔案獲取對應的class檔案
     * @param editor
     * @param file
     * @return
     */
    protected PsiClass getTargetClass(Editor editor, PsiFile file) {
        int offset = editor.getCaretModel().getOffset();
        PsiElement element = file.findElementAt(offset);
        if(element == null) {
            return null;
        } else {
            PsiClass target = PsiTreeUtil.getParentOfType(element, PsiClass.class);
            return target instanceof SyntheticElement ?null:target;
        }
    }
  • mClass.findMethodsByName("onCreate", false)判斷類是否包含某方法
  • JavaPsiFacade.getInstance(mProject).findClass("android.app.Activity", new EverythingGlobalScope(mProject));根據類名查詢類
  • PsiUtilBase.getPsiFileInEditor(mEditor, project);方法為獲取當前檔案;
  • psiclass.add(JavaPsiFacade.getElementFactory(mProject).createMethodFromText(sbInitView.toString(), psiclass))方法為類建立方法;
  • mFactory.mFactory.createMethodFromText(method.toString(), mClass)方法新增欄位;
  • onCreate.getBody().addAfter(mFactory.createStatementFromText("initView();", mClass), setContentViewStatement);方法為方法體新增內容。
    具體建立內容
    ```java
    import com.intellij.codeInsight.actions.ReformatCodeProcessor;
    import com.intellij.openapi.command.WriteCommandAction;
    import com.intellij.openapi.command.WriteCommandAction.Simple;
    import com.intellij.openapi.project.Project;
    import com.intellij.psi.*;
    import com.intellij.psi.codeStyle.JavaCodeStyleManager;
    import com.intellij.psi.search.EverythingGlobalScope;
    import entity.Element;
    import org.apache.http.util.TextUtils;

import java.util.List;

public class IdCreator extends Simple {

private PsiFile mFile;
private Project mProject;
private PsiClass mClass;
private List<Element> mElements;
private PsiElementFactory mFactory;
private String mSelectText;

public IdCreator(PsiFile psiFile, PsiClass psiClass, String command, List<Element> elements, String selectText) {
    super(psiClass.getProject(), command);

    mFile = psiFile;
    mProject = psiClass.getProject();
    mClass = psiClass;
    mElements = elements;
    // 獲取Factory
    mFactory = JavaPsiFacade.getElementFactory(mProject);
    mSelectText = selectText;
}

@Override
protected void run() throws Throwable {
    generateFields();
    generateFindViewById();
    // 重寫class
    JavaCodeStyleManager styleManager = JavaCodeStyleManager.getInstance(mProject);
    styleManager.optimizeImports(mFile);
    styleManager.shortenClassReferences(mClass);
    new ReformatCodeProcessor(mProject, mClass.getContainingFile(), null, false).runWithoutProgress();
}

/**
 * 建立變數
 */
private void generateFields() {
    for (Element element : mElements) {

        // remove duplicate field
        PsiField[] fields = mClass.getFields();
        boolean duplicateField = false;
        for (PsiField field : fields) {
            String name = field.getName();
            if (name != null && name.equals(element.getFieldName())) {
                duplicateField = true;
                break;
            }
        }

        if (duplicateField) {
            continue;
        }
        // 設定變數
        String text = element.xml.getAttributeValue("android:text");
        // text
        String fromText = "private " + element.name + " " + element.getFieldName() + ";";
        if (!TextUtils.isEmpty(text)) {
            fromText = "/** " + text + " */\n" + fromText;
        }
        // 新增到class
        mClass.add(mFactory.createFieldFromText(fromText, mClass));
    }
}

/**
 * 設定變數的值FindViewById,Activity和Fragment
 */
private void generateFindViewById() {
    // 根據類名查詢類
    PsiClass activityClass = JavaPsiFacade.getInstance(mProject).findClass("android.app.Activity", new EverythingGlobalScope(mProject));
    PsiClass activityCompatClass = JavaPsiFacade.getInstance(mProject).findClass("android.support.v7.app.AppCompatActivity", new EverythingGlobalScope(mProject));
    PsiClass fragmentClass = JavaPsiFacade.getInstance(mProject).findClass("android.app.Fragment", new EverythingGlobalScope(mProject));
    PsiClass fragmentV4Class = JavaPsiFacade.getInstance(mProject).findClass("android.support.v4.app.Fragment", new EverythingGlobalScope(mProject));
    // 判斷mClass是不是繼承activityClass或者activityCompatClass
    if ((activityClass != null && mClass.isInheritor(activityClass, true))
            || (activityCompatClass != null && mClass.isInheritor(activityCompatClass, true))
            || mClass.getName().contains("Activity")) {
        // 判斷是否有onCreate方法
        if (mClass.findMethodsByName("onCreate", false).length == 0) {
            StringBuilder method = new StringBuilder();
            method.append("@Override protected void onCreate(android.os.Bundle savedInstanceState) {\n");
            method.append("super.onCreate(savedInstanceState);\n");
            method.append("\t// TODO:run FindViewById again To setValue in initView method\n");
            method.append("\tsetContentView(R.layout.");
            method.append(mSelectText);
            method.append(");\n");
            method.append("}");
            // 新增
            mClass.add(mFactory.createMethodFromText(method.toString(), mClass));
        } else {
            // 獲取setContentView
            PsiStatement setContentViewStatement = null;
            // onCreate是否存在initView方法
            boolean hasInitViewStatement = false;

            PsiMethod onCreate = mClass.findMethodsByName("onCreate", false)[0];
            for (PsiStatement psiStatement : onCreate.getBody().getStatements()) {
                // 查詢setContentView
                if (psiStatement.getFirstChild() instanceof PsiMethodCallExpression) {
                    PsiReferenceExpression methodExpression = ((PsiMethodCallExpression) psiStatement.getFirstChild()).getMethodExpression();
                    if (methodExpression.getText().equals("setContentView")) {
                        setContentViewStatement = psiStatement;
                    } else if (methodExpression.getText().equals("initView")) {
                        hasInitViewStatement = true;
                    }
                }
            }

            if (!hasInitViewStatement && setContentViewStatement != null) {
                // 將initView()寫到setContentView()後面
                onCreate.getBody().addAfter(mFactory.createStatementFromText("initView();", mClass), setContentViewStatement);
            }

            generatorLayoutCode(null);
        }

        // 判斷mClass是不是繼承fragmentClass或者fragmentV4Class
    } else if ((fragmentClass != null && mClass.isInheritor(fragmentClass, true))
            || (fragmentV4Class != null && mClass.isInheritor(fragmentV4Class, true))
            || mClass.getName().contains("Fragment")) {
        // 判斷是否有onCreateView方法
        if (mClass.findMethodsByName("onCreateView", false).length == 0) {
            StringBuilder method = new StringBuilder();
            method.append("@Override public View onCreateView(android.view.LayoutInflater inflater, android.view.ViewGroup container, android.os.Bundle savedInstanceState) {\n");
            method.append("\t// TODO: run FindViewById again To setValue in initView method\n");
            method.append("\tView view = View.inflate(getActivity(), R.layout.");
            method.append(mSelectText);
            method.append(", null);");
            method.append("return view;");
            method.append("}");
            // 新增
            mClass.add(mFactory.createMethodFromText(method.toString(), mClass));

        } else {
            // 查詢onCreateView
            PsiReturnStatement returnStatement = null;
            // view
            String returnValue = null;
            // onCreateView是否存在initView方法
            boolean hasInitViewStatement = false;

            PsiMethod onCreate = mClass.findMethodsByName("onCreateView", false)[0];
            for (PsiStatement psiStatement : onCreate.getBody().getStatements()) {
                if (psiStatement instanceof PsiReturnStatement) {
                    // 獲取view的值
                    returnStatement = (PsiReturnStatement) psiStatement;
                    returnValue = returnStatement.getReturnValue().getText();
                } else if (psiStatement.getFirstChild() instanceof PsiMethodCallExpression) {
                    PsiReferenceExpression methodExpression = ((PsiMethodCallExpression) psiStatement.getFirstChild()).getMethodExpression();
                    if (methodExpression.getText().equals("initView")) {
                        hasInitViewStatement = true;
                    }
                }
            }

            if (!hasInitViewStatement && returnStatement != null && returnValue != null) {
                onCreate.getBody().addBefore(mFactory.createStatementFromText("initView(" + returnValue + ");", mClass), returnStatement);
            }
            generatorLayoutCode(returnValue);
        }
    }
}

/**
 * 寫initView方法
 *
 * @param findPre Fragment的話要view.findViewById
 */
private void generatorLayoutCode(String findPre) {
    // 判斷是否已有initView方法
    PsiMethod[] initViewMethods = mClass.findMethodsByName("initView", false);
    if (initViewMethods.length > 0 && initViewMethods[0].getBody() != null) {
        PsiCodeBlock initViewMethodBody = initViewMethods[0].getBody();
        for (Element element : mElements) {
            String pre = TextUtils.isEmpty(findPre) ? "" : findPre + ".";
            String s2 = element.getFieldName() + " = (" + element.name + ") " + pre + "findViewById(" + element.getFullID() + ");";
            initViewMethodBody.add(mFactory.createStatementFromText(s2, initViewMethods[0]));
        }
    } else {
        StringBuilder initView = new StringBuilder();
        if (TextUtils.isEmpty(findPre)) {
            initView.append("private void initView() {\n");
        } else {
            initView.append("private void initView(View " + findPre + ") {\n");
        }

        for (Element element : mElements) {
            String pre = TextUtils.isEmpty(findPre) ? "" : findPre + ".";
            initView.append(element.getFieldName() + " = (" + element.name + ")" + pre + "findViewById(" + element.getFullID() + ");\n");
        }
        initView.append("}\n");
        mClass.add(mFactory.createMethodFromText(initView.toString(), mClass));
    }

}

}
```

使用外掛

匯出外掛Build->Prepare All Plugin Modules For Deployment


使用外掛

Android Studio匯入外掛,當前是本地,直接通過Install plugin from disk...匯入。


匯入外掛

部落格的程式碼不是完整的,更多內容可以到GitHub上下載檢視GitHub,此外掛以後會繼續更新,歡迎Start,Issuse

釋出外掛

主要的步驟就是註冊賬號,提交相應的jar檔案,然後填寫資訊,最後等待稽核就可以了。


等著稽核

感謝



文/叫我旺仔(簡書作者)
原文連結:http://www.jianshu.com/p/ae2a890d3a2d
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。