上篇写了一个简易版插件的实现,今天写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(); } }