打造酷炫的AndroidStudio插件

作者:huachao1001 时间:2021-07-27 03:06:42 

前面几篇文章学习了AndroidStudio插件的基础后,这篇文章打算开发一个酷炫一点的插件。因为会用到前面的基础,所以如果没有看前面系列文章的话,请先返回。当然,如果有基础的可以忽略之。先看看本文实现的最终效果如下(好吧,很多人说看的眼花):

打造酷炫的AndroidStudio插件

虽然并没有什么实际用途,但是作为学习插件开发感觉挺有意思的。

1. 基本思路

基本思路可以归结如下几步:

1)、通过Editor对象可以拿到封装代码编辑框的JComponent对象,即调用如下函数:JComponent component = editor.getContentComponent();

2)、获取输入或删除的字符(或字符串。通过选中多个字符删除或粘贴则为字符串)。可以通过添加DocumentListener,监听文本变化。重写beforeDocumentChange函数,并通过DocumentEvent对象取得新的字符和旧的字符。分别通过函数:documentEvent.getNewFragment()、documentEvent.getOldFragment()。它们代表着输入的字符串和删除的字符串。

3)、将输入或删除的字符串在编辑框中显示出来。只需将各个字符串分别封装到Jlabel中,并将JLabel加入到JComponent中即可显示出输入或删除的字符串(或字符)。

4)、获取用于显示各个字符串的Jlabel对象在JComponent中的坐标位置。添加CaretListener,监听光标的位置。每次光标位置发生变化,就刷新到临时变量中。当要添加一个JLabel时,获取当前的临时变量中保存的位置即为Jlabel应存放的位置。

5)、动画效果。开启一个线程,对于输入的字符串,只需不断修改字体大小。对于删除的字符串,不断修改JLabel的位置和字体大小。

6)、插件状态保存到本地。用户点击开启或者关闭插件以及其他开关选项,需要保存起来,下一次开启AndroidStudio时可以恢复。只需实现PersistentStateComponent接口即可。

7)、用户未点击Action时,能自动注册DocumentListener。这主要是考虑到,用户开启了插件,下一次打开AndroidStudio时无需点击Aciton,直接输入时就能自动注册监听Document变化。由于注册DocumentListener需要Editor对象,而想要取得Editor对象只有两种方式:通过AnActionEvent对象的getData函数;另一种是通过DataContext对象,使用
PlatformDataKeys.EDITOR.getData(dataContext)方法。显然第一种方法只能在AnAction类的actionPerformed和update方法中才能取得。因此只能考虑用第二种方法,而在前面文章中介绍过,监听键盘字符输入时,可以取得DataContext对象。即重写TypedActionHandler接口的execute函数,execute参数中传递了DataContext对象。

可以看到,以上用到的知识都是前面3篇文章中介绍过的内容,并不复杂。只有第6条没有介绍,本文中会学习本地持久化数据。

2. 插件状态本地持久化

先看看如何实现本地持久化。首先定义一个全局共享变量类GlobalVar,使之实现PersistentStateComponent接口。先来个视觉上的认识,直接看代码。


