Butterknife做过android的人都知道,帮助我们少写了很多findViewById,setOnClickListener的机械代码。 Butterknife配合插件android-butterknife-zelezny使用起来,开发效率大大提升。一直感觉插件是很牛逼的东西,扩展了IDE的功能。平常需求迭代很忙,很遗憾没花时间自己写一些插件。今天借着研究Butterknife在组件化开发的问题,顺带研究了如何开发代码生成的插件。
首先创建一个插件的工程把android-butterknife-zelezny的代码拷贝进项目。
META-INF中的plugin.xml是插件的配置项。id ,name, version 是要上传到插件库的字段。 我们主要实现的是actions。里面包含我们实现的一些动作。为了方便观察效果,我修改了部分代码。
最终效果如下:
先讲InjectAction类的结构.InjectAction 继承于框架类BaseGenerateAction,代表是一个生成菜单的动作。
public class InjectAction extends BaseGenerateAction implements IConfirmListener, ICancelListener {
.....
}
当用户点击我们写的上下文菜单中的选项会触发actionPerformed——->actionPerformedImpl. 框架中有各种抽象出的类,如Project类,Editor代表光标选中的部分,项目文件PsiFile类。 Utils.getLayoutFileFromCaret(editor, file)先找到光标选中的R.layout.xxx文件。Utils.getIDsFromLayout(layout)找到layout文件里面的id用Element类包装起来。
@Override
public void actionPerformed(AnActionEvent event) {
Project project = event.getData(PlatformDataKeys.PROJECT);
Editor editor = event.getData(PlatformDataKeys.EDITOR);
actionPerformedImpl(project, editor);
}
@Override
public void actionPerformedImpl(Project project, Editor editor) {
PsiFile file = PsiUtilBase.getPsiFileInEditor(editor, project);
PsiFile layout = Utils.getLayoutFileFromCaret(editor, file);
if (layout == null) {
Utils.showErrorNotification(project, "No layout found");
return; // no layout found
}
log.info("Layout file: " + layout.getVirtualFile());
ArrayList<Element> elements = Utils.getIDsFromLayout(layout);
if (!elements.isEmpty()) {
showDialog(project, editor, elements);
} else {
Utils.showErrorNotification(project, "No IDs found in layout");
}
}
根据xml文件名称,在model和项目中递归查找。
public static PsiFile getLayoutFileFromCaret(Editor editor, PsiFile file) {
int offset = editor.getCaretModel().getOffset();
PsiElement candidateA = file.findElementAt(offset);
PsiElement candidateB = file.findElementAt(offset - 1);
PsiFile layout = findLayoutResource(candidateA);
if (layout != null) {
return layout;
}
return findLayoutResource(candidateB);
}
public static PsiFile findLayoutResource(PsiElement element) {
log.info("Finding layout resource for element: " + element.getText());
if (element == null) {
return null; // nothing to be used
}
if (!(element instanceof PsiIdentifier)) {
return null; // nothing to be used
}
PsiElement layout = element.getParent().getFirstChild();
if (layout == null) {
return null; // no file to process
}
if (!"R.layout".equals(layout.getText())) {
return null; // not layout file
}
Project project = element.getProject();
String name = String.format("%s.xml", element.getText());
return resolveLayoutResourceFile(element, project, name);
}
private static PsiFile resolveLayoutResourceFile(PsiElement element, Project project, String name) {
// restricting the search to the current module - searching the whole project could return wrong layouts
Module module = ModuleUtil.findModuleForPsiElement(element);
PsiFile[] files = null;
if (module != null) {
// first omit libraries, it might cause issues like (#103)
GlobalSearchScope moduleScope = module.getModuleWithDependenciesScope();
files = FilenameIndex.getFilesByName(project, name, moduleScope);
if (files == null || files.length <= 0) {
// now let's do a fallback including the libraries
moduleScope = module.getModuleWithDependenciesAndLibrariesScope(false);
files = FilenameIndex.getFilesByName(project, name, moduleScope);
}
}
if (files == null || files.length <= 0) {
files = FilenameIndex.getFilesByName(project, name, new EverythingGlobalScope(project));
if (files.length <= 0) {
return null; //no matching files
}
}
for (PsiFile file : files) {
log.info("Resolved layout resource file for name [" + name + "]: " + file.getVirtualFile());
}
return files[0];
}
public static PsiFile findLayoutResource(PsiFile file, Project project, String fileName) {
String name = String.format("%s.xml", fileName);
// restricting the search to the module of layout that includes the layout we are seaching for
return resolveLayoutResourceFile(file, project, name);
}
根据layout 查找里面的id,返回Element集合。
public static ArrayList<Element> getIDsFromLayout(final PsiFile file) {
final ArrayList<Element> elements = new ArrayList<Element>();
return getIDsFromLayout(file, elements);
}
/**
* Obtain all IDs from layout
*
* @param file
* @return
*/
public static ArrayList<Element> getIDsFromLayout(final PsiFile file, final ArrayList<Element> elements) {
file.accept(new XmlRecursiveElementVisitor() {
@Override
public void visitElement(final PsiElement element) {
super.visitElement(element);
if (element instanceof XmlTag) {
XmlTag tag = (XmlTag) element;
if (tag.getName().equalsIgnoreCase("include")) {
XmlAttribute layout = tag.getAttribute("layout", null);
if (layout != null) {
Project project = file.getProject();
PsiFile include = findLayoutResource(file, project, getLayoutName(layout.getValue()));
if (include != null) {
getIDsFromLayout(include, elements);
return;
}
}
}
// get element ID
XmlAttribute id = tag.getAttribute("android:id", null);
if (id == null) {
return; // missing android:id attribute
}
String value = id.getValue();
if (value == null) {
return; // empty value
}
// check if there is defined custom class
String name = tag.getName();
XmlAttribute clazz = tag.getAttribute("class", null);
if (clazz != null) {
name = clazz.getValue();
}
try {
elements.add(new Element(name, value));
} catch (IllegalArgumentException e) {
// TODO log
}
}
}
});
return elements;
}
根据正则表达式拆分名字。
public class Element {
// constants
private static final Pattern sIdPattern = Pattern.compile("@\\+?(android:)?id/([^$]+)$", Pattern.CASE_INSENSITIVE);
private static final Pattern sValidityPattern = Pattern.compile("^([a-zA-Z_\\$][\\w\\$]*)$", Pattern.CASE_INSENSITIVE);
public String id;
public boolean isAndroidNS = false;
public String nameFull; // element name with package
public String name; // element name
public String fieldName; // name of variable
public boolean isValid = false;
public boolean used = true;
public boolean isClick = true;
public Element(String name, String id) {
// id
final Matcher matcher = sIdPattern.matcher(id);
if (matcher.find() && matcher.groupCount() > 0) {
this.id = matcher.group(2);
String androidNS = matcher.group(1);
this.isAndroidNS = !(androidNS == null || androidNS.length() == 0);
}
// name
String[] packages = name.split("\\.");
if (packages.length > 1) {
this.nameFull = name;
this.name = packages[packages.length - 1];
} else {
this.nameFull = null;
this.name = name;
}
this.fieldName = getFieldName();
}
showDialog(project, editor, elements) 就是我们常看到的对话框。这是一个用java 原生UI框架生成的。
下面是展示ui框体的代码。ButterKnifeFactory.findButterKnifeForPsiElement(project, file)是对ButterKnife的版本进行判断,不同版本用到了不同注解,bind,bindview之类的。Definitions.adapters.contains(element.getQualifiedName())判断是不是个适配器。我们知道在适配器中是要创建成一个ViewHolder.EntryList就是上图中的UI控件面板。里面罗列了我们找到的Id 集合,包含他们的类型,id名,和即将要在代码中的字段名。createHolder 将作用于 Create ViewHolder这个checkbox. 右下角两个按钮将回调InjectAction中实现的接口方法。
protected void showDialog(Project project, Editor editor, ArrayList<Element> elements) {
PsiFile file = PsiUtilBase.getPsiFileInEditor(editor, project);
if (file == null) {
return;
}
PsiClass clazz = getTargetClass(editor, file);
final IButterKnife butterKnife = ButterKnifeFactory.findButterKnifeForPsiElement(project, file);
if (clazz == null || butterKnife == null) {
return;
}
// get parent classes and check if it's an adapter
boolean createHolder = false;
PsiReferenceList list = clazz.getExtendsList();
if (list != null) {
for (PsiJavaCodeReferenceElement element : list.getReferenceElements()) {
if (Definitions.adapters.contains(element.getQualifiedName())) {
createHolder = true;
}
}
}
// get already generated injections
ArrayList<String> ids = new ArrayList<String>();
PsiField[] fields = clazz.getAllFields();
String[] annotations;
String id;
for (PsiField field : fields) {
annotations = field.getFirstChild().getText().split(" ");
for (String annotation : annotations) {
id = Utils.getInjectionID(butterKnife, annotation.trim());
if (!Utils.isEmptyString(id)) {
ids.add(id);
}
}
}
EntryList panel = new EntryList(project, editor, elements, ids, createHolder, this, this);
mDialog = new JFrame();
mDialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
mDialog.getRootPane().setDefaultButton(panel.getConfirmButton());
mDialog.getContentPane().add(panel);
mDialog.pack();
mDialog.setLocationRelativeTo(null);
mDialog.setVisible(true);
}
点击确定后,重新获取file和layout ,感觉作者这里可以缓存这些东西。需要view绑定和 view 点击事件,就创建InjectWriter 并执行execute。
public void onConfirm(Project project, Editor editor, ArrayList<Element> elements, String fieldNamePrefix, boolean createHolder, boolean splitOnclickMethods) {
PsiFile file = PsiUtilBase.getPsiFileInEditor(editor, project);
if (file == null) {
return;
}
PsiFile layout = Utils.getLayoutFileFromCaret(editor, file);
closeDialog();
if (Utils.getInjectCount(elements) > 0 || Utils.getClickCount(elements) > 0) { // generate injections
new InjectWriter(file, getTargetClass(editor, file), "Generate Injections", elements, layout.getName(), fieldNamePrefix, createHolder, splitOnclickMethods).execute();
} else { // just notify user about no element selected
Utils.showInfoNotification(project, "No injection was selected");
}
}
public void onCancel() {
closeDialog();
}
InjectWriter.execute最终会执行他的run方法。根据IButterKnife版本和要求,对应调用
generateAdapter,generateFields,generateClick。JavaCodeStyleManager这个类负责导入包。。ReformatCodeProcessor负责格式化代码。
public class InjectWriter extends WriteCommandAction.Simple {
.........
@Override
public void run() throws Throwable {
final IButterKnife butterKnife = ButterKnifeFactory.findButterKnifeForPsiElement(mProject, mFile);
if (butterKnife == null) {
return; // Butterknife library is not available for project
}
if (mCreateHolder) {
generateAdapter(butterKnife);
} else {
if (Utils.getInjectCount(mElements) > 0) {
generateFields(butterKnife);
}
generateInjects(butterKnife);
if (Utils.getClickCount(mElements) > 0) {
generateClick();
}
Utils.showInfoNotification(mProject, String.valueOf(Utils.getInjectCount(mElements)) + " injections and " + String.valueOf(Utils.getClickCount(mElements)) + " onClick added to " + mFile.getName());
}
// reformat class
JavaCodeStyleManager styleManager = JavaCodeStyleManager.getInstance(mProject);
styleManager.optimizeImports(mFile);
styleManager.shortenClassReferences(mClass);
new ReformatCodeProcessor(mProject, mClass.getContainingFile(), null, false).runWithoutProgress();
}
我们随便挑一个generateFields来说,因为比较常见。其实就是遍历之前的id集合。StringBuilder追加java代码。 mClass.add(mFactory.createFieldFromText(injection.toString(), mClass));
方法加入生成代码。
protected void generateFields(@NotNull IButterKnife butterKnife) {
// add injections into main class
for (Element element : mElements) {
if (!element.used) {
continue;
}
StringBuilder injection = new StringBuilder();
// injection.append('@');
// injection.append(butterKnife.getFieldAnnotationCanonicalName());
// injection.append('(');
// injection.append(element.getFullID());
// injection.append(") ");
if (element.nameFull != null && element.nameFull.length() > 0) { // custom package+class
injection.append(element.nameFull);
} else if (Definitions.paths.containsKey(element.name)) { // listed class
injection.append(Definitions.paths.get(element.name));
} else { // android.widget
injection.append("android.widget.");
injection.append(element.name);
}
injection.append(" ");
injection.append(element.fieldName);
injection.append(";");
mClass.add(mFactory.createFieldFromText(injection.toString(), mClass));
}
}
对于一个插件项目点击菜单就可以生成一个jar,这个可以上传插件库,或者选择本地安装都可以,重启android Studio就可以使用。
讲解完毕,有以上的总结,我们可以结合自己的项目用插件的方式生成很多机械式的代码啦。