IOC注入框架设计<三>-------Android Studio插件开发来自动生成ButterKnife相关代码

在上一次https://www.cnblogs.com/webor2006/p/12392259.html中对于Android Studio的插件实现入了一下门,这次则通过一个案例对于编写Android Studio的插件进行全面的学习,可能在实际工作中自己来编写插件的机会不多,但是你如果懂得写插件的思路可能在实际某个场景上是能帮上忙的。

插件效果演示:

那这次要实现的插件效果长啥样呢?其实就是我们之前在使用ButterKnife时手写的代码让其被插件来自动生成,解放双手,下面来看一下最终效果:

而布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_test1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="test1" />

    <Button
        android:id="@+id/btn_test2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="test2" />

</LinearLayout>

正常我们要使用时肯定得利用ButterKnife的注解来初始化这些控件,但有了插件之后就不用写了,效果如下:

选择了之后则会根据布局文件的元素弹出一个选择框出来:

此时我们直接点击确定看一下效果:

看见木有:

而如果选择要生成事件方法,则:

相当的6~~接下来继续,该插件还有一些容错处理,比如我们光标选择的是一个无效的内容:

而如果选择了这样的位置则会让用户自己来输入布局文件的名称,同样也能正常生成代码:

以上就是关于这次要实现的插件功能的整个功能预览,效果还是相当赞的,当然要实现也不是很轻松,接下来则一点点来攻克这项技能。

实现插件:

新建插件项目:

到IntelliJ IDEA中新建一个插件工程:

其中咱们定义一下我们的插件在Android Studio中出现的位置:

接下来则就是在这个Action中来篇写插件的代码。

插件功能实现:

思路:

在正式编写之前,先来挼一下实现思路,这样实现起来也更加清晰:

具体实现:

【提示】:前方代码高能,会用到很多的新的API,主要理解大体流程既可,一些新的API这个到时有需要用的时再来翻阅就成,用得多了自然而然就熟了。

先来写好TODO:

下面则按步聚一一来实现,由于是纯小白第一次学写插件,所以尽量一句句代码来理解:

1、获取用户选择的layout文件名称:

其中啥时候mEditor会为null呢?也就是用户此时没有选中代码编辑区,而是在编辑区外,比如:

接下来则来获取一下光标所选的内容:

如果获取的用户选择的内容为空,那就有两种情况会出现:

此时则需要弹一个输入框让用户自己来输入当前的布局文件名。另一种情况是:

此时就直接从当前行的代码中提取出布局文件名,依据这样的分析下面来实现一下,稍稍有点复杂:

 

这里直接贴出代码了,有相印的注释,一看就能懂的:

    private String getCurrentLineLayout(Editor editor) {
        Document document = editor.getDocument();
        //取到插字光标模式对象
        CaretModel caretModel = editor.getCaretModel();
        //得到光标的位置
        int caretOffset = caretModel.getOffset();
        //得到一行开始和结束的地方
        int lineNum = document.getLineNumber(caretOffset);
        int lineStartOffset = document.getLineStartOffset(lineNum);
        int lineEndOffset = document.getLineEndOffset(lineNum);

        //获取一行内容
        String lineContent = document.getText(new TextRange(lineStartOffset, lineEndOffset));
        String layoutMatching = "R.layout.";//从行内容中来找布局匹配字符串

        if (!TextUtils.isEmpty(lineContent) && lineContent.contains(layoutMatching)) {
            //获取layout文件的字符串
            int startPosition = lineContent.indexOf(layoutMatching) + layoutMatching.length();
            int endPosition = lineContent.indexOf(")", startPosition);
            String layoutStr = lineContent.substring(startPosition, endPosition);
            return layoutStr;
        }
        return null;
    }

好,假如说此时还是没有获取到布局的文件名,那么此时就需要弹个对话框让用户进行输入了,也就是:

具体怎么显示呢?这里就得用到IDE提供的API了,这里直接贴出代码了,没啥好解释的,一回生二回熟:

其中有个工具类,里面则提显示提示框的,如:

 

package utils;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.ui.JBColor;

import java.awt.*;