/**
* 配置文件
* 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);
}

}

使用@State注解指定本地存储位置、id等。具体实现基本可以参照这个模板写,就是重写loadState()和getState()两个函数。另外需要注意一下getInstance()函数的写法。基本模板就这样,没有什么特别的地方,依葫芦画瓢就行。

还有一点特别重要,一定要记得在plugin.xml中注册这个持久化类。找到<extensions>标签,加入<applicationService>子标签,如下:


<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
<applicationService
  serviceImplementation="com.huachao.plugin.util.GlobalVar"
  serviceInterface="com.huachao.plugin.util.GlobalVar"
/>
</extensions>

这样写完以后,在获取数据的时候,直接如下:


private GlobalVar.State state = GlobalVar.getInstance().state;
//state.IS_ENABLE
//state.IS_RANDOM

3. 编写Action

主要包含2个Action:EnableAction和RandomColorAction。EnableAction用于设置插件的开启或关闭,RandomColorAction用于设置是否使用随机颜色。由于二者功能类似,我们只看看EnableAction的实现:


/**
* 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);
 }
}

}

代码比较简单,跟前面几篇文章中写的很相似。只需注意一下actionPerformed函数中调用了两个函数:


CharPanel.getInstance(component).clearAllStr();
GlobalVar.registerDocumentListener(project, editor, state.IS_ENABLE);

CharPanel对象中的clearAllStr()函数后面介绍,只需知道它是将缓存中的所有动画对象清除。GlobalVar对象中的registerDocumentListener ()函数是添加DocumentListener * 。实现本文效果的中枢是DocumentListener * ,是通过监听文本内容发生变化来获取实现字符动画效果的数据。因此应应可能早地将DocumentListener * 加入,而DocumentListener * 加入的时刻包括:用户点击Action、用户敲入字符。也就是说,多个地方都存在添加DocumentListener * 的可能。因此把这个函数抽出来,加入到GlobalVar中,具体实现如下:


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();
}
}

可以看到,一旦DocumentListener * 被加入,就会开启一个线程,这个线程是一直执行,实现动画效果。DocumentListener * 只需加入一次即可。

4. 实现动画

前面多次使用到了CharPanel对象,CharPanel对象就是用于实现动画效果。先源码:


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];
}
}

解释一下两个关键函数run()和draw()。run()函数是开启新线程开始执行的函数,它的实现是一个循环,当插件开启时会一直循环运行。CharPanel使用了2个集合来保持用户删除或者添加的字符串, charSet是会直接被显示出来的,bufferList保存的是DocumentListener * 监听到的输入或删除的字符串。输入或删除的字符串都封装到CharObj类中。run函数中每一次循环之前,先将bufferList中数据全部转移到charSet中。为什么要使用2个集合呢?这主要是因为,当循环遍历charSet时,如果DocumentListener监听到的变化数据直接加入到charSet中,会导致出错。因为Java的集合在遍历时,不允许添加或删除里面的元素。

run函数每一次循环都会调用draw()函数,draw()函数根据CharObj封装的数据,将JLabel的位置属性和字体属性重新设置一次,这样就使得JLabel有动画效果,因为run函数的每次循环的最后会逐步修改字体大小和位置数据。

5. 源码

其他代码比较简单,对着代码解释也没什么意思。直接献上源码,如有疑惑的地方请留言,我尽量找时间一一回复。

Github地址:https://github.com/huachao1001/Amazing-Mode

来源:http://blog.csdn.net/huachao1001/article/details/53898286

标签:AndroidStudio,插件
0
投稿

猜你喜欢

  • Springboot 使用内置tomcat禁止不安全HTTP的方法

    2022-07-12 10:45:45
  • Apache Shrio安全框架实现原理及实例详解

    2023-07-27 19:47:59
  • c# 通过经纬度查询 具体的地址和区域名称

    2023-08-15 11:41:53
  • 让C# Excel导入导出 支持不同版本Office

    2023-01-11 05:30:53
  • SpringBoot 多任务并行+线程池处理的实现

    2023-04-02 01:16:25
  • 简单了解Java多态向上转型相关原理

    2023-10-11 16:11:01
  • java获取注册ip实例

    2023-11-03 23:01:12
  • 浅谈Springboot下引入mybatis遇到的坑点

    2023-09-09 05:55:09
  • springboot整合kaptcha生成验证码功能

    2023-07-14 21:48:51
  • springboot自定义异常视图过程解析

    2023-06-29 09:44:36
  • 详解springmvc控制登录用户session失效后跳转登录页面

    2021-12-08 19:36:40
  • Idea 搭建Spring源码环境的超详细教程

    2023-09-30 16:13:17
  • Spring中多配置文件及引用其他bean的方式

    2023-07-01 17:31:03
  • GoLang与Java各自生成grpc代码流程介绍

    2021-06-20 09:28:50
  • Java CAS原子操作详解

    2023-05-06 15:37:15
  • Android实现调用摄像头拍照并存储照片

    2023-05-02 10:48:20
  • 深入了解Java对象的克隆

    2021-10-29 13:59:35
  • Java Idea TranslationPlugin翻译插件使用解析

    2023-12-01 10:25:02
  • C#实现把图片转换成二进制以及把二进制转换成图片的方法示例

    2023-08-10 15:25:51
  • mybatis 插件: 打印 sql 及其执行时间实现方法

    2023-05-29 16:49:19
  • asp之家 软件编程 m.aspxhome.com