手写ButterKnife插件核心代码

95 篇文章 0 订阅

上篇写了一个简易版插件的实现,今天写ButterKnife插件的核心代码实现.

思路--》获取AS上光标所在行是文本信息----》从工程找文件信息中的布局文件----》解析Xml文件,获取控件信息----》根据控件生成Ui----》根据确定按钮生成代码

                                                                                        

 

 

1》获取光标所在行的文本信息

通过继承AnAction 实现actionPerformed抽象方法,通过通过AnActionEvent对象可以获取editer对象

//获取使用插件的工程对象
Project project = anActionEvent.getProject();
//获取编译区对象(光标所在额页面)
Editor editor = anActionEvent.getData(PlatformDataKeys.EDITOR);
//获取当前光标所在行的名称---》activity_main
xmlFileName = getCurrentLayout(editor);
/**
 * 获取光标所在行 的布局名称
 *
 * @param editor
 * @return
 */
private String getCurrentLayout(Editor editor) {

    
    //获取editor的document模型
    Document document = editor.getDocument();
    //获取create模型
    CaretModel caretModel = editor.getCaretModel();
    //获取光标的位置(offset==当前编译页最顶部文本到广标所在位置的字母总和)
    int offset = caretModel.getOffset();

    //获取光标所在行号
    int lineNumber = document.getLineNumber(offset);
    //获取光标所在行号的开始位置
    int lineStartOffset = document.getLineStartOffset(lineNumber);
    //获取光标所在行号的结束位置
    int lineEndOffset = document.getLineEndOffset(lineNumber);

    //获取光标所在行是文本
    String layoutName = document.getText(new TextRange(lineStartOffset, lineEndOffset));
    String layout = "R.layout.";

    if (!TextUtils.isEmpty(layoutName) && layoutName.contains(layout)) {
        int start = layoutName.indexOf(layout) + layout.length();
        int end = layoutName.indexOf(")", start);

        return layoutName.substring(start, end);

    }


    return null;
}

 

2》从工程获取xml文件

//返回的是一个数字,可能存在不同包下有一样的xml文件,

//参数(搜索的工程对象,需要找的xml文件名,搜索范围)

PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, xmlFileName + ".xml", GlobalSearchScope.allScope(project));

 

3》解析XML

/**
 * 解析xml核心代码
 * 获取所有id
 *
 * @param file
 * @param list
 * @return
 */
public static java.util.List<Element> getIDsFromLayout(final PsiFile file, final java.util.List<Element> list) {
    file.accept(new XmlRecursiveElementVisitor() {
        /**
         * 系统会解析xml里面的文件有多少发控件,visitElement方法的执行几次
         * @param element
         */
        @Override
        public void visitElement(PsiElement element) {

            super.visitElement(element);

            //判断节点元素是xml标签
            if (element instanceof XmlTag) {
                XmlTag xmlTag = (XmlTag) element;
                //获取标签名-->TextView
                String name = xmlTag.getName();
                //如果name是include就需要另外处理--->获取布局名称,解析这布局
                //equalsIgnoreCase忽略大小写的比较
                if (name.equalsIgnoreCase("include")) {
                    //include信息--->layout="@layout/activity_text"
                    XmlAttribute layout = xmlTag.getAttribute("layout", null);
                    //   <include layout="@layout/activity_text"/>
                    //获取布局名称  layout.getValue()==@layout/activity_text
                    //getLayoutName()方法返回activity_text
                    String layoutName = getLayoutName(layout.getValue());

                    //获取project对象
                    Project project = file.getProject();
                    //获取布局文件--》例如activity_main.xml文件
                    PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, layoutName + ".xml", GlobalSearchScope.allScope(project));

                    //定义个xml标签对象
                    XmlFile include = null;
                    if (psiFiles.length > 0) {
                        include = (XmlFile) psiFiles[0];
                    }

                    if (include != null) {
                        //递归解析
                        getIDsFromLayout(include, list);
                        return;
                    }


                }

                /**
                 * 获取元素信息,保存到自定义的Element对象里面
                 */
                //id信息
                //xmlTag.getAttribute这个方法参数就相当布局key 返回对应value
                //例如 android:id="@+id/text" --》android:id就key  返回
                XmlAttribute value = xmlTag.getAttribute("android:id");

                //如果控件没有id就不做处理
                if (value == null) {
                    return;
                }

                //反回值 --》@+id/text
                String id = value.getValue();
                if (TextUtils.isEmpty(id)) {
                    return;
                }

                //获取控件类型 比如 TextVie Butter
                XmlAttribute aClass = xmlTag.getAttribute("class");
                if (aClass != null) {
                    //获取全类名--》包名+TextView
                    name = aClass.getValue();
                }

                //保存到封装类Element里面
                Element element1 = new Element(name, id, xmlTag);
                //保存到集合
                list.add(element1);


            }


        }
    });


    return list;
}

 