public class Util {
    /**
     * 显示dialog
     *
     * @param editor
     * @param result 内容
     * @param time   显示时间,单位秒
     */
    public static void showPopupBalloon(final Editor editor, final String result, final int time) {
        ApplicationManager.getApplication().invokeLater(new Runnable() {
            public void run() {
                JBPopupFactory factory = JBPopupFactory.getInstance();
                factory.createHtmlTextBalloonBuilder(result, null, new JBColor(new Color(116, 214, 238), new Color(76, 112, 117)), null)
                        .setFadeoutTime(time * 1000)
                        .createBalloon()
                        .show(factory.guessBestPopupLocation(editor), Balloon.Position.below);
            }
        });
    }


}

2、找到对应的XML文件并把XML文件中所有的ID解析放到集合中:

找到了资源文件之后,接下来则需要解析这个布局文件,将里面所有带ID的View解析出来,如:

而解析此时就需要将它转换成PSI(Program Sturcture Interface)对象,啥叫PSI呢?像带有结构的文件都可以是PSI,如XML、Java、Html、Css,而具体如何转调用相应API既可:

 

而PsiFile有N多子类:

 

很明显咱们的布局文件的PsiFile的子类型是XmlFile,所以:

其中Element是咱们自己封装的,在之后的UI绘制会用到,定义如下:

package entity;

import com.intellij.psi.xml.XmlTag;
import org.apache.http.util.TextUtils;
import utils.Util;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Element {

    // 判断id正则                                                  android:id="@+id/btn",只有满足这种才是View的ID了
    private static final Pattern sIdPattern = Pattern.compile("@\\+?(android:)?id/([^$]+)$", Pattern.CASE_INSENSITIVE);
    // id
    private String id;
    // 名字如TextView
    private String name;
    // 命名风格1 aa_bb_cc; 2 aaBbCc 3 mAaBbCc
    private int fieldNameType = 3;
    private String fieldName;
    private XmlTag xml;
    // 是否生成
    private boolean isCreateFiled = true;
    // 是否Clickable
    private boolean isCreateClickMethod = false;

    /**
     * 构造函数
     *
     * @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) {
            // 包名.TextView
            // name="TextView"
            this.name = packages[packages.length - 1];
        } else {
            this.name = name;
        }

        this.xml = xml;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getFieldNameType() {
        return fieldNameType;
    }

    public void setFieldNameType(int fieldNameType) {
        this.fieldNameType = fieldNameType;
    }

    public XmlTag getXml() {
        return xml;
    }

    public void setXml(XmlTag xml) {
        this.xml = xml;
    }

    // 是否创建Filed属性
    public void setIsCreateFiled(boolean isCreateFiled) {
        this.isCreateFiled = isCreateFiled;
    }

    public boolean isCreateFiled() {
        return isCreateFiled;
    }

    // 是否创建Click方法
    public void setIsCreateClickMethod(boolean isCreateClickMethod) {
        this.isCreateClickMethod = isCreateClickMethod;
    }

    public boolean isCreateClickMethod() {
        return isCreateClickMethod;
    }

    /**
     * 获取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() {
        if (TextUtils.isEmpty(this.fieldName)) {
            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(Util.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(Util.firstToUpperCase(names[i]));
                }
                fieldName = sb.toString();
            }
            this.fieldName = fieldName;
        }
        return this.fieldName;
    }

    public void setFieldName(String fieldName) {
        this.fieldName = fieldName;
    }
}

接下来则就是集中来实现这个解析方法了:

很明显需要遍历整个布局文件,这里可以用现成的API就能达到遍历的效果,如下:

比如我们布局中有2个Button,那么这个visitElement()回调就会调用两次,接下来则就可以处理具体的控件解析了:

/**
     * 获取所有带有id的元素
     */
    public static java.util.List<Element> getIDsFromLayout(final PsiFile file, final java.util.List<Element> elements) {
        //遍历一个文件的所有元素
        file.accept(new XmlRecursiveElementVisitor() {
            @Override
            public void visitElement(PsiElement element) {
                super.visitElement(element);
                //element就是一个XML文件中的所有节点
                if (element instanceof XmlTag) {
                    XmlTag tag = (XmlTag) element;
                    //获取Tag的名字(Button)或自定义的
                    String name = tag.getName();
                    //如果有include
                    if (name.equalsIgnoreCase("include")) {
                        //TODO:获得具体的布局文件继续进行递归布局解析
                        return;
                    }
                    //获取id  android:id="@+id/btn_test1"
                    XmlAttribute id = tag.getAttribute("android:id", null);
                    if (id == null) {
                        return;
                    }
                    //获取id的值   @+id/btn_test1
                    String idValue = id.getValue();
                    if (idValue == null) {
                        return;
                    }
                    //获取节点对应的类  比如 TextView  Button
                    XmlAttribute aClass = tag.getAttribute("class", null);
                    if (aClass != null) {
                        //得到类名    "包名.Button"
                        name = aClass.getValue();
                    }
                    //添加到list中
                    Element e = new Element(name, idValue, tag);
                    elements.add(e);
                }

            }
        });

        return elements;
    }

此时则需要处理一下include标签的情况,很明显它又包含了一个布局文件,则需要对它也进行递归解析,如下:

/**
     * 获取所有带有id的元素
     */
    public static java.util.List<Element> getIDsFromLayout(final PsiFile file, final java.util.List<Element> elements) {
        //遍历一个文件的所有元素
        file.accept(new XmlRecursiveElementVisitor() {
            @Override
            public void visitElement(PsiElement element) {
                super.visitElement(element);
                //element就是一个XML文件中的所有节点
                if (element instanceof XmlTag) {
                    XmlTag tag = (XmlTag) element;
                    //获取Tag的名字(Button)或自定义的
                    String name = tag.getName();
                    //如果有include
                    if (name.equalsIgnoreCase("include")) {
                        //获得具体的布局文件继续进行递归布局解析
                        XmlAttribute layout = tag.getAttribute("layout", null);
                        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  android:id="@+id/btn_test1"
                    XmlAttribute id = tag.getAttribute("android:id", null);
                    if (id == null) {
                        return;
                    }
                    //获取id的值   @+id/btn_test1
                    String idValue = id.getValue();
                    if (idValue == null) {
                        return;
                    }
                    //获取节点对应的类  比如 TextView  Button
                    XmlAttribute aClass = tag.getAttribute("class", null);
                    if (aClass != null) {
                        //得到类名    "包名.Button"
                        name = aClass.getValue();
                    }
                    //添加到list中
                    Element e = new Element(name, idValue, tag);
                    elements.add(e);
                }

            }
        });

        return elements;
    }

    /**
     * layout.getValue()返回的值为@layout/layout_view
     * 该方法返回layout_view
     */
    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];
    }

