打造酷炫AndroidStudio插件

简介: 打造酷炫AndroidStudio插件

打造酷炫AndroidStudio插件


转载请注明出处:【huachao1001的专栏:http://blog.csdn.net/huachao1001/article/details/53898286】

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

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

微信图片_20221214190814.gif

1. 基本思路

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

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

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

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

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

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

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

用户未点击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

相关文章
|
机器学习/深度学习 算法 数据可视化
python/matlab图像去雾/去雨综述
python/matlab图像去雾/去雨综述
1547 120
|
3月前
|
存储 人工智能 缓存
最新版 Chatbox (跨平台 AI 客户端应用) 功能介绍及接入阿里云百炼 Coding Plan、Token Plan 教程
Chatbox作为一款开源、跨平台的AI客户端应用,凭借多模型聚合、本地优先、多模态交互与MCP生态集成等核心优势,成为开发者与内容创作者统一管理AI服务的首选工具。最新版Chatbox深度兼容阿里云百炼平台,支持Coding Plan与Token Plan两大订阅方案无缝接入,让用户可在统一界面中高效调用通义千问系列模型,实现编程开发、内容创作、信息处理等全场景AI能力落地。本文将全面拆解Chatbox的核心功能,从安装部署、基础配置到自定义工作流,再到阿里云百炼Coding Plan、Token Plan的完整接入流程,提供可直接运行的代码命令与实操步骤,助力用户快速搭建高效、安全、可扩展
625 0
|
3月前
|
人工智能 移动开发 前端开发
文章转视频用什么软件?图文作者开源自动化工作流的真实复盘
本文为公众号图文作者量身打造,详解“文章→视频”自动化方案:从GPT-SoVITS配音、Whisper对齐、Manim/Stable Diffusion生成画面,到FFmpeg/花生AI合成的全开源管线;再升级至花生AI半自动打包流程。含可运行代码、避坑指南与成本对比,实现低成本批量出片。
|
4月前
|
人工智能 移动开发 前端开发
7个神级技巧,彻底去除网站的 AI 味儿!
大家好,我是程序员鱼皮。 先做个小测试,下面这几个网站,你能看出哪些是 AI 做的吗? 公布答案:全都是 AI 做的! 是不是觉得有点意外? “为啥我用 AI 搓出来的网站一股子 AI 味儿?而这些网站看起来干净很多呢?” 这就是接下来我要分享的: 什么是 AI 编程的 AI 味儿? 为什么网站会有 AI 味儿? 怎么去除网站的 AI 味儿? 点个收藏,我们开始 ⭐️ 推荐观看本文对应视频版,效果
417 0
7个神级技巧,彻底去除网站的 AI 味儿!
|
4月前
|
传感器 人工智能 运维
AI Agent设计与实战深度剖析:三大任务链路、四大智能体范式全解析
AI Agent也就是人工智能智能体,是一类具备环境感知、自主决策、任务执行与反馈迭代能力的智能实体。区别于传统大语言模型仅能完成问答、文本生成等被动输出任务,AI Agent可以主动感知外部环境变化,根据预设目标动态调整执行策略,联动各类工具完成全流程任务,并且在交互过程中持续积累经验、优化行为逻辑。如今这项技术已经广泛渗透到智能家居、工业生产、自动驾驶、智能运维、企业协同等众多领域,成为数字化与智能化转型的核心技术之一。
461 0
|
4月前
|
存储 缓存 算法
Java在分布式ID生成器(Leaf——美团技术方案)中的实现
1.分布式ID的更高要求 雪花算法依赖时钟,存在回拨问题;数据库号段方案需要频繁访问DB。美团的Leaf方案提供了两种模式:号段模式(Leaf-segment)和雪花模式(Leaf-snowflake),均用Java实现https://zzblmyl.com,解决了上述痛点。
333 0
|
4月前
|
机器学习/深度学习 PyTorch 测试技术
PyTorch深度学习实战 |多头注意力机制
摘要:本文详细介绍了Transformer中的多头自注意力机制,从整体结构到实现细节,包括四部分内容:(1)多头自注意力的基本架构;(2)内部计算流程解析;(3)注意力计算公式详解;(4)代码实现。重点阐述了多头并行的计算方式、缩放点积注意力的计算步骤(QK转置、缩放、softmax和加权求和),以及残差连接和层归一化的作用。通过&quot;Are you OK?&quot;示例展示了输入张量如何经过8个64维注意力头处理后拼接成512维输出。文章最后提供了完整的PyTorch实现代码,并附测试用例验证模型
324 0
|
6月前
|
Prometheus 并行计算 异构计算
containerd 节点 GPU 镜像预热记录
本次在GPU节点复现推理环境时,首遇镜像拉取失败(ImagePullBackOff),Pod卡在ContainerCreating状态。通过`crictl pull`逐源验证并预热vLLM、CUDA、Prometheus及pause镜像,明确分离镜像问题与模型问题,提升排障效率。(239字)
|
6月前
|
数据采集 人工智能 自然语言处理
AI与就业革命——替代、创造与技能重构
“AI会取代我的工作吗?”这或许是过去三年被问及最多的问题。
1542 0
|
9月前
|
人工智能 安全 C++
破解AI编程落地痛点:MonkeyCodeAI效率与安全双提升实战解析
长亭科技MonkeyCodeAI是企业级开源AI研发基础设施,破解AI编程工具同质化、数据不安全、依赖海外模型等痛点。支持双模融合(补全+Agent)、私有化部署、国产模型适配及全流程研发赋能,开箱即用,安全可控。
744 2
破解AI编程落地痛点:MonkeyCodeAI效率与安全双提升实战解析