打造酷炫AndroidStudio插件

vr077785 4年前
   <p>前面几篇文章学习了AndroidStudio插件的基础后,这篇文章打算开发一个酷炫一点的插件。因为会用到前面的基础,所以如果没有看前面系列文章的话,请先返回。当然,如果有基础的可以忽略之。先看看本文实现的最终效果如下(好吧,很多人说看的眼花):</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/1efe299cd7742ce2617b477ef5c40b66.gif"></p>    <p style="text-align:center">最终效果</p>    <p>虽然并没有什么实际用途,但是作为学习插件开发感觉挺有意思的。</p>    <h2>1. 基本思路</h2>    <p>基本思路可以归结如下几步:</p>    <ol>     <li> <p>通过 Editor 对象可以拿到封装代码编辑框的 JComponent 对象,即调用如下函数: JComponent component = editor.getContentComponent();</p> </li>     <li> <p>获取输入或删除的字符(或字符串。通过选中多个字符删除或粘贴则为字符串)。可以通过添加DocumentListener,监听文本变化。重写beforeDocumentChange函数,并通过DocumentEvent对象取得新的字符和旧的字符。分别通过函数: documentEvent.getNewFragment() 、 documentEvent.getOldFragment() 。它们代表着输入的字符串和删除的字符串。</p> </li>     <li> <p>将输入或删除的字符串在编辑框中显示出来。只需将各个字符串分别封装到 Jlabel 中,并将 JLabel 加入到 JComponent 中即可显示出输入或删除的字符串(或字符)。</p> </li>     <li> <p>获取用于显示各个字符串的 Jlabel 对象在 JComponent 中的坐标位置。添加 CaretListener ,监听光标的位置。每次光标位置发生变化,就刷新到临时变量中。当要添加一个 JLabel 时,获取当前的临时变量中保存的位置即为 Jlabel 应存放的位置。</p> </li>     <li> <p>动画效果。开启一个线程,对于输入的字符串,只需不断修改字体大小。对于删除的字符串,不断修改 JLabel 的位置和字体大小。</p> </li>     <li> <p>插件状态保存到本地。用户点击开启或者关闭插件以及其他开关选项,需要保存起来,下一次开启AndroidStudio时可以恢复。只需实现PersistentStateComponent接口即可。</p> </li>     <li> <p>用户未点击Action时,能自动注册DocumentListener。这主要是考虑到,用户开启了插件,下一次打开AndroidStudio时无需点击Aciton,直接输入时就能自动注册监听Document变化。由于注册DocumentListener需要Editor对象,而想要取得Editor对象只有两种方式:通过AnActionEvent对象的getData函数;另一种是通过DataContext对象,使用</p> <p>PlatformDataKeys.EDITOR.getData(dataContext) 方法。显然第一种方法只能在 AnAction 类的 actionPerformed 和 update 方法中才能取得。因此只能考虑用第二种方法,而在前面文章中介绍过,监听键盘字符输入时,可以取得 DataContext 对象。即重写 TypedActionHandler 接口的 execute 函数,execute参数中传递了 DataContext 对象。</p> </li>    </ol>    <p>可以看到,以上用到的知识都是前面3篇文章中介绍过的内容,并不复杂。只有第6条没有介绍,本文中会学习本地持久化数据。</p>    <h2>2. 插件状态本地持久化</h2>    <p>先看看如何实现本地持久化。首先定义一个全局共享变量类GlobalVar,使之实现 PersistentStateComponent 接口。先来个视觉上的认识,直接看代码。</p>    <pre>  /**   * 配置文件   * Created by huachao on 2016/12/27.   */  @State(          name = "amazing-mode",          storages = {                  @Storage(                          id = "amazing-mode",                          file = "$APP_CONFIG$/amazing-mode_setting.xml"                  )          }  )  public class GlobalVar implements PersistentStateComponent<GlobalVar.State> {        public static final class State {          public boolean IS_ENABLE;          public boolean IS_RANDOM;      }        @Nullable      @Override      public State getState() {          return this.state;      }        @Override      public void loadState(State state) {          this.state = state;      }        public State state = new State();        public GlobalVar() {            state.IS_ENABLE = false;          state.IS_RANDOM = false;      }        public static GlobalVar getInstance() {          return ServiceManager.getService(GlobalVar.class);      }    }</pre>    <p>使用 @State 注解指定本地存储位置、id等。具体实现基本可以参照这个模板写,就是重写loadState()和getState()两个函数。另外需要注意一下getInstance()函数的写法。基本模板就这样,没有什么特别的地方,依葫芦画瓢就行。</p>    <p>还有一点特别重要,一定要记得在 plugin.xml 中注册这个持久化类。找到 <extensions> 标签,加入 <applicationService> 子标签,如下:</p>    <pre>  <extensions defaultExtensionNs="com.intellij">      <!-- Add your extensions here -->      <applicationService              serviceImplementation="com.huachao.plugin.util.GlobalVar"              serviceInterface="com.huachao.plugin.util.GlobalVar"      />  </extensions></pre>    <p>这样写完以后,在获取数据的时候,直接如下:</p>    <pre>  private GlobalVar.State state = GlobalVar.getInstance().state;  //state.IS_ENABLE  //state.IS_RANDOM</pre>    <h2>3. 编写Action</h2>    <p>主要包含2个Action: EnableAction 和 RandomColorAction 。 EnableAction 用于设置插件的开启或关闭, RandomColorAction 用于设置是否使用随机颜色。由于二者功能类似,我们只看看EnableAction的实现:</p>    <pre>  /**   * Created by huachao on 2016/12/27.   */  public class EnableAction extends AnAction {      private GlobalVar.State state = GlobalVar.getInstance().state;          @Override      public void update(AnActionEvent e) {          Project project = e.getData(PlatformDataKeys.PROJECT);          Editor editor = e.getData(PlatformDataKeys.EDITOR);          if (editor == null || project == null) {              e.getPresentation().setEnabled(false);          } else {              JComponent component = editor.getContentComponent();              if (component == null) {                  e.getPresentation().setEnabled(false);              } else {                  e.getPresentation().setEnabled(true);              }          }          updateState(e.getPresentation());      }        @Override      public void actionPerformed(AnActionEvent e) {          Project project = e.getData(PlatformDataKeys.PROJECT);          Editor editor = e.getData(PlatformDataKeys.EDITOR);          if (editor == null || project == null) {              return;          }          JComponent component = editor.getContentComponent();          if (component == null)              return;          state.IS_ENABLE = !state.IS_ENABLE;          updateState(e.getPresentation());            //只要点击Enable项,就把缓存中所有的文本清理          CharPanel.getInstance(component).clearAllStr();            GlobalVar.registerDocumentListener(project, editor, state.IS_ENABLE);      }          private void updateState(Presentation presentation) {            if (state.IS_ENABLE) {              presentation.setText("Enable");              presentation.setIcon(AllIcons.General.InspectionsOK);          } else {              presentation.setText("Disable");              presentation.setIcon(AllIcons.Actions.Cancel);          }      }      }</pre>    <p>代码比较简单,跟前面几篇文章中写的很相似。只需注意一下actionPerformed函数中调用了两个函数:</p>    <pre>  CharPanel.getInstance(component).clearAllStr();  GlobalVar.registerDocumentListener(project, editor, state.IS_ENABLE);</pre>    <p>CharPanel 对象中的 clearAllStr() 函数后面介绍,只需知道它是将缓存中的所有动画对象清除。 GlobalVar 对象中的 registerDocumentListener () 函数是添加 DocumentListener 监听器。实现本文效果的中枢是 DocumentListener 监听器,是通过监听文本内容发生变化来获取实现字符动画效果的数据。因此应应可能早地将 DocumentListener 监听器加入,而 DocumentListener 监听器加入的时刻包括:用户点击Action、用户敲入字符。也就是说,多个地方都存在添加 DocumentListener 监听器的可能。因此把这个函数抽出来,加入到GlobalVar中,具体实现如下:</p>    <pre>  private static AmazingDocumentListener amazingDocumentListener = null;    public static void registerDocumentListener(Project project, Editor editor, boolean isFromEnableAction) {      if (!hasAddListener || isFromEnableAction) {          hasAddListener = true;          JComponent component = editor.getContentComponent();          if (component == null)              return;          if (amazingDocumentListener == null) {                amazingDocumentListener = new AmazingDocumentListener(project);              Document document = editor.getDocument();              document.addDocumentListener(amazingDocumentListener);          }            Thread thread = new Thread(CharPanel.getInstance(component));          thread.start();      }  }</pre>    <p>可以看到,一旦 DocumentListener 监听器被加入,就会开启一个线程,这个线程是一直执行,实现动画效果。 DocumentListener 监听器只需加入一次即可。</p>    <h2>4. 实现动画</h2>    <p>前面多次使用到了CharPanel对象,CharPanel对象就是用于实现动画效果。先源码:</p>    <pre>  package com.huachao.plugin.util;    import com.huachao.plugin.Entity.CharObj;    import javax.swing.*;  import java.awt.*;  import java.util.*;  import java.util.List;    /**   * Created by huachao on 2016/12/27.   */  public class CharPanel implements Runnable {      private JComponent mComponent;      private Point mCurPosition;      private Set<CharObj> charSet = new HashSet<CharObj>();      private List<CharObj> bufferList = new ArrayList<CharObj>();          private GlobalVar.State state = GlobalVar.getInstance().state;          public void setComponent(JComponent component) {          mComponent = component;      }          public void run() {          while (state.IS_ENABLE) {              if (GlobalVar.font != null) {                  synchronized (bufferList) {                      charSet.addAll(bufferList);                      bufferList.clear();                  }                  draw();                  int minFontSize = GlobalVar.font.getSize();                    //修改各个Label的属性,使之能以动画形式出现和消失                  Iterator<CharObj> it = charSet.iterator();                  while (it.hasNext()) {                      CharObj obj = it.next();                      if (obj.isAdd()) {//如果是添加到文本框                          if (obj.getSize() <= minFontSize) {//当字体大小到达最小后,使之消失                              mComponent.remove(obj.getLabel());                              it.remove();                          } else {//否则,继续减小                              int size = obj.getSize() - 6 < minFontSize ? minFontSize : (obj.getSize() - 6);                              obj.setSize(size);                          }                      } else {//如果是从文本框中删除                          Point p = obj.getPosition();                          if (p.y <= 0 || obj.getSize() <= 0) {//如果到达最底下,则清理                              mComponent.remove(obj.getLabel());                              it.remove();                          } else {                              p.y = p.y - 10;                              int size = obj.getSize() - 1 < 0 ? 0 : (obj.getSize() - 1);                              obj.setSize(size);                          }                      }                  }                }              try {                  if (charSet.isEmpty()) {                      synchronized (charSet) {                          charSet.wait();                      }                  }                  Thread.currentThread().sleep(50);              } catch (InterruptedException e) {                  e.printStackTrace();              }          }      }        //绘制文本,本质上只是修改各个文本的位置和字体大小      private void draw() {          if (mComponent == null)              return;            for (CharObj obj : charSet) {              JLabel label = obj.getLabel();                Font font = new Font(GlobalVar.font.getName(), GlobalVar.font.getStyle(), obj.getSize());                label.setFont(font);              FontMetrics metrics = label.getFontMetrics(label.getFont());              int textH = metrics.getHeight(); //字符串的高, 只和字体有关              int textW = metrics.stringWidth(label.getText()); //字符串的宽              label.setBounds(obj.getPosition().x, obj.getPosition().y - (textH - GlobalVar.minTextHeight), textW, textH);          }          mComponent.invalidate();      }        public void clearAllStr() {          synchronized (bufferList) {              bufferList.clear();              charSet.clear();                Iterator<CharObj> setIt = charSet.iterator();              while (setIt.hasNext()) {                  CharObj obj = setIt.next();                  mComponent.remove(obj.getLabel());              }                Iterator<CharObj> bufferIt = bufferList.iterator();              while (bufferIt.hasNext()) {                  CharObj obj = bufferIt.next();                  mComponent.remove(obj.getLabel());              }           }      }        //单例模式,静态内部类      private static class SingletonHolder {          //静态初始化器,由JVM来保证线程安全          private static CharPanel instance = new CharPanel();      }        //返回单例对象      public static CharPanel getInstance(JComponent component) {          if (component != null) {              SingletonHolder.instance.mComponent = component;          }          return SingletonHolder.instance;      }        //由光标监听器回调,由此可动态获取当前光标位置      public void setPosition(Point position) {          this.mCurPosition = position;      }        /**       * 将字符串添加到列表中。       *       * @isAdd 如果为true表示十新增字符串,否则为被删除字符串       * @str 字符串       */      public void addStrToList(String str, boolean isAdd) {          if (mComponent != null && mCurPosition != null) {                CharObj charObj = new CharObj(mCurPosition.y);              JLabel label = new JLabel(str);              charObj.setStr(str);              charObj.setAdd(isAdd);              charObj.setLabel(label);              if (isAdd)                  charObj.setSize(60);              else                  charObj.setSize(GlobalVar.font.getSize());              charObj.setPosition(mCurPosition);              if (state.IS_RANDOM) {                  label.setForeground(randomColor());              } else {                  label.setForeground(GlobalVar.defaultForgroundColor);              }              synchronized (bufferList) {                  bufferList.add(charObj);              }              if (charSet.isEmpty()) {                  synchronized (charSet) {                      charSet.notify();                  }              }                mComponent.add(label);          }      }        //以下用于产生随机颜色      private static final Color[] COLORS = {Color.GREEN, Color.BLACK, Color.BLUE, Color.ORANGE, Color.YELLOW, Color.RED, Color.CYAN, Color.MAGENTA};        private Color randomColor() {          int max = COLORS.length;          int index = new Random().nextInt(max);          return COLORS[index];      }  }</pre>    <p>解释一下两个关键函数 run() 和 draw() 。 run() 函数是开启新线程开始执行的函数,它的实现是一个循环,当插件开启时会一直循环运行。 CharPanel 使用了2个集合来保持用户删除或者添加的字符串, charSet 是会直接被显示出来的, bufferList 保存的是 DocumentListener 监听器监听到的输入或删除的字符串。输入或删除的字符串都封装到 CharObj 类中。run函数中每一次循环之前,先将bufferList中数据全部转移到charSet中。为什么要使用2个集合呢?这主要是因为,当循环遍历charSet时,如果DocumentListener监听到的变化数据直接加入到charSet中,会导致出错。因为Java的集合在遍历时,不允许添加或删除里面的元素。</p>    <p>run函数每一次循环都会调用draw()函数,draw()函数根据CharObj封装的数据,将JLabel的位置属性和字体属性重新设置一次,这样就使得JLabel有动画效果,因为run函数的每次循环的最后会逐步修改字体大小和位置数据。</p>    <p> </p>    <p> </p>    <p>来自:https://github.com/huachao1001/Amazing-Mode</p>    <p> </p>