至此,XML中的元素就已经解析完了,另外这里封装成工具方法也就是可以供未来实际工作需要进行使用,那时候只需要知道怎么用就成了,具体的细节不可能完全记住。

3、生成用户选择的UI 

接下来则需要生成用户选择框了,纯Java Swing的编程,这里就不一一去解读了,代码量有点大,而且也不是这次的核心,重点是理解写插件的流程,直接贴出了:

其中新建了三个跟View有关的类,如下:

IdBean则为每一行View的元数据:

package view;

import entity.Element;

import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;


public class IdBean extends JPanel {
    private JCheckBox mEnableCheckBox;
    private JLabel mIdJLabel;
    private JCheckBox mClickCheckBox;
    private JTextField mFieldJTextField;

    /**
     * mEnableCheckBox接口
     */
    public interface EnableActionListener {
        void setEnable(JCheckBox enableCheckBox, Element element);
    }

    private EnableActionListener mEnableListener;

    public void setEnableActionListener(EnableActionListener enableActionListener) {
        mEnableListener = enableActionListener;
    }

    /**
     * mFieldJTextField接口
     */
    public interface FieldFocusListener {
        void setFieldName(JTextField fieldJTextField);
    }

    private FieldFocusListener mFieldFocusListener;

    public void setFieldFocusListener(FieldFocusListener fieldFocusListener) {
        mFieldFocusListener = fieldFocusListener;
    }

    /**
     * mClickCheckBox接口
     */
    public interface ClickActionListener {
        void setClick(JCheckBox clickCheckBox);
    }

    private ClickActionListener mClickListener;

    public void setClickActionListener(ClickActionListener clickListener) {
        mClickListener = clickListener;
    }