3》绘制UI

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》生成代码(Simple相当线程)

 

public class ViewFieldMethodCreator extends Simple {

    private FindViewByIdDialog mDialog;
    private Editor mEditor;
    private PsiFile mFile;
    private Project mProject;
    private PsiClass mClass;
    private List<Element> elementList;
    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;
        elementList = elements;
        // 获取Factory
        mFactory = JavaPsiFacade.getElementFactory(mProject);
    }

    /**
     * 单独用一个线程来生成代码
     *
     * @throws Throwable
     */
    @Override
    protected void run() throws Throwable {
        //生成属性
        generateFields();
        //生成点击监听方法
        generateOnClickListener();
        //重新mainActivity 把内容插入到MainActivity。Java文件里面
        //1找到对应的项目
        JavaCodeStyleManager styleManager=JavaCodeStyleManager.getInstance(mProject);
        //2连接psi文件  (mFile 光标所在可编辑界面文件)
        styleManager.optimizeImports(mFile);

        //3 就是2中的psi关联上mClass
        styleManager.shortenClassReferences(mClass);

        //4执行
        ReformatCodeProcessor reformatCodeProcessor = new ReformatCodeProcessor(mProject, mClass.getContainingFile(), null, false);
        //执行代码
        reformatCodeProcessor.run();

        Util.showPopupBalloon(mEditor,"生成成功",5);

    }


    /**
     * 生成属性-->  @BindView(R.id.text)
     * TextView text;
     */
    private void generateFields() {
        for (Element element : elementList) {

            /**
             *方法注入
             * 执行下面代码就写到AS的编辑页面里面
             * isCreateFiled是否勾选属性
             */
            if (element.isCreateFiled()) {
                //拼接字符串
                StringBuffer stringBuffer = new StringBuffer();
//            @BindView(R.id.text)
                stringBuffer.append("@BindView(" + element.getFullID() + ")\n");
//         private   TextView text;
                stringBuffer.append("private " + element.getName() + " " + element.getFieldName() + ";");


                //核心代码注入到MainActivity里面

                //public interface PsiField extends PsiJvmMember, PsiVariable, PsiDocCommentOwner, JvmField {
                PsiField fieldFromText = mFactory.createFieldFromText(stringBuffer.toString(), mClass);

                mClass.add(fieldFromText);
            }


        }

    }


    /**
     * 生成点击监听方法
     *
     * @OnClick(R.id.text) public void onViewClicked(TextView tvText) {
     * * <p>
     * * }
     */
    private void generateOnClickListener() {
        for (Element mElement : elementList) {
            //是否勾选点击监听方法
            boolean createClickMethod = mElement.isCreateClickMethod();
            if (createClickMethod) {
                //getClickMethodName这个方法主要text_name的转换成驼峰命令的 textName
                //name为方法名
                String name = getClickMethodName(mElement) + "Clicked";
                //获取mClass(相当mainActivity是否有name这方法,有就返回方法集合)
                PsiMethod[] methodsByName = mClass.findMethodsByName(name, true);

                //看activity里面是否有这方法,没有就创建,
                if (methodsByName.length <= 0) {
                    //写监听方法
                    createClickMethod(name, mElement);
                }


            }
        }

    }

    /**
     * 写方法 核心代码
     *
     * @param name     ------》@OnClick(R.id.text) public void onViewClicked(TextView tvText) {
     *                 <p>
     *                 }
     * @param mElement
     */
    private void createClickMethod(String name, Element mElement) {
        StringBuffer sb = new StringBuffer();
//        @OnClick(R.id.text)
        sb.append("@OnClick(" + mElement.getFullID() + ")\n");
//        public void onViewClicked(TextView tvText) {
        sb.append("public void on" + name + "(" + mElement.getName() + " " + getClickMethodName(mElement)+ ")" + "{\n");

//        }
        sb.append("}");

        PsiMethod methodFromText = mFactory.createMethodFromText(sb.toString(), mClass);
        mClass.add(methodFromText);

    }

    /**
     * 把tv_text的命名转换成 tvText
     *
     * @param mElement
     * @return
     */
    private String getClickMethodName(Element mElement) {
        String[] name = mElement.getId().split("_");
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < name.length; i++) {
            if (i == 0) {
                sb.append(name[i]);
            } else {
                sb.append(Util.firstToUpperCase(name[i]));
            }
        }

        return sb.toString();

    }



}

//源码地址https://github.com/zhudaihao/ButterKnifePlugin

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值