打造酷炫的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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

时间: 2024-11-01 01:17:59

打造酷炫的AndroidStudio插件的相关文章

移动社交时代 如何打造酷炫的用户体验

大家好,我是来自听云的常旭,今天我带来的内容是不体现无未来,如何打造酷炫的用户体验,相信各位还记得在以前我们在做转账这个动作的时候,可能要去到银行的柜面,而慢慢出现了ATM机,而再往后大概在2003,2004年的时候又有了网银那套系统,在那个年代我们认为网银就是一个面子工程,但后来通过现在的视角再回看的时候我们会发现银行大部分的业务都是来自于网银,而今天我们更多的动作是通过一个移动的APP做转账,那在这个过程中我们会发现实际上每一个时间点都会有不同的应用出现,而在一个应用刚出现的这个过程中它会有

PS手把手教你打造酷炫的专属星球特效

  我们该如何打造星球特效?一定有人会告诉你用极坐标.但这个方法真的有些落伍了.今天@他山之眼 向大家强力推荐一个超方便的方法,操作更加方便,效果更加酷炫,一起来把你熟悉的场景变成一颗星球悬浮在太空中,体验一把上帝视角的感觉! 想要让自己的照片与众不同? 星球特效可以满足你这一切. 极坐标一般是这样操作: 即先把图片垂直翻转,然后使用极坐标. 之所以要垂直翻转,是因为画面从上到下,反映到极坐标里面就是从内到外,为了保证出现星球的效果,所以要进行垂直翻转. 但是极坐标有几个无法克服的缺点: 一是要

PS教你快速打造酷炫抢眼的逆光场景

  需求方说:"要有光!"于是,便有了光-- 平时浏览一些优秀作品的时候,经常会看到一些光效处理非常棒的作品.有时我们看后会反问自己:"这些使页面看起来高大的光是怎么打上去的呢?今天搜狐同学通过一个案例来教大家快速打造逆光场景.来找属于你的光吧! 当然,这只是本人自己常用的方法,方法是活的,只要能够做出好的效果就行,这个案例也算不得高大上,只希望那些需要光的同学能够从这篇教程里找到属于自己的光. 先来看一下最终的效果: 这是一个天龙全球争霸赛复活赛的专题页面,这里也许有人会有

RxJava实践之打造酷炫启动页

之前注意到coding APP启动页很是酷炫,今天我们使用RxJava和属性动画模仿实现其效果. 一.新建启动页WelcomeActivity 注意,我们这里让WelcomeActivity继承Activity不要继承AppCompatActivity,因为AppCompatActivity会默认去加载主题,造成卡顿  public class WelcomeActivity extends Activity {        @Override      protected void onCr

打造酷炫实用APP动效的两个关键

  编者按:动效化显然已成为移动互联网产品的新趋势,如何设计出有趣且吸引人的动效已成为设计师们的新课题.不同的产品适合不同类型的动效,有些产品适合炫酷的动效,有些则不适合.切记不要把动效设计成华而不实的花架子,而应该将其视为提升用户体验的新方法. 移动互联网时代已经到来,APP已如天空的繁星,数也数不清.随着手机硬件的不断升级,实现炫酷且流畅的动效不再是遥远的梦想.如果你是APP达人,喜欢试用各种APP,你肯定会发现越来越多的APP开始动效化. 一个真正的美女一定是同时兼具外在美和内在美,评价一

利用photoshop打造酷炫的人物特效

  本教程的目的当然不是让你欣赏楚楚动人落雁沉鱼的异国美女,而是教你如何用看起来很糟糕的笔刷.一系列图片以及图层调整制作生化大片中的场景. 本教程难度三颗星,中级,不过其中有一些小技巧还是非常有用了,快来试试吧~ 注意: 本教程基于PS CS6,因此一些截图与之前的版本略有不同,个别笔刷也仅供CS6版本使用. 最终效果: 教程所需素材下载: Model by ~tigersgirl Flag City Building Grunge Brush Cloud Brush Step 1 新建一个文件

PS教你打造酷炫的九重天魔幻特效

  所谓九重天特效,是指具有很强的层次感和空间感,整体效果夸张而又震撼的画面效果,其突出特征为画面充满着扭曲感觉,整体向中心聚拢. Step 1:筛选 在做这种特效之前,我们首要要筛选图片,不是所有的图片都适合制作这种特效的. 1.画面有水平线. 这是一个很重要的因素,只有当画面有水平线时,才能形成完美的弧形,进而形成夸张的画面效果. 2.画面层次分明. 只有当画面有明显的层次时,才能凸显出画面的气势. 例如这一张图片就有清晰而丰富的层次: 3.画面有明显的区分 例如天空是天空,地面是地面,如果

PS教你打造酷炫的人物特效

  注意:本教程基于PS CS6,因此一些截图与之前的版本略有不同,个别笔刷也仅供CS6版本使用. 最终效果: 教程所需素材下载: Model by ~tigersgirl Flag City Building Grunge Brush Cloud Brush Step 1 新建一个文件,大小为1290*700像素,背景为黑色.再新建一个图层,用grunge笔刷(素材中的第四个)随意的在背景中画几笔. 在PS中打开美女的照片(素材中的第一个),用快速选择工具选出美女,如下: 将其复制粘贴到我们的

PS教你利用堆栈手法打造酷炫的长曝光效果

  我们先看一组图片: 图片好不好看我们先不谈,但是这种摄影手法体现的"艺术灵感"正是摄影的魅力. 这种图片是怎么拍摄的呢? 我先介绍一个概念:堆栈. 什么叫堆栈?Adobe这么说: 图像堆栈将一组参考帧相似.但品质或内容不同的图像组合在一起.将多个图像组合到堆栈中之后,您就可以对它们进行处理,生成一个复合视图,消除不需要的内容或杂色. 简单点儿理解,就是把一叠图片放在那里,然后通过某种算法,取每张图片一点儿或者混合每张图片一点儿,最后达到一种需要的效果. 举个例子,我们可以用堆栈来合