    /**
     * 构造方法
     *
     * @param layout         布局
     * @param emptyBorder    border
     * @param jCheckBox      是否生成+name
     * @param jLabelId       id
     * @param jCheckBoxClick onClick
     * @param jTextField     字段名
     */
    public IdBean(LayoutManager layout, EmptyBorder emptyBorder,
                  JCheckBox jCheckBox, JLabel jLabelId, JCheckBox jCheckBoxClick, JTextField jTextField,
                  Element element) {
        super(layout);
        initLayout(layout, emptyBorder);
        mEnableCheckBox = jCheckBox;
        mIdJLabel = jLabelId;
        mClickCheckBox = jCheckBoxClick;
        mFieldJTextField = jTextField;
        initComponent(element);
        addComponent();
    }

    /**
     * addComponent
     */
    private void addComponent() {
        this.add(mEnableCheckBox);
        this.add(mIdJLabel);
        this.add(mClickCheckBox);
        this.add(mFieldJTextField);
    }

    /**
     * 设置Component
     */
    private void initComponent(Element element) {
        mEnableCheckBox.setSelected(element.isCreateFiled());
        mClickCheckBox.setEnabled(true);
        if (element.isCreateClickMethod()) {
            mClickCheckBox.setSelected(element.isCreateClickMethod());
        }

        mIdJLabel.setEnabled(element.isCreateFiled());
        mFieldJTextField.setEnabled(element.isCreateFiled());

        // 设置左对齐
        mEnableCheckBox.setHorizontalAlignment(JLabel.LEFT);
        mIdJLabel.setHorizontalAlignment(JLabel.LEFT);
        mFieldJTextField.setHorizontalAlignment(JTextField.LEFT);
        // 监听
        mEnableCheckBox.addActionListener(e -> {
            if (mEnableListener != null) {
                mEnableListener.setEnable(mEnableCheckBox,element);
                mIdJLabel.setEnabled(mEnableCheckBox.isSelected());
                mFieldJTextField.setEnabled(mEnableCheckBox.isSelected());
            }
        });
        // 监听
        mClickCheckBox.addActionListener(e -> {
            if (mClickListener != null) {
                mClickListener.setClick(mClickCheckBox);
            }
        });
        // 监听
        mFieldJTextField.addFocusListener(new FocusListener() {
            @Override
            public void focusGained(FocusEvent e) {
                if (mFieldFocusListener != null) {
                    mFieldFocusListener.setFieldName(mFieldJTextField);
                }
            }

            @Override
            public void focusLost(FocusEvent e) {
                if (mFieldFocusListener != null) {
                    mFieldFocusListener.setFieldName(mFieldJTextField);
                }
            }
        });
    }

    /**
     * 设置布局相关
     *
     * @param layout
     * @param emptyBorder
     */
    private void initLayout(LayoutManager layout, EmptyBorder emptyBorder) {
        // 设置布局内容
        this.setLayout(layout);
        // 设置border
        this.setBorder(emptyBorder);
    }
}

都是UI的细节,其实跟Android的UI还比较类似。

FindViewByIdDialog:则为整个窗体的封装,代码量较多:

package view;

import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiFile;
import com.intellij.ui.components.JBScrollPane;
import entity.Element;

import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;


public class FindViewByIdDialog extends JFrame implements ActionListener, IdBean.EnableActionListener {
    private String mTitle = "FindViewByIdDialog";
    private Project mProject;
    private Editor mEditor;
    private String mSelectedText;
    private List<Element> mElements;
    // 获取当前文件
    private PsiFile mPsiFile;
    // 获取class
    private PsiClass mClass;


    // 标签JPanel
    private JPanel mPanelTitle = new JPanel();
    private JLabel mTitleId = new JLabel("ViewId");
    private JLabel mTitleClick = new JLabel("OnClick");
    private JLabel mTitleField = new JLabel("ViewFiled");

    // 内容JPanel
    private JPanel mContentJPanel = new JPanel();
    private GridBagLayout mContentLayout = new GridBagLayout();
    private GridBagConstraints mContentConstraints = new GridBagConstraints();
    // 内容JBScrollPane滚动
    private JBScrollPane jScrollPane;

