前言
上一篇文章我已經(jīng)分享了自己簡易打造的IOC注解框架:SteadyoungIOC。留下了文中快速生成代碼的插件未解析单雾,今天就來一步步為大家解析這個(gè)插件的開發(fā)過程赚哗。首先為接觸過Android Studio插件開發(fā)的同學(xué)可以先閱讀:學(xué)會(huì)編寫Android Studio插件 別停留在用的程度了。
分析
下面看看上期中自動(dòng)生成代碼的效果:
自動(dòng)生成注解代碼硅堆,跟ButterKnife的插件類似,但是我們自己寫的插件生成的注解代碼更加符合google源碼規(guī)范屿储,而且是基于我們自己簡易打造的IOC注解框架:SteadyoungIOC。我參考了ButterKnife的源碼渐逃,因?yàn)樵创a過于復(fù)雜够掠,為了提高上手度,只引用了部分功能朴乖。
我們先來整理一下思路祖屏,要實(shí)現(xiàn)這么個(gè)插件我們需要做一些什么東東:
- 獲取光標(biāo)所在行的布局文件 --> R.layout.xxxx.xml助赞;
- 搜索整個(gè)項(xiàng)目獲取到R.layout.xxxx.xml文件;
- 通過該布局文件去遍歷找出含有id的布局標(biāo)簽袁勺,當(dāng)然如果考慮完善一點(diǎn)需要考慮include等等雹食;
- 遍歷完成后生成對(duì)話框,讓用戶可以自己選擇需要生成注解的View以及點(diǎn)擊事件期丰,這個(gè)是Java GUI里面的內(nèi)容
- 最后當(dāng)用戶點(diǎn)擊確定生成最終的注解代碼即可
這么說起來還是挺簡單的群叶,當(dāng)然其中的細(xì)節(jié)還是讓人很蛋疼的,需要不斷反復(fù)的調(diào)試钝荡。
實(shí)現(xiàn)
- 獲取光標(biāo)所在行的布局文件 --> R.layout.xxxx.xml街立;
/**
* 獲取當(dāng)前光標(biāo)的layout文件
*/
private String getCurrentLayout(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);
// 可能是另外一種情況 View.inflate
if (layoutStr.contains(",")) {
endPosition = lineContent.indexOf(",", startPosition);
layoutStr = lineContent.substring(startPosition, endPosition);
}
return layoutStr;
}
return null;
}
- 搜索整個(gè)項(xiàng)目獲取到R.layout.xxxx.xml文件;
@Override
public void actionPerformed(AnActionEvent e) {
// 獲取project
Project project = e.getProject();
// 獲取選中內(nèi)容
final Editor mEditor = e.getData(PlatformDataKeys.EDITOR);
if (null == mEditor) {
return;
}
SelectionModel model = mEditor.getSelectionModel();
mSelectedText = model.getSelectedText();
// 未選中布局內(nèi)容埠通,顯示dialog
if (TextUtils.isEmpty(mSelectedText)) {
// 獲取光標(biāo)所在位置的布局
mSelectedText = getCurrentLayout(mEditor);
if (TextUtils.isEmpty(mSelectedText)) {
mSelectedText = Messages.showInputDialog(project, "布局內(nèi)容:(不需要輸入R.layout.)", "未選中布局內(nèi)容赎离,請(qǐng)輸入layout文件名", Messages.getInformationIcon());
if (TextUtils.isEmpty(mSelectedText)) {
Util.showPopupBalloon(mEditor, "未輸入layout文件名", 5);
return;
}
}
}
// 獲取布局文件,通過FilenameIndex.getFilesByName獲取
// GlobalSearchScope.allScope(project)搜索整個(gè)項(xiàng)目
PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, mSelectedText + ".xml", GlobalSearchScope.allScope(project));
if (psiFiles.length <= 0) {
Util.showPopupBalloon(mEditor, "未找到選中的布局文件" + mSelectedText, 5);
return;
}
XmlFile xmlFile = (XmlFile) psiFiles[0];
List<Element> elements = new ArrayList<>();
Util.getIDsFromLayout(xmlFile, elements);
// 將代碼寫入文件端辱,不允許在主線程中進(jìn)行實(shí)時(shí)的文件寫入
if (elements.size() != 0) {
PsiFile psiFile = PsiUtilBase.getPsiFileInEditor(mEditor, project);
PsiClass psiClass = Util.getTargetClass(mEditor, psiFile);
// 有的話就創(chuàng)建變量和findViewById
if (mDialog != null && mDialog.isShowing()) {
mDialog.cancelDialog();
}
mDialog = new FindViewByIdDialog(mEditor, project, psiFile, psiClass, elements, mSelectedText);
mDialog.showDialog();
} else {
Util.showPopupBalloon(mEditor, "未找到任何Id", 5);
}
}
- 通過該布局文件去遍歷找出含有id的布局標(biāo)簽梁剔,當(dāng)然如果考慮完善一點(diǎn)需要考慮include等等;
/**
* 獲取所有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
// 遍歷一個(gè)文件的所有元素
file.accept(new XmlRecursiveElementVisitor() {
@Override
public void visitElement(PsiElement element) {
super.visitElement(element);
// 解析Xml標(biāo)簽
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;
}
- 遍歷完成后生成對(duì)話框舞蔽,讓用戶可以自己選擇需要生成注解的View以及點(diǎn)擊事件荣病,這個(gè)是Java GUI里面的內(nèi)容,我直接百度找的代碼實(shí)現(xiàn)了效果渗柿,貼出部分源碼:
/**
* 解析mElements个盆,并添加到JPanel
*/
private void initContentPanel() {
mContentJPanel.removeAll();
// 設(shè)置內(nèi)容
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);
// 監(jiān)聽
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);
}
- 最后當(dāng)用戶點(diǎn)擊確定生成最終的注解代碼即可,主要生成注解@FindView(R.id.XXX)朵栖、@OnClick(R.id.XXX)颊亮、在OnCreate中生成SteadyoungIOC.jnject(this)等
/**
* 創(chuàng)建變量
*/
private void generateFields() {
for (Element element : mElements) {
if (mClass.getText().contains("@FindView(" + element.getFullID() + ")")) {
// 不創(chuàng)建新的變量
continue;
}
// 設(shè)置變量名,獲取text里面的內(nèi)容
String text = element.getXml().getAttributeValue("android:text");
if (TextUtils.isEmpty(text)) {
// 如果是text為空陨溅,則獲取hint里面的內(nèi)容
text = element.getXml().getAttributeValue("android:hint");
}
// 如果是@string/app_name類似
if (!TextUtils.isEmpty(text) && text.contains("@string/")) {
text = text.replace("@string/", "");
// 獲取strings.xml
PsiFile[] psiFiles = FilenameIndex.getFilesByName(mProject, "strings.xml", GlobalSearchScope.allScope(mProject));
if (psiFiles.length > 0) {
for (PsiFile psiFile : psiFiles) {
// 獲取src\main\res\values下面的strings.xml文件
String dirName = psiFile.getParent().toString();
if (dirName.contains("src\\main\\res\\values")) {
text = Util.getTextFromStringsXml(psiFile, text);
}
}
}
}
StringBuilder fromText = new StringBuilder();
if (!TextUtils.isEmpty(text)) {
fromText.append("/****" + text + "****/\n");
}
fromText.append("@FindView(" + element.getFullID() + ")\n");
fromText.append("private ");
fromText.append(element.getName());
fromText.append(" ");
fromText.append(element.getFieldName());
fromText.append(";");
// 創(chuàng)建點(diǎn)擊方法
if (element.isCreateFiled()) {
// 添加到class
mClass.add(mFactory.createFieldFromText(fromText.toString(), mClass));
}
}
}
/**
* 創(chuàng)建OnClick方法
*/
private void generateOnClickMethod() {
for (Element element : mElements) {
// 可以使用并且可以點(diǎn)擊
if (element.isCreateClickMethod()) {
// 需要?jiǎng)?chuàng)建OnClick方法
String methodName = getClickMethodName(element) + "Click";
PsiMethod[] onClickMethods = mClass.findMethodsByName(methodName, true);
boolean clickMethodExist = onClickMethods.length > 0;
if (!clickMethodExist) {
// 創(chuàng)建點(diǎn)擊方法
createClickMethod(methodName, element);
}
}
}
}
/**
* 創(chuàng)建一個(gè)點(diǎn)擊事件
*/
private void createClickMethod(String methodName, Element element) {
// 拼接方法的字符串
StringBuilder methodBuilder = new StringBuilder();
methodBuilder.append("@OnClick(" + element.getFullID() + ")\n");
methodBuilder.append("private void " + methodName + "(" + element.getName() + " " + getClickMethodName(element) + "){");
methodBuilder.append("\n}");
// 創(chuàng)建OnClick方法
mClass.add(mFactory.createMethodFromText(methodBuilder.toString(), mClass));
}
/**
* 獲取點(diǎn)擊方法的名稱
*/
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();
}
/**
* 在加載布局后根據(jù)activity Fragement View 來初始化注解框架
*/
private void generateInjects() {
PsiClass activityClass = JavaPsiFacade.getInstance(mProject).findClass(
"android.app.Activity", new EverythingGlobalScope(mProject));
PsiClass fragmentClass = JavaPsiFacade.getInstance(mProject).findClass(
"android.app.Fragment", new EverythingGlobalScope(mProject));
PsiClass supportFragmentClass = JavaPsiFacade.getInstance(mProject).findClass(
"android.support.v4.app.Fragment", new EverythingGlobalScope(mProject));
// Check for Activity class
if (activityClass != null && mClass.isInheritor(activityClass, true)) {
generateActivityBind();
// Check for Fragment class
}
// else if ((fragmentClass != null && mClass.isInheritor(fragmentClass, true)) || (supportFragmentClass != null && mClass.isInheritor(supportFragmentClass, true))) {
// generateFragmentBindAndUnbind();
// }
}
/**
* activity在加載布局后生成ViewUtils.inject(this)代碼
*/
private void generateActivityBind() {
PsiElementFactory mFactory = JavaPsiFacade.getElementFactory(mProject);
if (mClass.findMethodsByName("onCreate", false).length == 0) {
// Add an empty stub of onCreate()
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: add setContentView(...) invocation\n");
method.append(VIEW_BIND);
method.append("(this);\n");
method.append("}");
mClass.add(mFactory.createMethodFromText(method.toString(), mClass));
} else {
PsiMethod onCreate = mClass.findMethodsByName("onCreate", false)[0];
if (!containsViewInjectLine(onCreate, VIEW_BIND)) {
for (PsiStatement statement : onCreate.getBody().getStatements()) {
// Search for setContentView()
if (statement.getFirstChild() instanceof PsiMethodCallExpression) {
PsiReferenceExpression methodExpression
= ((PsiMethodCallExpression) statement.getFirstChild())
.getMethodExpression();
// Insert ButterKnife.inject()/ButterKnife.bind() after setContentView()
if (methodExpression.getText().equals("setContentView")) {
onCreate.getBody().addAfter(mFactory.createStatementFromText(
VIEW_BIND + "(this);", mClass), statement);
break;
}
}
}
}
}
}
/**
* 判斷OnCreate中是否有初始化注解框架代碼
* @param method
* @param line
* @return
*/
private boolean containsViewInjectLine(PsiMethod method, String line) {
final PsiCodeBlock body = method.getBody();
if (body == null) {
return false;
}
PsiStatement[] statements = body.getStatements();
for (PsiStatement psiStatement : statements) {
String statementAsString = psiStatement.getText();
if (psiStatement instanceof PsiExpressionStatement && (statementAsString.contains(line))) {
return true;
}
}
return false;
}
學(xué)習(xí)Android Studio插件開發(fā)需要一些時(shí)間编兄,如果時(shí)間夠可以多了解,加班比較多那么先用著這些好用的插件声登,了解大概插件開發(fā)流程就夠了,主要精力還是要在Android開發(fā)中揣苏。
插件源碼地址:https://github.com/Steadyoung/SteadyoungIOC-CodePlug
同款框架源碼地址:https://github.com/Steadyoung/SteadyoungIOC