├── .gitignore ├── resources ├── demoB.gif └── icons │ └── vg.png ├── AndroidViewGenerator.jar ├── src └── com │ └── footprint │ └── viewgenerator │ ├── iface │ ├── ICancelListener.java │ └── IConfirmListener.java │ ├── common │ ├── Definitions.java │ ├── Utils.java │ └── InjectWriter.java │ ├── form │ ├── EntryHeader.java │ ├── Entry.java │ └── EntryList.java │ ├── Settings │ ├── Settings.java │ └── Settings.form │ ├── model │ ├── Element.java │ └── VGContext.java │ └── action │ └── ViewGenerateAction.java ├── README.md └── META-INF └── plugin.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.class 3 | 4 | .idea/* -------------------------------------------------------------------------------- /resources/demoB.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianyuebits/AndroidViewGenerator/HEAD/resources/demoB.gif -------------------------------------------------------------------------------- /resources/icons/vg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianyuebits/AndroidViewGenerator/HEAD/resources/icons/vg.png -------------------------------------------------------------------------------- /AndroidViewGenerator.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianyuebits/AndroidViewGenerator/HEAD/AndroidViewGenerator.jar -------------------------------------------------------------------------------- /src/com/footprint/viewgenerator/iface/ICancelListener.java: -------------------------------------------------------------------------------- 1 | package com.footprint.viewgenerator.iface; 2 | 3 | public interface ICancelListener { 4 | 5 | public void onCancel(); 6 | } 7 | -------------------------------------------------------------------------------- /src/com/footprint/viewgenerator/iface/IConfirmListener.java: -------------------------------------------------------------------------------- 1 | package com.footprint.viewgenerator.iface; 2 | 3 | import com.footprint.viewgenerator.model.Element; 4 | import com.footprint.viewgenerator.model.VGContext; 5 | 6 | import java.util.ArrayList; 7 | 8 | public interface IConfirmListener { 9 | public void onConfirm(VGContext context, ArrayList elements, String fieldNamePrefix); 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidViewGenerator 2 | 在[ButterKnife](https://github.com/JakeWharton/butterknife)这样强大的注解库出来之后,使用注解进行UI开发已经非常普遍。但考虑到效率、学习成本等问题,findViewById方式仍然是不错的选择。 3 | 4 | 本项目针对日常开发中遇到的UI相关的繁琐操作进行自动化实现,降低开发者在这方面所需要的精力。 5 | 6 | ###主要功能 7 | 1. 支持为Activity、Fragment以及任意类(比如自定义View)从Layout文件生成View实例并初始化; 8 | 2. 支持为Adapter生成ViewHolder模板; 9 | 3. 支持为View添加监听; 10 | 4. 支持增量式修改(考虑到实例化的View会被使用,因此不支持View删除); 11 | 12 | ###演示 13 | ![AndroidViewGenerator演示](resources/demoB.gif) 14 | 加载不出来的可以看这个 [__链接__](http://7xktd8.com1.z0.glb.clouddn.com/demoB.gif)。 15 | 16 | ###安装 17 | 1. 从[这里](https://plugins.jetbrains.com/plugin/8219?pr=)下载,选择Android Studio -> Preferences -> Plugins -> Install plugin from disk... -> 选择下载的jar包 -> 点击OK,重启即可; 18 | 2. 选择Android Studio -> Preferences -> Plugins -> Browse repositories... -> 搜索 "Android View Generator",安装即可。 19 | 20 | 21 | #TODO 22 | 1. 变量、实例化增量式修改; —— Done 23 | 2. 监听、View增加增量式修改;—— Done 24 | 3. 支持ViewHolder的生成;—— Done 25 | 26 | #感谢 27 | 本项目基于 [__android-butterknife-zelezny__](https://github.com/avast/android-butterknife-zelezny) 改造。因此特别感谢:@Avast。 -------------------------------------------------------------------------------- /src/com/footprint/viewgenerator/common/Definitions.java: -------------------------------------------------------------------------------- 1 | package com.footprint.viewgenerator.common; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | 6 | public class Definitions { 7 | 8 | public static final HashMap paths = new HashMap(); 9 | public static final ArrayList adapters = new ArrayList(); 10 | public static final String ViewClickListener = "android.view.View.OnClickListener"; 11 | public static final String Activity_InitViewMethodInvoked = "initView();"; 12 | public static final String Other_InitViewMethodInvoked = "initView(rootView);"; 13 | public static final String FindViewById = "findViewById("; 14 | public static final String SetOnClickListener = ".setOnClickListener("; 15 | public static final String IMPORT = "import "; 16 | 17 | static { 18 | // special classes; default package is android.widget.* 19 | paths.put("WebView", "android.webkit.WebView"); 20 | paths.put("View", "android.view.View"); 21 | 22 | // adapters 23 | adapters.add("android.widget.ListAdapter"); 24 | adapters.add("android.widget.ArrayAdapter"); 25 | adapters.add("android.widget.BaseAdapter"); 26 | adapters.add("android.widget.HeaderViewListAdapter"); 27 | adapters.add("android.widget.SimpleAdapter"); 28 | adapters.add("android.support.v4.widget.CursorAdapter"); 29 | adapters.add("android.support.v4.widget.SimpleCursorAdapter"); 30 | adapters.add("android.support.v4.widget.ResourceCursorAdapter"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/com/footprint/viewgenerator/form/EntryHeader.java: -------------------------------------------------------------------------------- 1 | package com.footprint.viewgenerator.form; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | 6 | public class EntryHeader extends JPanel { 7 | 8 | protected JLabel mType; 9 | protected JLabel mID; 10 | protected JLabel mEvent; 11 | protected JLabel mName; 12 | 13 | public EntryHeader() { 14 | mType = new JLabel("Element"); 15 | mType.setPreferredSize(new Dimension(100, 26)); 16 | mType.setFont(new Font(mType.getFont().getFontName(), Font.BOLD, mType.getFont().getSize())); 17 | 18 | mID = new JLabel("ID"); 19 | mID.setPreferredSize(new Dimension(100, 26)); 20 | mID.setFont(new Font(mID.getFont().getFontName(), Font.BOLD, mID.getFont().getSize())); 21 | 22 | mEvent = new JLabel("OnClick"); 23 | mEvent.setPreferredSize(new Dimension(100, 26)); 24 | mEvent.setFont(new Font(mEvent.getFont().getFontName(), Font.BOLD, mEvent.getFont().getSize())); 25 | 26 | mName = new JLabel("Variable Name"); 27 | mName.setPreferredSize(new Dimension(100, 26)); 28 | mName.setFont(new Font(mName.getFont().getFontName(), Font.BOLD, mName.getFont().getSize())); 29 | 30 | setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS)); 31 | add(Box.createRigidArea(new Dimension(52, 0))); 32 | add(mType); 33 | add(Box.createRigidArea(new Dimension(12, 0))); 34 | add(mID); 35 | add(Box.createRigidArea(new Dimension(12, 0))); 36 | add(mEvent); 37 | add(Box.createRigidArea(new Dimension(22, 0))); 38 | add(mName); 39 | add(Box.createHorizontalGlue()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/com/footprint/viewgenerator/Settings/Settings.java: -------------------------------------------------------------------------------- 1 | package com.footprint.viewgenerator.Settings; 2 | 3 | import com.footprint.viewgenerator.common.Utils; 4 | import com.intellij.ide.util.PropertiesComponent; 5 | import com.intellij.openapi.options.Configurable; 6 | import com.intellij.openapi.options.ConfigurationException; 7 | import org.jetbrains.annotations.Nls; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import javax.swing.*; 11 | 12 | /** 13 | * Settings UI for the plugin. 14 | * 15 | * @author David Vávra (vavra@avast.com) 16 | */ 17 | public class Settings implements Configurable { 18 | 19 | public static final String PREFIX = "butterknifezelezny_prefix"; 20 | public static final String VIEWHOLDER_CLASS_NAME = "butterknifezelezny_viewholder_class_name"; 21 | 22 | private JPanel mPanel; 23 | private JTextField mHolderName; 24 | private JTextField mPrefix; 25 | 26 | @Nls 27 | @Override 28 | public String getDisplayName() { 29 | return "ButterKnifeZelezny"; 30 | } 31 | 32 | @Nullable 33 | @Override 34 | public String getHelpTopic() { 35 | return null; 36 | } 37 | 38 | @Nullable 39 | @Override 40 | public JComponent createComponent() { 41 | reset(); 42 | return mPanel; 43 | } 44 | 45 | @Override 46 | public boolean isModified() { 47 | return true; 48 | } 49 | 50 | @Override 51 | public void apply() throws ConfigurationException { 52 | PropertiesComponent.getInstance().setValue(PREFIX, mPrefix.getText()); 53 | PropertiesComponent.getInstance().setValue(VIEWHOLDER_CLASS_NAME, mHolderName.getText()); 54 | } 55 | 56 | @Override 57 | public void reset() { 58 | mPrefix.setText(Utils.getPrefix()); 59 | mHolderName.setText(Utils.getViewHolderClassName()); 60 | } 61 | 62 | @Override 63 | public void disposeUIResources() { 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | com.footprint.asplugin.plugin.viewgenerator 3 | Android View Generator 4 | 1.2.1 5 | BigFootprint 6 | 7 | 9 | 1)Generating fields from selected layout XMLs in activities/fragments/adapters;
10 | 2)Initializing the fields by findViewById method;
11 | 3)Generating viewholder-template for adapter;
12 | ]]>
13 | 14 | 16 | Initial Release
17 |