    // 底部JPanel
    // LayoutInflater JPanel
    private JPanel mPanelInflater = new JPanel(new FlowLayout(FlowLayout.LEFT));
    // 是否全选
    private JCheckBox mCheckAll = new JCheckBox("ViewWidget");
    // 确定、取消JPanel
    private JPanel mPanelButtonRight = new JPanel();
    private JButton mButtonConfirm = new JButton("确定");
    private JButton mButtonCancel = new JButton("取消");

    // GridBagLayout不要求组件的大小相同便可以将组件垂直、水平或沿它们的基线对齐
    private GridBagLayout mLayout = new GridBagLayout();
    // GridBagConstraints用来控制添加进的组件的显示位置
    private GridBagConstraints mConstraints = new GridBagConstraints();

    public FindViewByIdDialog(Editor editor, Project project, PsiFile psiFile, PsiClass psiClass, List<Element> elements, String selectedText) {
        mEditor = editor;
        mProject = project;
        mSelectedText = selectedText;
        mElements = elements;
        mPsiFile = psiFile;
        mClass = psiClass;
        initTopPanel();
        initExist();
        initContentPanel();
        initBottomPanel();
        setConstraints();
        setDialog();
    }

    /**
     * 判断已存在的变量,设置全选
     * 判断onclick是否写入
     */
    private void initExist() {
        // 判断是否全选  记录当前可用的个数
        int mCurrentAbleSize = 0;
        // 判断是否已存在的变量
        boolean isFdExist = false;

        for (Element element : mElements) {
            // 判断ViewById是否存在
            if (mClass.getText().contains("@ViewById(" + element.getFullID() + ")")) {
                isFdExist = true;
            } else {
                isFdExist = false;
            }

            // 如果当前没有该属性注解存在
            if (!isFdExist) {
                mCurrentAbleSize++;
                element.setIsCreateFiled(true);
            } else {
                element.setIsCreateFiled(false);
            }

            mCheckAll.setSelected(mCurrentAbleSize == mElements.size());
            mCheckAll.addActionListener(this);
        }
    }

    /**
     * 添加头部
     */
    private void initTopPanel() {
        mPanelTitle.setLayout(new GridLayout(1, 4, 10, 10));
        mPanelTitle.setBorder(new EmptyBorder(5, 10, 5, 10));
        mTitleId.setHorizontalAlignment(JLabel.LEFT);
        mTitleClick.setHorizontalAlignment(JLabel.LEFT);
        mTitleField.setHorizontalAlignment(JLabel.LEFT);
        // 添加到JPanel
        mPanelTitle.add(mCheckAll);
        mPanelTitle.add(mTitleId);
        mPanelTitle.add(mTitleClick);
        mPanelTitle.add(mTitleField);
        mPanelTitle.setSize(720, 30);
        // 添加到JFrame
        getContentPane().add(mPanelTitle, 0);
    }

    /**
     * 添加底部
     */
    private void initBottomPanel() {
        // 添加监听
        mButtonConfirm.addActionListener(this);
        mButtonCancel.addActionListener(this);
        // 右边
        mPanelButtonRight.add(mButtonConfirm);
        mPanelButtonRight.add(mButtonCancel);
        // 添加到JFrame
        getContentPane().add(mPanelInflater, 2);
        getContentPane().add(mPanelButtonRight, 3);
    }

    /**
     * 解析mElements,并添加到JPanel
     */
    private void initContentPanel() {
        mContentJPanel.removeAll();
        // 设置内容
        for (int i = 0; i < mElements.size(); i++) {
            Element mElement = mElements.get(i);
            IdBean itemJPanel = new IdBean(new GridLayout(1, 4, 10, 10),
                    new EmptyBorder(5, 10, 5, 10),
                    new JCheckBox(mElement.getName()),
                    new JLabel(mElement.getId()),
                    new JCheckBox(),
                    new JTextField(mElement.getFieldName()),
                    mElement);
            // 监听
            itemJPanel.setEnableActionListener(this);
            itemJPanel.setClickActionListener(clickCheckBox -> mElement.setIsCreateClickMethod(clickCheckBox.isSelected()));
            itemJPanel.setFieldFocusListener(fieldJTextField -> mElement.setFieldName(fieldJTextField.getText()));
            mContentJPanel.add(itemJPanel);
            mContentConstraints.fill = GridBagConstraints.HORIZONTAL;
            mContentConstraints.gridwidth = 0;
            mContentConstraints.gridx = 0;
            mContentConstraints.gridy = i;
            mContentConstraints.weightx = 1;
            mContentLayout.setConstraints(itemJPanel, mContentConstraints);
        }
        mContentJPanel.setLayout(mContentLayout);
        jScrollPane = new JBScrollPane(mContentJPanel);
        jScrollPane.revalidate();
        // 添加到JFrame
        getContentPane().add(jScrollPane, 1);
    }

