打造酷炫的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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。