18 | 【1.1.1】—— 2016/02/26
19 | 修复 Bug 20 |

21 | 【1.2】—— 2016/02/27
22 | 全面支持增量式更新View 23 |

24 | 【1.2.1】—— 2016/02/27
25 | BugFix: 不添加Click的时候生成代码错误 26 | ]]> 27 |
28 | 29 | 30 | 31 | 32 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 55 | 56 | 57 | 58 | 59 |
-------------------------------------------------------------------------------- /src/com/footprint/viewgenerator/Settings/Settings.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | -------------------------------------------------------------------------------- /src/com/footprint/viewgenerator/model/Element.java: -------------------------------------------------------------------------------- 1 | package com.footprint.viewgenerator.model; 2 | 3 | 4 | import com.footprint.viewgenerator.common.Utils; 5 | 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | public class Element { 10 | 11 | // constants 12 | private static final Pattern sIdPattern = Pattern.compile("@\\+?(android:)?id/([^$]+)$", Pattern.CASE_INSENSITIVE); 13 | private static final Pattern sValidityPattern = Pattern.compile("^([a-zA-Z_\\$][\\w\\$]*)$", Pattern.CASE_INSENSITIVE); 14 | public String id; 15 | public boolean isAndroidNS = false; 16 | public String nameFull; // element name with package 17 | public String name; // element name 18 | public String fieldName; // name of variable 19 | public boolean isValid = false; 20 | public boolean needDeal = true;//是否需要自动处理 21 | public boolean isClick = true;//是否需要添加监听 22 | public String typeName = "";//类型名称(不带包名) 23 | public boolean isDeclared = false;//是否已经声明过 24 | public boolean isInit = false;//是否已经初始化 25 | 26 | public Element(String name, String id) { 27 | // id 28 | final Matcher matcher = sIdPattern.matcher(id); 29 | if (matcher.find() && matcher.groupCount() > 0) { 30 | this.id = matcher.group(2); 31 | 32 | String androidNS = matcher.group(1); 33 | this.isAndroidNS = !(androidNS == null || androidNS.length() == 0); 34 | } 35 | 36 | // name 37 | String[] packages = name.split("\\."); 38 | if (packages.length > 1) { 39 | this.nameFull = name; 40 | this.name = packages[packages.length - 1]; 41 | } else { 42 | this.nameFull = null; 43 | this.name = name; 44 | } 45 | 46 | this.fieldName = getFieldName(); 47 | } 48 | 49 | /** 50 | * Create full ID for using in layout XML files 51 | * 52 | * @return 53 | */ 54 | public String getFullID() { 55 | StringBuilder fullID = new StringBuilder(); 56 | String rPrefix; 57 | 58 | if (isAndroidNS) { 59 | rPrefix = "android.R.id."; 60 | } else { 61 | rPrefix = "R.id."; 62 | } 63 | 64 | fullID.append(rPrefix); 65 | fullID.append(id); 66 | 67 | return fullID.toString(); 68 | } 69 | 70 | /** 71 | * Generate field name if it's not done yet 72 | * 73 | * @return 74 | */ 75 | private String getFieldName() { 76 | String[] words = this.id.split("_"); 77 | StringBuilder sb = new StringBuilder(); 78 | sb.append(Utils.getPrefix()); 79 | 80 | for (int i = 0; i < words.length; i++) { 81 | String[] idTokens = words[i].split("\\."); 82 | char[] chars = idTokens[idTokens.length - 1].toCharArray(); 83 | if (i > 0 || !Utils.isEmptyString(Utils.getPrefix())) { 84 | chars[0] = Character.toUpperCase(chars[0]); 85 | } 86 | 87 | sb.append(chars); 88 | } 89 | 90 | return sb.toString(); 91 | } 92 | 93 | /** 94 | * Check validity of field name 95 | * 96 | * @return 97 | */ 98 | public boolean checkValidity() { 99 | Matcher matcher = sValidityPattern.matcher(fieldName); 100 | isValid = matcher.find(); 101 | 102 | return isValid; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/com/footprint/viewgenerator/form/Entry.java: -------------------------------------------------------------------------------- 1 | package com.footprint.viewgenerator.form; 2 | 3 | import com.footprint.viewgenerator.model.Element; 4 | import com.footprint.viewgenerator.model.VGContext; 5 | 6 | import javax.swing.*; 7 | import javax.swing.event.ChangeEvent; 8 | import javax.swing.event.ChangeListener; 9 | import java.awt.*; 10 | import java.awt.event.FocusEvent; 11 | import java.awt.event.FocusListener; 12 | 13 | public class Entry extends JPanel { 14 | 15 | protected EntryList mParent; 16 | protected Element mElement; 17 | protected VGContext mContext; 18 | 19 | // UI 20 | protected JCheckBox mCheck; 21 | protected JLabel mType; 22 | protected JLabel mID; 23 | protected JCheckBox mEvent; 24 | protected JTextField mName; 25 | protected Color mNameDefaultColor; 26 | protected Color mNameErrorColor = new Color(0x880000); 27 | 28 | public Entry(EntryList parent, Element element, VGContext context) { 29 | mElement = element; 30 | mParent = parent; 31 | mContext = context; 32 | 33 | mCheck = new JCheckBox(); 34 | mCheck.setPreferredSize(new Dimension(40, 26)); 35 | mCheck.addChangeListener(new CheckListener()); 36 | 37 | mEvent = new JCheckBox(); 38 | mEvent.setPreferredSize(new Dimension(100, 26)); 39 | 40 | mType = new JLabel(mElement.name); 41 | mType.setPreferredSize(new Dimension(100, 26)); 42 | 43 | mID = new JLabel(mElement.id); 44 | mID.setPreferredSize(new Dimension(100, 26)); 45 | 46 | mName = new JTextField(mElement.fieldName, 10); 47 | mNameDefaultColor = mName.getBackground(); 48 | mName.setPreferredSize(new Dimension(120, 26)); 49 | mName.addFocusListener(new FocusListener() { 50 | @Override 51 | public void focusGained(FocusEvent e) { 52 | // empty 53 | } 54 | 55 | @Override 56 | public void focusLost(FocusEvent e) { 57 | syncElement(); 58 | } 59 | }); 60 | 61 | mCheck.setSelected(true); 62 | if (element.isDeclared) { 63 | mCheck.setEnabled(false);//默认选中且不能取消 64 | } 65 | 66 | if (mContext.getClickIdsList().contains(mElement.getFullID())) { 67 | mEvent.setSelected(true); 68 | mEvent.setEnabled(false);//默认选中且不能取消 69 | } 70 | 71 | if (mContext.getFieldNameList().contains(mElement.fieldName)) { 72 | mName.setEditable(false);//已经声明则不能编辑 73 | } 74 | 75 | setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS)); 76 | setMaximumSize(new Dimension(Short.MAX_VALUE, 54)); 77 | add(mCheck); 78 | add(Box.createRigidArea(new Dimension(10, 0))); 79 | add(mType); 80 | add(Box.createRigidArea(new Dimension(10, 0))); 81 | add(mID); 82 | add(Box.createRigidArea(new Dimension(10, 0))); 83 | add(mEvent); 84 | add(Box.createRigidArea(new Dimension(10, 0))); 85 | add(mName); 86 | add(Box.createHorizontalGlue()); 87 | 88 | checkState(); 89 | } 90 | 91 | public Element syncElement() { 92 | mElement.needDeal = mCheck.isSelected(); 93 | mElement.isClick = mEvent.isSelected() && mElement.needDeal;//需要处理 94 | mElement.fieldName = mName.getText(); 95 | 96 | if (mElement.checkValidity()) { 97 | mName.setBackground(mNameDefaultColor); 98 | } else { 99 | mName.setBackground(mNameErrorColor); 100 | } 101 | 102 | return mElement; 103 | } 104 | 105 | private void checkState() { 106 | if (mCheck.isSelected()) { 107 | mType.setEnabled(true); 108 | mID.setEnabled(true); 109 | mName.setEnabled(true); 110 | } else { 111 | mType.setEnabled(false); 112 | mID.setEnabled(false); 113 | mName.setEnabled(false); 114 | } 115 | } 116 | 117 | public class CheckListener implements ChangeListener { 118 | @Override 119 | public void stateChanged(ChangeEvent event) { 120 | checkState(); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/com/footprint/viewgenerator/action/ViewGenerateAction.java: -------------------------------------------------------------------------------- 1 | package com.footprint.viewgenerator.action; 2 | 3 | import com.footprint.viewgenerator.common.InjectWriter; 4 | import com.footprint.viewgenerator.common.Utils; 5 | import com.footprint.viewgenerator.form.EntryList; 6 | import com.footprint.viewgenerator.iface.ICancelListener; 7 | import com.footprint.viewgenerator.iface.IConfirmListener; 8 | import com.footprint.viewgenerator.model.Element; 9 | import com.footprint.viewgenerator.model.VGContext; 10 | import com.intellij.codeInsight.CodeInsightActionHandler; 11 | import com.intellij.codeInsight.generation.actions.BaseGenerateAction; 12 | import com.intellij.openapi.actionSystem.AnActionEvent; 13 | import com.intellij.openapi.actionSystem.PlatformDataKeys; 14 | import com.intellij.openapi.editor.Editor; 15 | import com.intellij.openapi.project.Project; 16 | import com.intellij.psi.PsiClass; 17 | import com.intellij.psi.PsiFile; 18 | import com.intellij.psi.util.PsiUtilBase; 19 | 20 | import javax.swing.*; 21 | import java.util.ArrayList; 22 | 23 | /** 24 | * Created by liquanmin on 16/2/24. 25 | */ 26 | public class ViewGenerateAction extends BaseGenerateAction implements IConfirmListener, ICancelListener { 27 | protected JFrame mDialog; 28 | protected VGContext context; 29 | 30 | public ViewGenerateAction() { 31 | super(new CodeInsightActionHandler() { 32 | @Override 33 | public void invoke(Project project, Editor editor, PsiFile psiFile) { 34 | 35 | } 36 | 37 | @Override 38 | public boolean startInWriteAction() { 39 | return false; 40 | } 41 | }); 42 | } 43 | 44 | public void actionPerformed(AnActionEvent event) { 45 | Project project = event.getData(PlatformDataKeys.PROJECT); 46 | Editor editor = event.getData(PlatformDataKeys.EDITOR); 47 | 48 | actionPerformedImpl(project, editor); 49 | } 50 | 51 | @Override 52 | public void actionPerformedImpl(Project project, Editor editor) { 53 | super.actionPerformedImpl(project, editor); 54 | PsiFile file = PsiUtilBase.getPsiFileInEditor(editor, project); 55 | if (file == null) { 56 | return; 57 | } 58 | 59 | PsiFile layout = Utils.getLayoutFileFromCaret(editor, file); 60 | if (layout == null) { 61 | Utils.showErrorNotification(project, "No layout found"); 62 | return; 63 | } 64 | 65 | PsiClass clazz = getTargetClass(editor, file); 66 | if (clazz == null) { 67 | Utils.showErrorNotification(project, "No class found"); 68 | return; 69 | } 70 | 71 | ArrayList elements = Utils.getIDsFromLayout(layout); 72 | if (elements.isEmpty()) { 73 | Utils.showErrorNotification(project, "No IDs found in layout"); 74 | return; 75 | } 76 | 77 | context = new VGContext(project, file, layout, clazz); 78 | context.parseClass(); 79 | context.preDealWithElements(elements); 80 | showDialog(elements); 81 | } 82 | 83 | protected void showDialog(ArrayList elements) { 84 | EntryList panel = new EntryList(context, elements, this, this); 85 | mDialog = new JFrame(); 86 | mDialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 87 | mDialog.getRootPane().setDefaultButton(panel.getConfirmButton()); 88 | mDialog.getContentPane().add(panel); 89 | mDialog.pack(); 90 | mDialog.setLocationRelativeTo(null); 91 | mDialog.setVisible(true); 92 | } 93 | 94 | @Override 95 | public void onCancel() { 96 | closeDialog(); 97 | } 98 | 99 | @Override 100 | public void onConfirm(VGContext context, ArrayList elements, String fieldNamePrefix) { 101 | closeDialog(); 102 | Utils.dealElementList(elements); 103 | if (Utils.getInjectCount(context, elements) > 0 || Utils.getClickCount(context, elements) > 0) { // generate injections 104 | new InjectWriter(context, "Generate Injections", elements, fieldNamePrefix).execute(); 105 | } else { // just notify user about no element selected 106 | Utils.showInfoNotification(context.getProject(), "No injection was selected"); 107 | } 108 | } 109 | 110 | protected void closeDialog() { 111 | if (mDialog == null) { 112 | return; 113 | } 114 | 115 | mDialog.setVisible(false); 116 | mDialog.dispose(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/com/footprint/viewgenerator/form/EntryList.java: -------------------------------------------------------------------------------- 1 | package com.footprint.viewgenerator.form; 2 | 3 | import com.footprint.viewgenerator.iface.ICancelListener; 4 | import com.footprint.viewgenerator.iface.IConfirmListener; 5 | import com.footprint.viewgenerator.model.Element; 6 | import com.footprint.viewgenerator.model.VGContext; 7 | import com.intellij.ui.components.JBScrollPane; 8 | 9 | import javax.swing.*; 10 | import javax.swing.event.ChangeEvent; 11 | import javax.swing.event.ChangeListener; 12 | import java.awt.*; 13 | import java.awt.event.ActionEvent; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | public class EntryList extends JPanel { 18 | protected ArrayList mElements; 19 | protected List mEntries = new ArrayList(); 20 | protected VGContext mContext; 21 | protected String mPrefix = null; 22 | protected IConfirmListener mConfirmListener; 23 | protected ICancelListener mCancelListener; 24 | protected JCheckBox mHolderCheck; 25 | protected JLabel mHolderLabel; 26 | protected JButton mConfirm; 27 | protected JButton mCancel; 28 | 29 | public EntryList(VGContext context, ArrayList elements, IConfirmListener confirmListener, ICancelListener cancelListener) { 30 | mContext = context; 31 | mConfirmListener = confirmListener; 32 | mCancelListener = cancelListener; 33 | mElements = elements; 34 | 35 | setPreferredSize(new Dimension(640, 360)); 36 | setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); 37 | 38 | addInjections(); 39 | addButtons(); 40 | } 41 | 42 | protected void addInjections() { 43 | JPanel contentPanel = new JPanel(); 44 | contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.PAGE_AXIS)); 45 | contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); 46 | contentPanel.add(new EntryHeader()); 47 | contentPanel.add(Box.createRigidArea(new Dimension(0, 5))); 48 | 49 | JPanel injectionsPanel = new JPanel(); 50 | injectionsPanel.setLayout(new BoxLayout(injectionsPanel, BoxLayout.PAGE_AXIS)); 51 | injectionsPanel.add(Box.createRigidArea(new Dimension(0, 5))); 52 | 53 | int cnt = 0; 54 | mEntries.clear(); 55 | for (Element element : mElements) { 56 | Entry entry = new Entry(this, element, mContext); 57 | 58 | if (cnt > 0) { 59 | injectionsPanel.add(Box.createRigidArea(new Dimension(0, 5))); 60 | } 61 | injectionsPanel.add(entry); 62 | cnt++; 63 | 64 | mEntries.add(entry); 65 | } 66 | injectionsPanel.add(Box.createVerticalGlue()); 67 | injectionsPanel.add(Box.createRigidArea(new Dimension(0, 5))); 68 | 69 | JBScrollPane scrollPane = new JBScrollPane(injectionsPanel); 70 | contentPanel.add(scrollPane); 71 | 72 | add(contentPanel, BorderLayout.CENTER); 73 | refresh(); 74 | } 75 | 76 | protected void addButtons() { 77 | mHolderCheck = new JCheckBox(); 78 | mHolderCheck.setPreferredSize(new Dimension(32, 26)); 79 | mHolderCheck.setSelected(mContext.isAdapter()); 80 | mHolderCheck.setEnabled(mContext.isAdapter()); 81 | mHolderCheck.addChangeListener(new CheckHolderListener()); 82 | 83 | mHolderLabel = new JLabel(); 84 | mHolderLabel.setText("Create ViewHolder"); 85 | 86 | JPanel holderPanel = new JPanel(); 87 | holderPanel.setLayout(new BoxLayout(holderPanel, BoxLayout.LINE_AXIS)); 88 | holderPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10)); 89 | holderPanel.add(mHolderCheck); 90 | holderPanel.add(mHolderLabel); 91 | holderPanel.add(Box.createHorizontalGlue()); 92 | add(holderPanel, BorderLayout.PAGE_END); 93 | 94 | mCancel = new JButton(); 95 | mCancel.setAction(new CancelAction()); 96 | mCancel.setPreferredSize(new Dimension(120, 26)); 97 | mCancel.setText("Cancel"); 98 | mCancel.setVisible(true); 99 | 100 | mConfirm = new JButton(); 101 | mConfirm.setAction(new ConfirmAction()); 102 | mConfirm.setPreferredSize(new Dimension(120, 26)); 103 | mConfirm.setText("Confirm"); 104 | mConfirm.setVisible(true); 105 | 106 | JPanel buttonPanel = new JPanel(); 107 | buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.LINE_AXIS)); 108 | buttonPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); 109 | buttonPanel.add(Box.createHorizontalGlue()); 110 | buttonPanel.add(mCancel); 111 | buttonPanel.add(Box.createRigidArea(new Dimension(10, 0))); 112 | buttonPanel.add(mConfirm); 113 | 114 | add(buttonPanel, BorderLayout.PAGE_END); 115 | refresh(); 116 | } 117 | 118 | protected void refresh() { 119 | revalidate(); 120 | 121 | if (mConfirm != null) { 122 | mConfirm.setVisible(mElements.size() > 0); 123 | } 124 | } 125 | 126 | protected boolean checkValidity() { 127 | boolean valid = true; 128 | 129 | for (Element element : mElements) { 130 | if (!element.checkValidity()) { 131 | valid = false; 132 | } 133 | } 134 | 135 | return valid; 136 | } 137 | 138 | public JButton getConfirmButton() { 139 | return mConfirm; 140 | } 141 | 142 | 143 | public class CheckHolderListener implements ChangeListener { 144 | @Override 145 | public void stateChanged(ChangeEvent event) { 146 | mContext.setIfCreateViewHolder(mHolderCheck.isSelected()); 147 | } 148 | } 149 | 150 | protected class ConfirmAction extends AbstractAction { 151 | public void actionPerformed(ActionEvent event) { 152 | boolean valid = checkValidity(); 153 | 154 | for (Entry entry : mEntries) { 155 | entry.syncElement(); 156 | } 157 | 158 | if (valid) { 159 | if (mConfirmListener != null) { 160 | mConfirmListener.onConfirm(mContext, mElements, mPrefix); 161 | } 162 | } 163 | } 164 | } 165 | 166 | protected class CancelAction extends AbstractAction { 167 | public void actionPerformed(ActionEvent event) { 168 | if (mCancelListener != null) { 169 | mCancelListener.onCancel(); 170 | } 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/com/footprint/viewgenerator/model/VGContext.java: -------------------------------------------------------------------------------- 1 | package com.footprint.viewgenerator.model; 2 | 3 | import com.footprint.viewgenerator.common.Definitions; 4 | import com.footprint.viewgenerator.common.Utils; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.psi.*; 7 | import com.intellij.psi.search.EverythingGlobalScope; 8 | 9 | import java.util.ArrayList; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | 13 | /** 14 | * Created by liquanmin on 16/2/27. 15 | */ 16 | public class VGContext { 17 | //ID->Element: 从Layout中获取 18 | private HashMap elementsIdMap = new HashMap(); 19 | 20 | //ID->Name: 从initView中获取 21 | private HashMap elementsIdNameMap = new HashMap(); 22 | 23 | //已经声明的属性变量名字 24 | private List fieldNameList = new ArrayList(8); 25 | 26 | //已经在onClick方法中有Id的变量,等效于添加了监听的变量 27 | private List clickIdsList = new ArrayList(8); 28 | 29 | //所有添加监听的View属性名字到Statement的映射 30 | private List clickViewNameList = new ArrayList(8); 31 | 32 | //所有import的包.类名 33 | protected List importStrList = new ArrayList(); 34 | 35 | protected PsiImportList importList; 36 | 37 | //是否已经添加OnClickListener监听 38 | protected boolean isClickClass = false; 39 | 40 | //是否需要创建ViewHolder 41 | protected boolean ifCreateViewHolder = false; 42 | 43 | //是否是Adapter类 44 | private boolean isAdapter = false; 45 | 46 | private PsiClass activityClass; 47 | private PsiClass fragmentClass; 48 | private PsiClass supportFragmentClass; 49 | 50 | private PsiClass mClass; 51 | private PsiFile mFile; 52 | private PsiFile mLayoutFile; 53 | private Project mProject; 54 | 55 | public VGContext(Project mProject, PsiFile psiFile, PsiFile layoutFile, PsiClass mClass) { 56 | this.mClass = mClass; 57 | this.mProject = mProject; 58 | this.mFile = psiFile; 59 | this.mLayoutFile = layoutFile; 60 | } 61 | 62 | /** 63 | * 预处理Element列表 64 | */ 65 | public void preDealWithElements(List mElements) { 66 | //解析实例化变量 67 | PsiMethod[] initViews = mClass.findMethodsByName("initView", false); 68 | elementsIdNameMap.clear(); 69 | clickViewNameList.clear(); 70 | if (initViews.length > 0) { 71 | PsiMethod method = initViews[0]; 72 | final PsiCodeBlock body = method.getBody(); 73 | if (body != null) { 74 | PsiStatement[] statements = body.getStatements(); 75 | for (PsiStatement psiStatement : statements) { 76 | String statementAsString = psiStatement.getText(); 77 | if (statementAsString.contains(Definitions.FindViewById)) {//声明语句 78 | statementAsString = Utils.replaceBlank(statementAsString); 79 | String fieldName = statementAsString.substring(0, statementAsString.indexOf("=")); 80 | String id = statementAsString.substring(statementAsString.indexOf("(R.id.") + 1, statementAsString.indexOf(");")); 81 | elementsIdNameMap.put(id, fieldName); 82 | } 83 | 84 | if (statementAsString.contains(Definitions.SetOnClickListener)) {//监听语句 85 | statementAsString = Utils.replaceBlank(statementAsString); 86 | String fieldName = statementAsString.substring(0, statementAsString.indexOf(Definitions.SetOnClickListener)); 87 | clickViewNameList.add(fieldName); 88 | } 89 | } 90 | } 91 | } 92 | 93 | //解析click变量 94 | PsiMethod[] onClicks = mClass.findMethodsByName("onClick", false); 95 | clickIdsList.clear(); 96 | if (onClicks.length > 0) { 97 | PsiMethod method = onClicks[0]; 98 | final PsiCodeBlock body = method.getBody(); 99 | if (body != null) { 100 | PsiStatement[] statements = body.getStatements(); 101 | for (PsiStatement psiStatement : statements) { 102 | if (psiStatement instanceof PsiIfStatement) { 103 | String ifText = Utils.replaceBlank(psiStatement.getText()); 104 | StringBuilder stringBuilder = new StringBuilder(); 105 | for (Element element : mElements) { 106 | Utils.clearStringBuilder(stringBuilder); 107 | stringBuilder.append("if(view.getId()==") 108 | .append(element.getFullID()) 109 | .append(")"); 110 | 111 | if (ifText.contains(stringBuilder.toString())) { 112 | clickIdsList.add(element.getFullID()); 113 | element.isClick = true; 114 | } 115 | } 116 | break; 117 | } 118 | } 119 | } 120 | } 121 | 122 | //获取声明的所有变量名字 123 | fieldNameList.clear(); 124 | for (PsiField psiField : mClass.getAllFields()) { 125 | fieldNameList.add(psiField.getName()); 126 | } 127 | 128 | elementsIdMap.clear(); 129 | for (Element element : mElements) { 130 | elementsIdMap.put(element.getFullID(), element); 131 | //ID映射到的变量名称换成代码中的 132 | if (elementsIdNameMap.containsKey(element.getFullID())) { 133 | element.fieldName = elementsIdNameMap.get(element.getFullID());//这个名字可能被修改过,以修改过的为标准 134 | element.isInit = true; 135 | } 136 | 137 | //有同样名字的变量就认为是声明过的 138 | if (fieldNameList.contains(element.fieldName)) { 139 | element.isDeclared = true; 140 | } 141 | } 142 | } 143 | 144 | public void parseClass() { 145 | PsiReferenceList extendsList = mClass.getExtendsList(); 146 | if (extendsList != null) { 147 | for (PsiJavaCodeReferenceElement element : extendsList.getReferenceElements()) { 148 | if (Definitions.adapters.contains(element.getQualifiedName())) { 149 | isAdapter = true; 150 | ifCreateViewHolder = true; 151 | break; 152 | } 153 | } 154 | } 155 | 156 | PsiReferenceList implementsList = mClass.getImplementsList(); 157 | if (implementsList != null) { 158 | for (PsiJavaCodeReferenceElement element : implementsList.getReferenceElements()) { 159 | if (Definitions.ViewClickListener.equals(element.getQualifiedName())) { 160 | isClickClass = true; 161 | break; 162 | } 163 | } 164 | } 165 | 166 | importList = ((PsiJavaFile) mClass.getContainingFile()).getImportList(); 167 | PsiImportStatement[] importStatements = importList.getImportStatements(); 168 | importStrList.clear(); 169 | StringBuilder stringBuilder = new StringBuilder(); 170 | for (PsiImportStatement statement : importStatements) { 171 | stringBuilder.delete(0, stringBuilder.length()); 172 | stringBuilder.append(statement.getText()); 173 | stringBuilder.delete(0, Definitions.IMPORT.length()); 174 | stringBuilder.delete(stringBuilder.length() - 1, stringBuilder.length()); 175 | importStrList.add(stringBuilder.toString()); 176 | } 177 | 178 | activityClass = JavaPsiFacade.getInstance(mProject).findClass( 179 | "android.app.Activity", new EverythingGlobalScope(mProject)); 180 | fragmentClass = JavaPsiFacade.getInstance(mProject).findClass( 181 | "android.app.Fragment", new EverythingGlobalScope(mProject)); 182 | supportFragmentClass = JavaPsiFacade.getInstance(mProject).findClass( 183 | "android.support.v4.app.Fragment", new EverythingGlobalScope(mProject)); 184 | } 185 | 186 | public HashMap getElementsIdMap() { 187 | return elementsIdMap; 188 | } 189 | 190 | public HashMap getElementsIdNameMap() { 191 | return elementsIdNameMap; 192 | } 193 | 194 | public List getFieldNameList() { 195 | return fieldNameList; 196 | } 197 | 198 | public List getClickIdsList() { 199 | return clickIdsList; 200 | } 201 | 202 | public boolean isAdapter() { 203 | return isAdapter; 204 | } 205 | 206 | public boolean ifCreateViewHolder() { 207 | return ifCreateViewHolder; 208 | } 209 | 210 | public void setIfCreateViewHolder(boolean ifCreateViewHolder) { 211 | this.ifCreateViewHolder = ifCreateViewHolder; 212 | } 213 | 214 | public List getImportStrList() { 215 | return importStrList; 216 | } 217 | 218 | public PsiImportList getImportList() { 219 | return importList; 220 | } 221 | 222 | public List getClickViewNameList() { 223 | return clickViewNameList; 224 | } 225 | 226 | public PsiFile getmFile() { 227 | return mFile; 228 | } 229 | 230 | public PsiFile getmLayoutFile() { 231 | return mLayoutFile; 232 | } 233 | 234 | public boolean isClickClass() { 235 | return isClickClass; 236 | } 237 | 238 | public PsiClass getmClass() { 239 | return mClass; 240 | } 241 | 242 | public Project getProject() { 243 | return mProject; 244 | } 245 | 246 | public boolean isActivity() { 247 | return activityClass != null && mClass.isInheritor(activityClass, true); 248 | } 249 | 250 | public boolean isFragment() { 251 | return (fragmentClass != null && mClass.isInheritor(fragmentClass, true)) || (supportFragmentClass != null && mClass.isInheritor(supportFragmentClass, true)); 252 | } 253 | 254 | public boolean containsMethod(String methodName) { 255 | return Utils.ifClassContainsMethod(mClass, methodName); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/com/footprint/viewgenerator/common/Utils.java: -------------------------------------------------------------------------------- 1 | package com.footprint.viewgenerator.common; 2 | 3 | import com.footprint.viewgenerator.Settings.Settings; 4 | import com.footprint.viewgenerator.model.Element; 5 | import com.footprint.viewgenerator.model.VGContext; 6 | import com.intellij.ide.util.PropertiesComponent; 7 | import com.intellij.openapi.editor.Editor; 8 | import com.intellij.openapi.module.Module; 9 | import com.intellij.openapi.module.ModuleUtil; 10 | import com.intellij.openapi.project.Project; 11 | import com.intellij.openapi.projectRoots.ProjectJdkTable; 12 | import com.intellij.openapi.projectRoots.Sdk; 13 | import com.intellij.openapi.ui.MessageType; 14 | import com.intellij.openapi.ui.popup.Balloon; 15 | import com.intellij.openapi.ui.popup.JBPopupFactory; 16 | import com.intellij.openapi.wm.StatusBar; 17 | import com.intellij.openapi.wm.WindowManager; 18 | import com.intellij.psi.*; 19 | import com.intellij.psi.codeStyle.CodeStyleSettings; 20 | import com.intellij.psi.codeStyle.CodeStyleSettingsManager; 21 | import com.intellij.psi.search.EverythingGlobalScope; 22 | import com.intellij.psi.search.FilenameIndex; 23 | import com.intellij.psi.search.GlobalSearchScope; 24 | import com.intellij.psi.xml.XmlAttribute; 25 | import com.intellij.psi.xml.XmlTag; 26 | import com.intellij.ui.awt.RelativePoint; 27 | import org.jetbrains.annotations.NotNull; 28 | 29 | import java.util.ArrayList; 30 | import java.util.Iterator; 31 | import java.util.regex.Matcher; 32 | import java.util.regex.Pattern; 33 | 34 | public class Utils { 35 | 36 | /** 37 | * Is using Android SDK? 38 | */ 39 | public static Sdk findAndroidSDK() { 40 | Sdk[] allJDKs = ProjectJdkTable.getInstance().getAllJdks(); 41 | for (Sdk sdk : allJDKs) { 42 | if (sdk.getSdkType().getName().toLowerCase().contains("android")) { 43 | return sdk; 44 | } 45 | } 46 | 47 | return null; // no Android SDK found 48 | } 49 | 50 | /** 51 | * Try to find layout XML file in current source on cursor's position 52 | * 53 | * @param editor 54 | * @param file 55 | * @return 56 | */ 57 | public static PsiFile getLayoutFileFromCaret(Editor editor, PsiFile file) { 58 | int offset = editor.getCaretModel().getOffset(); 59 | 60 | PsiElement candidateA = file.findElementAt(offset); 61 | PsiElement candidateB = file.findElementAt(offset - 1); 62 | 63 | PsiFile layout = findLayoutResource(candidateA); 64 | if (layout != null) { 65 | return layout; 66 | } 67 | 68 | return findLayoutResource(candidateB); 69 | } 70 | 71 | /** 72 | * Try to find layout XML file in selected element 73 | * 74 | * @param element 75 | * @return 76 | */ 77 | public static PsiFile findLayoutResource(PsiElement element) { 78 | if (element == null) { 79 | return null; // nothing to be used 80 | } 81 | if (!(element instanceof PsiIdentifier)) { 82 | return null; // nothing to be used 83 | } 84 | 85 | PsiElement layout = element.getParent().getFirstChild(); 86 | if (layout == null) { 87 | return null; // no file to process 88 | } 89 | if (!"R.layout".equals(layout.getText())) { 90 | return null; // not layout file 91 | } 92 | 93 | Project project = element.getProject(); 94 | String name = String.format("%s.xml", element.getText()); 95 | return resolveLayoutResourceFile(element, project, name); 96 | 97 | 98 | } 99 | 100 | private static PsiFile resolveLayoutResourceFile(PsiElement element, Project project, String name) { 101 | // restricting the search to the current module - searching the whole project could return wrong layouts 102 | Module module = ModuleUtil.findModuleForPsiElement(element); 103 | PsiFile[] files = null; 104 | if (module != null) { 105 | GlobalSearchScope moduleScope = module.getModuleWithDependenciesAndLibrariesScope(false); 106 | files = FilenameIndex.getFilesByName(project, name, moduleScope); 107 | } 108 | if (files == null || files.length <= 0) { 109 | // fallback to search through the whole project 110 | // useful when the project is not properly configured - when the resource directory is not configured 111 | files = FilenameIndex.getFilesByName(project, name, new EverythingGlobalScope(project)); 112 | if (files.length <= 0) { 113 | return null; //no matching files 114 | } 115 | } 116 | 117 | // TODO - we have a problem here - we still can have multiple layouts (some coming from a dependency) 118 | // we need to resolve R class properly and find the proper layout for the R class 119 | return files[0]; 120 | } 121 | 122 | /** 123 | * Try to find layout XML file by name 124 | * 125 | * @param file 126 | * @param project 127 | * @param fileName 128 | * @return 129 | */ 130 | public static PsiFile findLayoutResource(PsiFile file, Project project, String fileName) { 131 | String name = String.format("%s.xml", fileName); 132 | // restricting the search to the module of layout that includes the layout we are seaching for 133 | return resolveLayoutResourceFile(file, project, name); 134 | } 135 | 136 | /** 137 | * Obtain all IDs from layout 138 | * 139 | * @param file 140 | * @return 141 | */ 142 | public static ArrayList getIDsFromLayout(final PsiFile file) { 143 | final ArrayList elements = new ArrayList(); 144 | 145 | return getIDsFromLayout(file, elements); 146 | } 147 | 148 | /** 149 | * Obtain all IDs from layout 150 | * 151 | * @param file 152 | * @return 153 | */ 154 | public static ArrayList getIDsFromLayout(final PsiFile file, final ArrayList elements) { 155 | file.accept(new XmlRecursiveElementVisitor() { 156 | 157 | @Override 158 | public void visitElement(final PsiElement element) { 159 | super.visitElement(element); 160 | 161 | if (element instanceof XmlTag) { 162 | XmlTag tag = (XmlTag) element; 163 | 164 | if (tag.getName().equalsIgnoreCase("include")) { 165 | XmlAttribute layout = tag.getAttribute("layout", null); 166 | 167 | if (layout != null) { 168 | Project project = file.getProject(); 169 | PsiFile include = findLayoutResource(file, project, getLayoutName(layout.getValue())); 170 | 171 | if (include != null) { 172 | getIDsFromLayout(include, elements); 173 | 174 | return; 175 | } 176 | } 177 | } 178 | 179 | // get element ID 180 | XmlAttribute id = tag.getAttribute("android:id", null); 181 | if (id == null) { 182 | return; // missing android:id attribute 183 | } 184 | String value = id.getValue(); 185 | if (value == null) { 186 | return; // empty value 187 | } 188 | 189 | // check if there is defined custom class 190 | String name = tag.getName(); 191 | XmlAttribute clazz = tag.getAttribute("class", null); 192 | if (clazz != null) { 193 | name = clazz.getValue(); 194 | } 195 | 196 | try { 197 | elements.add(new Element(name, value)); 198 | } catch (IllegalArgumentException e) { 199 | // TODO log 200 | } 201 | } 202 | } 203 | }); 204 | 205 | return elements; 206 | } 207 | 208 | /** 209 | * Get layout name from XML identifier (@layout/....) 210 | * 211 | * @param layout 212 | * @return 213 | */ 214 | public static String getLayoutName(String layout) { 215 | if (layout == null || !layout.startsWith("@") || !layout.contains("/")) { 216 | return null; // it's not layout identifier 217 | } 218 | 219 | String[] parts = layout.split("/"); 220 | if (parts.length != 2) { 221 | return null; // not enough parts 222 | } 223 | 224 | return parts[1]; 225 | } 226 | 227 | /** 228 | * Display simple notification - information 229 | * 230 | * @param project 231 | * @param text 232 | */ 233 | public static void showInfoNotification(Project project, String text) { 234 | showNotification(project, MessageType.INFO, text); 235 | } 236 | 237 | /** 238 | * Display simple notification - error 239 | * 240 | * @param project 241 | * @param text 242 | */ 243 | public static void showErrorNotification(Project project, String text) { 244 | showNotification(project, MessageType.ERROR, text); 245 | } 246 | 247 | /** 248 | * Display simple notification of given type 249 | * 250 | * @param project 251 | * @param type 252 | * @param text 253 | */ 254 | public static void showNotification(Project project, MessageType type, String text) { 255 | StatusBar statusBar = WindowManager.getInstance().getStatusBar(project); 256 | 257 | JBPopupFactory.getInstance() 258 | .createHtmlTextBalloonBuilder(text, type, null) 259 | .setFadeoutTime(7500) 260 | .createBalloon() 261 | .show(RelativePoint.getCenterOf(statusBar.getComponent()), Balloon.Position.atRight); 262 | } 263 | 264 | /** 265 | * Load field name prefix from code style 266 | * 267 | * @return 268 | */ 269 | public static String getPrefix() { 270 | if (PropertiesComponent.getInstance().isValueSet(Settings.PREFIX)) { 271 | return PropertiesComponent.getInstance().getValue(Settings.PREFIX); 272 | } else { 273 | CodeStyleSettingsManager manager = CodeStyleSettingsManager.getInstance(); 274 | CodeStyleSettings settings = manager.getCurrentSettings(); 275 | return settings.FIELD_NAME_PREFIX; 276 | } 277 | } 278 | 279 | public static String getViewHolderClassName() { 280 | return PropertiesComponent.getInstance().getValue(Settings.VIEWHOLDER_CLASS_NAME, "ViewHolder"); 281 | } 282 | 283 | /** 284 | * 删除不需要处理的Element 285 | */ 286 | public static void dealElementList(ArrayList elements) { 287 | Iterator iterator = elements.iterator(); 288 | while (iterator.hasNext()) { 289 | Element element = iterator.next(); 290 | if (!element.needDeal) 291 | iterator.remove(); 292 | } 293 | } 294 | 295 | public static int getInjectCount(VGContext context, ArrayList elements) { 296 | int cnt = 0; 297 | for (Element element : elements) { 298 | if (!context.getFieldNameList().contains(element.fieldName)) { 299 | cnt++; 300 | } 301 | } 302 | return cnt; 303 | } 304 | 305 | public static int getClickCount(VGContext context, ArrayList elements) { 306 | int cnt = 0; 307 | for (Element element : elements) { 308 | if (!context.getClickIdsList().contains(element.getFullID()) 309 | && element.isClick) { 310 | cnt++; 311 | } 312 | } 313 | return cnt; 314 | } 315 | 316 | /** 317 | * Easier way to check if string is empty 318 | * 319 | * @param text 320 | * @return 321 | */ 322 | public static boolean isEmptyString(String text) { 323 | return (text == null || text.trim().length() == 0); 324 | } 325 | 326 | /** 327 | * Check whether classpath of a module that corresponds to a {@link PsiElement} contains given class. 328 | * 329 | * @param project Project 330 | * @param psiElement Element for which we check the class 331 | * @param className Class name of the searched class 332 | * @return True if the class is present on the classpath 333 | * @since 1.3 334 | */ 335 | public static boolean isClassAvailableForPsiFile(@NotNull Project project, @NotNull PsiElement psiElement, @NotNull String className) { 336 | Module module = ModuleUtil.findModuleForPsiElement(psiElement); 337 | if (module == null) { 338 | return false; 339 | } 340 | GlobalSearchScope moduleScope = module.getModuleWithDependenciesAndLibrariesScope(false); 341 | PsiClass classInModule = JavaPsiFacade.getInstance(project).findClass(className, moduleScope); 342 | return classInModule != null; 343 | } 344 | 345 | /** 346 | * Check whether classpath of a the whole project contains given class. 347 | * This is only fallback for wrongly setup projects. 348 | * 349 | * @param project Project 350 | * @param className Class name of the searched class 351 | * @return True if the class is present on the classpath 352 | * @since 1.3.1 353 | */ 354 | public static boolean isClassAvailableForProject(@NotNull Project project, @NotNull String className) { 355 | PsiClass classInModule = JavaPsiFacade.getInstance(project).findClass(className, 356 | new EverythingGlobalScope(project)); 357 | return classInModule != null; 358 | } 359 | 360 | /** 361 | * 去除空格换行等字符 362 | */ 363 | public static String replaceBlank(String str) { 364 | String dest = ""; 365 | if (str != null) { 366 | Pattern p = Pattern.compile("\\s*|\t|\r|\n"); 367 | Matcher m = p.matcher(str); 368 | dest = m.replaceAll(""); 369 | } 370 | return dest; 371 | } 372 | 373 | public static boolean isLayoutStatement(PsiStatement statement) { 374 | return statement instanceof PsiExpressionStatement && statement.getText().contains("R.layout."); 375 | } 376 | 377 | public static String getIdFromLayoutStatement(String layoutStatement) { 378 | StringBuilder stringBuilder = new StringBuilder(layoutStatement); 379 | stringBuilder.delete(0, stringBuilder.indexOf("R.layout.")); 380 | 381 | if (stringBuilder.indexOf(")") > 0) { 382 | stringBuilder.delete(stringBuilder.indexOf(")"), stringBuilder.length()); 383 | } 384 | 385 | if (stringBuilder.indexOf(";") > 0) { 386 | stringBuilder.delete(stringBuilder.indexOf(";"), stringBuilder.length()); 387 | } 388 | 389 | return replaceBlank(stringBuilder.toString()); 390 | } 391 | 392 | public static void clearStringBuilder(StringBuilder builder) { 393 | if (builder == null) 394 | return; 395 | 396 | builder.delete(0, builder.length()); 397 | } 398 | 399 | public static boolean ifClassContainsMethod(PsiClass psiClass, String methodName) { 400 | return psiClass.findMethodsByName(methodName, false).length != 0; 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /src/com/footprint/viewgenerator/common/InjectWriter.java: -------------------------------------------------------------------------------- 1 | package com.footprint.viewgenerator.common; 2 | 3 | import com.footprint.viewgenerator.model.Element; 4 | import com.footprint.viewgenerator.model.VGContext; 5 | import com.intellij.codeInsight.actions.ReformatCodeProcessor; 6 | import com.intellij.openapi.command.WriteCommandAction; 7 | import com.intellij.psi.*; 8 | import com.intellij.psi.codeStyle.JavaCodeStyleManager; 9 | import com.intellij.psi.search.GlobalSearchScope; 10 | 11 | import java.util.ArrayList; 12 | 13 | public class InjectWriter extends WriteCommandAction.Simple { 14 | protected ArrayList mElements; 15 | protected PsiElementFactory mFactory; 16 | protected String mFieldNamePrefix; 17 | 18 | protected VGContext mContext; 19 | 20 | public InjectWriter(VGContext context, String command, ArrayList elements, String fieldNamePrefix) { 21 | super(context.getProject(), command); 22 | 23 | mElements = elements; 24 | mFactory = JavaPsiFacade.getElementFactory(context.getProject()); 25 | mFieldNamePrefix = fieldNamePrefix; 26 | mContext = context; 27 | } 28 | 29 | @Override 30 | public void run() throws Throwable { 31 | if (mContext.ifCreateViewHolder()) { 32 | generateAdapter(); 33 | } else { 34 | if (Utils.getInjectCount(mContext, mElements) > 0) { 35 | generateFields(); 36 | } 37 | 38 | if (Utils.getClickCount(mContext, mElements) > 0) { 39 | generateClick(); 40 | } 41 | 42 | generateInitMethods(getPsiClass()); 43 | Utils.showInfoNotification(mContext.getProject(), "Generation Done"); 44 | } 45 | 46 | // reformat class 47 | JavaCodeStyleManager styleManager = JavaCodeStyleManager.getInstance(mContext.getProject()); 48 | styleManager.optimizeImports(mContext.getmFile()); 49 | styleManager.shortenClassReferences(getPsiClass()); 50 | new ReformatCodeProcessor(mContext.getProject(), getPsiClass().getContainingFile(), null, false).runWithoutProgress(); 51 | } 52 | 53 | protected void generateInitMethods(PsiClass parentClass) { 54 | if (!Utils.ifClassContainsMethod(parentClass, "initView")) {//不存在该方法 55 | // 添加initView()方法 56 | StringBuilder method = new StringBuilder(); 57 | if (mContext.isActivity()) { 58 | method.append("private void initView() {\n"); 59 | } else { 60 | method.append("private void initView(View rootView) {\n"); 61 | } 62 | for (Element element : mElements) { 63 | method.append(generateFindViewByIdText(element)).append("\n"); 64 | if (element.isClick && !mContext.ifCreateViewHolder()) {//添加监听 65 | method.append(element.fieldName + ".setOnClickListener(" + getPsiClass().getName() + ".this);"); 66 | } 67 | } 68 | method.append("}"); 69 | parentClass.add(mFactory.createMethodFromText(method.toString(), getPsiClass())); 70 | 71 | addInitViewMethodInvoked(); 72 | } else {//已经有该方法,只需要在init后面插入即可 73 | PsiMethod initView = parentClass.findMethodsByName("initView", false)[0]; 74 | PsiCodeBlock initViewBody = initView.getBody(); 75 | for (Element element : mElements) { 76 | if (element.isInit) {//已经初始化了 77 | if (element.isClick && !mContext.getClickViewNameList().contains(element.fieldName)) { 78 | //重新添加的Click事件,遍历Body 79 | for (PsiStatement psiStatement : initViewBody.getStatements()) { 80 | if (Utils.replaceBlank(psiStatement.getText()) 81 | .contains(element.fieldName + "=(")) { 82 | initViewBody.addAfter(mFactory.createStatementFromText(element.fieldName + ".setOnClickListener(" + getPsiClass().getName() + ".this);", getPsiClass()), psiStatement); 83 | break; 84 | } 85 | } 86 | } 87 | } else { 88 | initViewBody.addBefore(mFactory.createStatementFromText(generateFindViewByIdText(element), getPsiClass()), initViewBody.getLastBodyElement()); 89 | if (element.isClick) { 90 | initViewBody.addBefore(mFactory.createStatementFromText(element.fieldName + ".setOnClickListener(" + getPsiClass().getName() + ".this);", getPsiClass()), initViewBody.getLastBodyElement()); 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | private PsiClass getPsiClass() { 98 | return mContext.getmClass(); 99 | } 100 | 101 | protected void generateClick() { 102 | StringBuilder method = new StringBuilder(); 103 | addViewClickListenerInterface(); 104 | 105 | //没有onClick方法 106 | if (getPsiClass().findMethodsByName("onClick", false).length == 0) { 107 | method.append("@Override \n"); 108 | method.append("public void onClick(android.view.View view) {\n"); 109 | boolean isFirst = true; 110 | for (Element element : mElements) { 111 | if (element.isClick) { 112 | if (isFirst) { 113 | method.append("if(view.getId() == " + element.getFullID() + "){\n\n"); 114 | isFirst = false; 115 | } else { 116 | method.append("}else if(view.getId() == " + element.getFullID() + "){\n\n"); 117 | } 118 | } 119 | } 120 | method.append("}}"); 121 | getPsiClass().add(mFactory.createMethodFromText(method.toString(), getPsiClass())); 122 | } else { 123 | PsiMethod onClick = getPsiClass().findMethodsByName("onClick", false)[0]; 124 | PsiIfStatement psiIfStatement = null; 125 | for (PsiStatement statement : onClick.getBody().getStatements()) { 126 | if (statement instanceof PsiIfStatement) { 127 | psiIfStatement = (PsiIfStatement) statement; 128 | break; 129 | } 130 | } 131 | 132 | boolean isFirst = true; 133 | for (Element element : mElements) { 134 | if (element.isClick && !mContext.getClickIdsList().contains(element.getFullID())) { 135 | if (isFirst) { 136 | method.append("if(view.getId() == " + element.getFullID() + "){\n\n"); 137 | isFirst = false; 138 | } else { 139 | method.append("}else if(view.getId() == " + element.getFullID() + "){\n\n"); 140 | } 141 | } 142 | } 143 | 144 | if (method.length() > 0) 145 | method.append("}"); 146 | 147 | if (psiIfStatement == null) {//有onClick方法,但是没有if语句 148 | onClick.getBody().addAfter(mFactory.createStatementFromText(method.toString(), getPsiClass()), onClick.getBody().getFirstBodyElement()); 149 | } else { 150 | method.insert(0, psiIfStatement.getText() + " else "); 151 | psiIfStatement.replace(mFactory.createStatementFromText(method.toString(), getPsiClass())); 152 | } 153 | } 154 | } 155 | 156 | /** 157 | * Create ViewHolder for adapters with injections 158 | */ 159 | protected void generateAdapter() { 160 | PsiClass holderClass = getPsiClass().findInnerClassByName(Utils.getViewHolderClassName(), true); 161 | if (holderClass != null) { 162 | holderClass.delete(); 163 | } 164 | 165 | // view holder class 166 | StringBuilder holderBuilder = new StringBuilder(); 167 | holderBuilder.append(Utils.getViewHolderClassName()); 168 | holderBuilder.append("(android.view.View rootView) {"); 169 | holderBuilder.append(Definitions.Other_InitViewMethodInvoked); 170 | holderBuilder.append("}"); 171 | 172 | PsiClass viewHolder = mFactory.createClassFromText(holderBuilder.toString(), getPsiClass()); 173 | viewHolder.setName(Utils.getViewHolderClassName()); 174 | 175 | // add injections into main class 176 | StringBuilder injection = new StringBuilder(); 177 | for (Element element : mElements) { 178 | if (mContext.getFieldNameList().contains(element.fieldName)) {//没有勾选,或者同样名字的变量已经声明过了 179 | continue; 180 | } 181 | 182 | injection.delete(0, injection.length()); 183 | injection.append("protected "); 184 | injection.append(getFieldTypeName(element)); 185 | injection.append(" "); 186 | injection.append(element.fieldName); 187 | injection.append(";"); 188 | 189 | viewHolder.add(mFactory.createFieldFromText(injection.toString(), getPsiClass())); 190 | } 191 | 192 | generateInitMethods(viewHolder); 193 | getPsiClass().add(viewHolder); 194 | //添加static 195 | getPsiClass().addBefore(mFactory.createKeyword("static", getPsiClass()), getPsiClass().findInnerClassByName(Utils.getViewHolderClassName(), true)); 196 | 197 | processAdapterGetViewMethod(); 198 | } 199 | 200 | private void processAdapterGetViewMethod() { 201 | PsiMethod getView = getPsiClass().findMethodsByName("getView", false)[0]; 202 | //已经生成过了 203 | if (Utils.replaceBlank(getView.getBody().getText()).contains("ViewHolderviewHolder=null;")) { 204 | return; 205 | } 206 | 207 | String layoutStatement = null; 208 | for (PsiStatement statement : getView.getBody().getStatements()) { 209 | if (Utils.isLayoutStatement(statement)) { 210 | layoutStatement = statement.getText(); 211 | statement.replace(mFactory.createStatementFromText("View view = convertView;", getPsiClass())); 212 | } 213 | if (statement instanceof PsiReturnStatement) { 214 | //设置语句 215 | getView.getBody().addBefore(mFactory.createStatementFromText("ViewHolder viewHolder = null;", getPsiClass()), statement); 216 | getView.getBody().addBefore(mFactory.createStatementFromText(getViewHolderCreateStr(layoutStatement), getPsiClass()), statement); 217 | statement.replace(mFactory.createStatementFromText("return view;", getPsiClass())); 218 | } 219 | } 220 | } 221 | 222 | private String getViewHolderCreateStr(String layoutStatement) { 223 | return "if(view == null || !(view.getTag() instanceof ViewHolder)){view = LayoutInflater.from(parent.getContext()).inflate(" + 224 | Utils.getIdFromLayoutStatement(layoutStatement) + 225 | ", null);viewHolder = new ViewHolder(view);view.setTag(viewHolder);}else{viewHolder = (ViewHolder)view.getTag();}"; 226 | } 227 | 228 | //添加接口实现 229 | private void addViewClickListenerInterface() { 230 | if (!mContext.isClickClass()) { 231 | PsiClass clickClass = getPsiClassByName(Definitions.ViewClickListener); 232 | if (clickClass != null) { 233 | PsiJavaCodeReferenceElement ref = mFactory.createClassReferenceElement(clickClass); 234 | getPsiClass().getImplementsList().add(ref); 235 | } else { 236 | Utils.showErrorNotification(mContext.getProject(), "Can't find View.OnClickListener!"); 237 | } 238 | } 239 | } 240 | 241 | private PsiClass getPsiClassByName(String cls) { 242 | GlobalSearchScope searchScope = GlobalSearchScope.allScope(mContext.getProject()); 243 | JavaPsiFacade javaPsiFacade = JavaPsiFacade.getInstance(mContext.getProject()); 244 | return javaPsiFacade.findClass(cls, searchScope); 245 | } 246 | 247 | /** 248 | * Create fields for injections inside main class 249 | */ 250 | protected void generateFields() { 251 | // add injections into main class 252 | StringBuilder injection = new StringBuilder(); 253 | 254 | if (!mContext.isActivity() && !mContext.getFieldNameList().contains("rootView")) { 255 | injection.delete(0, injection.length()); 256 | injection.append("protected View rootView;"); 257 | getPsiClass().add(mFactory.createFieldFromText(injection.toString(), getPsiClass())); 258 | } 259 | 260 | for (Element element : mElements) { 261 | if (element.isDeclared) {//没有勾选,或者同样名字的变量已经声明过了 262 | continue; 263 | } 264 | 265 | injection.delete(0, injection.length()); 266 | injection.append("protected "); 267 | injection.append(getFieldTypeName(element)); 268 | injection.append(" "); 269 | injection.append(element.fieldName); 270 | injection.append(";"); 271 | 272 | getPsiClass().add(mFactory.createFieldFromText(injection.toString(), getPsiClass())); 273 | } 274 | } 275 | 276 | protected String getFieldTypeName(Element element) { 277 | if (element.typeName.equals("")) { 278 | if (element.nameFull != null && element.nameFull.length() > 0) { // custom package+class 279 | element.typeName = element.nameFull; 280 | } else if (Definitions.paths.containsKey(element.name)) { // listed class 281 | element.typeName = Definitions.paths.get(element.name); 282 | } else { // android.widget 283 | element.typeName = "android.widget." + element.name; 284 | } 285 | } 286 | 287 | if (element.typeName.contains(".")) { 288 | if (!mContext.getImportStrList().contains(element.typeName)) { 289 | PsiClass importClass = getPsiClassByName(element.typeName); 290 | if (importClass != null) { 291 | mContext.getImportList().add(mFactory.createImportStatement(importClass)); 292 | mContext.getImportStrList().add(element.typeName); 293 | } 294 | } 295 | element.typeName = element.typeName.substring(element.typeName.lastIndexOf(".") + 1, element.typeName.length()); 296 | } 297 | return element.typeName; 298 | } 299 | 300 | StringBuilder stringBuilder = new StringBuilder(); 301 | 302 | protected String generateFindViewByIdText(Element element) { 303 | stringBuilder.delete(0, stringBuilder.length()); 304 | stringBuilder.append(element.fieldName) 305 | .append("=(") 306 | .append(getFieldTypeName(element)); 307 | 308 | if (mContext.isActivity()) { 309 | stringBuilder.append(")findViewById("); 310 | } else { 311 | stringBuilder.append(")rootView.findViewById("); 312 | } 313 | 314 | stringBuilder.append(element.getFullID()) 315 | .append(");"); 316 | 317 | return stringBuilder.toString(); 318 | } 319 | 320 | //添加InitView方法的调用 321 | private void addInitViewMethodInvoked() { 322 | //Activity处理 323 | if (mContext.isActivity()) { 324 | PsiMethod onCreate = getPsiClass().findMethodsByName("onCreate", false)[0]; 325 | if (!containsInitViewMethodInvokedLine(onCreate, Definitions.Activity_InitViewMethodInvoked)) { 326 | for (PsiStatement statement : onCreate.getBody().getStatements()) { 327 | if (Utils.isLayoutStatement(statement)) {// 328 | statement.replace(mFactory.createStatementFromText("super.setContentView(" + Utils.getIdFromLayoutStatement(statement.getText()) + ");", getPsiClass())); 329 | onCreate.getBody().addBefore(mFactory.createStatementFromText( 330 | Definitions.Activity_InitViewMethodInvoked, getPsiClass()), onCreate.getBody().getLastBodyElement()); 331 | } 332 | } 333 | } 334 | } else if (mContext.isFragment()) { 335 | PsiMethod onCreateView = getPsiClass().findMethodsByName("onCreateView", false)[0]; 336 | if (!containsInitViewMethodInvokedLine(onCreateView, Definitions.Other_InitViewMethodInvoked)) { 337 | boolean isReturnMode = false; 338 | for (PsiStatement statement : onCreateView.getBody().getStatements()) { 339 | //解析 return R.layout.activity.main 340 | if (statement instanceof PsiReturnStatement) { 341 | String returnValue = ((PsiReturnStatement) statement).getReturnValue().getText(); 342 | if (returnValue.contains("R.layout")) { 343 | onCreateView.getBody().addBefore(mFactory.createStatementFromText("rootView = inflater.inflate(" + Utils.getIdFromLayoutStatement(returnValue) + ", null);", getPsiClass()), statement); 344 | onCreateView.getBody().addBefore(mFactory.createStatementFromText(Definitions.Other_InitViewMethodInvoked, getPsiClass()), statement); 345 | statement.replace(mFactory.createStatementFromText("return rootView;", getPsiClass())); 346 | isReturnMode = true; 347 | } 348 | break; 349 | } 350 | } 351 | 352 | if (!isReturnMode) { 353 | for (PsiStatement statement : onCreateView.getBody().getStatements()) { 354 | /* 355 | * 解析 356 | * 357 | * R.layout.activity.main 358 | * return null 359 | * */ 360 | if (Utils.isLayoutStatement(statement)) { 361 | statement.replace(mFactory.createStatementFromText("rootView = inflater.inflate(" 362 | + Utils.getIdFromLayoutStatement(statement.getText()) + ", null);", getPsiClass())); 363 | } 364 | 365 | if (statement instanceof PsiReturnStatement) { 366 | //设置语句 367 | onCreateView.getBody().addBefore(mFactory.createStatementFromText("initView(rootView);", getPsiClass()), statement); 368 | statement.replace(mFactory.createStatementFromText("return rootView;", getPsiClass())); 369 | } 370 | } 371 | } 372 | } 373 | } 374 | } 375 | 376 | private boolean containsInitViewMethodInvokedLine(PsiMethod method, String line) { 377 | final PsiCodeBlock body = method.getBody(); 378 | if (body == null) { 379 | return false; 380 | } 381 | PsiStatement[] statements = body.getStatements(); 382 | for (PsiStatement psiStatement : statements) { 383 | String statementAsString = psiStatement.getText(); 384 | if (psiStatement instanceof PsiExpressionStatement && (statementAsString.contains(line))) { 385 | return true; 386 | } 387 | } 388 | return false; 389 | } 390 | } --------------------------------------------------------------------------------