    /**
     * 设置Constraints
     */
    private void setConstraints() {
        // 使组件完全填满其显示区域
        mConstraints.fill = GridBagConstraints.BOTH;
        // 设置组件水平所占用的格子数,如果为0,就说明该组件是该行的最后一个
        mConstraints.gridwidth = 0;
        // 第几列
        mConstraints.gridx = 0;
        // 第几行
        mConstraints.gridy = 0;
        // 行拉伸0不拉伸,1完全拉伸
        mConstraints.weightx = 1;
        // 列拉伸0不拉伸,1完全拉伸
        mConstraints.weighty = 0;
        // 设置组件
        mLayout.setConstraints(mPanelTitle, mConstraints);
        mConstraints.fill = GridBagConstraints.BOTH;
        mConstraints.gridwidth = 1;
        mConstraints.gridx = 0;
        mConstraints.gridy = 1;
        mConstraints.weightx = 1;
        mConstraints.weighty = 1;
        mLayout.setConstraints(jScrollPane, mConstraints);
        mConstraints.fill = GridBagConstraints.HORIZONTAL;
        mConstraints.gridwidth = 0;
        mConstraints.gridx = 0;
        mConstraints.gridy = 2;
        mConstraints.weightx = 1;
        mConstraints.weighty = 0;
        mLayout.setConstraints(mPanelInflater, mConstraints);
        mConstraints.fill = GridBagConstraints.NONE;
        mConstraints.gridwidth = 0;
        mConstraints.gridx = 0;
        mConstraints.gridy = 3;
        mConstraints.weightx = 0;
        mConstraints.weighty = 0;
        mConstraints.anchor = GridBagConstraints.EAST;
        mLayout.setConstraints(mPanelButtonRight, mConstraints);
    }

    /**
     * 显示dialog
     */
    public void showDialog() {
        // 显示
        setVisible(true);
    }

    /**
     * 设置JFrame参数
     */
    private void setDialog() {
        // 设置标题
        setTitle(mTitle);
        // 设置布局管理
        setLayout(mLayout);
        // 不可拉伸
        setResizable(false);
        // 设置大小
        setSize(720, 405);
        // 自适应大小
        // pack();
        // 设置居中,放在setSize后面
        setLocationRelativeTo(null);
        // 显示最前
        setAlwaysOnTop(true);
    }

    /**
     * 关闭dialog
     */
    public void cancelDialog() {
        setVisible(false);
        dispose();
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        switch (e.getActionCommand()) {
            case "确定":
                cancelDialog();
                //根据用户的选择来生成代码,生成activity中要插入的代码
                setCreator();
                break;
            case "取消":
                cancelDialog();
                break;
            case "ViewWidget":
                // 刷新
                for (Element mElement : mElements) {
                    mElement.setIsCreateFiled(mCheckAll.isSelected());
                }
                remove(jScrollPane);
                initContentPanel();
                setConstraints();
                revalidate();
                break;
        }
    }

    /**
     * 生成
     */
    private void setCreator() {
        new ViewFieldMethodCreator(this, mEditor, mPsiFile, mClass,
                "Generate Injections", mElements, mSelectedText)
                .execute();
    }

    /**
     * 更新所有选中的CheckBox
     */
    private void updateAllSelectCb() {
        boolean isAllSelect = true;
        for (Element element : mElements) {
            if (!element.isCreateFiled()) {
                isAllSelect = false;
                break;
            }
        }
        mCheckAll.setSelected(isAllSelect);
    }

    @Override
    public void setEnable(JCheckBox enableCheckBox, Element element) {
        element.setIsCreateFiled(enableCheckBox.isSelected());
        updateAllSelectCb();
    }
}

4、根据用户使用UI时的选择生成最终代码:

其中点击确定之后,则会通过ViewFieldMethodCreator为我们生成代码:

package view;

import com.intellij.codeInsight.actions.ReformatCodeProcessor;
import com.intellij.openapi.command.WriteCommandAction.Simple;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import entity.Element;
import utils.Util;

import java.util.List;

/**
 * 用来生成我们需要的控件注入与事件注入代码
 */
public class ViewFieldMethodCreator extends Simple {

    private FindViewByIdDialog mDialog;
    private Editor mEditor;
    private PsiFile mFile;
    private Project mProject;
    private PsiClass mClass;
    private List<Element> mElements;
    private PsiElementFactory mFactory;

    public ViewFieldMethodCreator(FindViewByIdDialog dialog, Editor editor, PsiFile psiFile, PsiClass psiClass, String command, List<Element> elements, String selectedText) {
        super(psiClass.getProject(), command);
        mDialog = dialog;
        mEditor = editor;
        mFile = psiFile;
        mProject = psiClass.getProject();
        mClass = psiClass;
        mElements = elements;
        // 获取Factory
        mFactory = JavaPsiFacade.getElementFactory(mProject);
    }

    /**
     * 单独用一个线程来生成代码
     */
    @Override
    protected void run() throws Throwable {
        //1、TODO:生成属性

        //2、TODO:生成方法

        //3、TODO:重写Mainactivity类  把内容插入到MainActivity.java文件中
        //      a.找到对应的项目
        //      b.连接到一个PSI文件
        //      c.  2中的PSI关联上mClass
        //      d. 执行写入
    }
}

其中在生成代码时会执行它的run()方法,接下来则集中处理这个run()方法:

生成属性:

其实就是根据Element来进行字符串的组拼,具体代码如下:

生成方法:

就是这块:

其实现跟上面的属性差不多,代码如下:

/**
     * 创建监听事件方法
     */
    private void generateOnClickMethod() {
        for (Element element : mElements) {
            if (element.isCreateClickMethod()) {
                //生成onClick()
                String methodName = getClickMethodName(element) + "Click";
                PsiMethod[] onClickMethod = mClass.findMethodsByName(methodName, true);
                boolean clickMethodExist = onClickMethod.length > 0;
                if (!clickMethodExist) {
                    createClickMethod(methodName, element);
                }
            }
        }
    }

    private void createClickMethod(String methodName, Element element) {
//        @OnClick(R.id.tvText)
//        private void tvTextClick(TextView tvText) {
//        }
        StringBuilder methodBuilder = new StringBuilder();
        methodBuilder.append("@OnClick(" + element.getFullID() + ")\n");
        methodBuilder.append("private void " + methodName + "(" + element.getName() + " "
                + getClickMethodName(element) + "){");
        methodBuilder.append("\n}");
        //创建onclick方法
        mClass.add(mFactory.createMethodFromText(methodBuilder.toString(), mClass));
    }

    /**
     * 获取点击方法的名称,如果是tv_text这种格式会转成tvText
     */
    public String getClickMethodName(Element element) {
        String[] names = element.getId().split("_");
        // aaBbCc
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < names.length; i++) {
            if (i == 0) {
                sb.append(names[i]);
            } else {
                sb.append(Util.firstToUpperCase(names[i]));
            }
        }
        return sb.toString();
    }

执行写入:

现在要生成的代码已经添加到了PsiClass对象当中了,接下来则需要将其写入到目标类中,具体代码如下:

至此,所有插件的代码已经完了,接下来则可以打包生成插件了。

打包安装:

最后再来安装一下,具体就不演示了,在实际运行过程中发现还是有一处代码写得有问题:

学会了写插件之后,在实际工作中如果遇到了一些重复性比较高的机械式的代码则都可以用这种思路去化解,不过目前还未在实际工作中真实写过插件,先把这个技能解锁了,待未来有机会随时恭候!!

posted on 2020-03-04 22:26  cexo  阅读(556)  评论(0编辑  收藏  举报

导航