我被老板炒鱿鱼了!因为我在IDE里看漂亮小姐姐跳舞!
本文作者
作者:陈小缘
链接:
https://blog.csdn.net/u011387817/article/details/119217486
本文由作者授权发布。
这篇文章被我耽搁了好久好久没发出来,原因是因为太长了,不太好编辑,不过我还是决定先给大家演示下效果:
就这样,Android Studio 蹦的一声就成了播放器,关键还是沉浸在代码之下的,你还可以继续写代码。
当然看电影写代码比较费脑子,在我让群友多聊技术之后,他们果断往群里发了个视频,我们看下效果:
好了,没错,这篇文章讲的是一个视频背景插件的开发,从想法、到调研、到各种断点猜测、到尝试、到优化,最终完成。
鉴于篇幅较长,先写上安装方式,感兴趣的同学可以先体验上。
源码地址:
https://github.com/wuyr/intellij-media-player
本地安装:
到 releases 里下载最新的 intellij-media-player.zip 后拖把它拖进IDE中并重启。
https://github.com/wuyr/intellij-media-player/releases
在线安装:
Settings -> Plugins -> Marketplace里搜索Media Player:
点击安装即可;
安装后,顶部导航栏会多一个 Player,剩下你应该明白了。
注:如果需要循环播放,在控制栏的左下角有个回收图标点击即可。
好了,下面开始一起见证这个插件的诞生吧!
去年在新电脑上看视频的时候,在触摸板上做了一个缩放的手势把程序列表call出来了:
我那时候是纯黑色的壁纸,视频也刚好播放到白色衣服人物在黑夜中的画面,加上若隐若现的应用程序图标,这虚实结合的效果使得画面中的人物变得立体起来了!甚至有一种身临其境的感觉!
我当时就觉得,哇这种效果好棒啊,就像在播放透明背景的视频一样。记得那时候还在鸿神的群里讨论了一下关于播放透明视频的话题,后面有群友提到Android Studio就有个自带的设置透明背景图的功能。
第二天,好奇的我想知道Android Studio它是怎么做到把窗口下所有组件都设置成半透明并且把图片放进去的。。。
这一节需要用到IntelliJ IDEA来进行debug,以Android Studio作为这次debug的目标,没有Android Studio的同学,用JetBrains系列的其他IDE也可以,都有这个功能的。
先把IDE对应版本的源码下载下来,看下AS的Build Version:
可以看到是202.7660开头的,然后在 JetBrains/intellij-community 这里找到对应的idea版本:
https://github.com/JetBrains/intellij-community/releases
下载、解压(我一般会把它重命名为source并放到IDEA的根目录下)。
接着打开IDEA,创建一个Plugin项目(这一步网上教程多如牛毛,这里就不赘述了),在Project Structure -> SDKs里把刚刚解压的源码关联上;
关联源码之后,到Run/Debug Configuration里新建一个Remote JVM Debug的configuration:
先不要关闭页面,打开Android Studio程序目录下的bin文件夹,找到studio.sh(Windows系统的是studio.bat),复制一份,可以命名为debug.sh(Windows系统保留bat后缀),然后编辑:
在文件的末尾,Run the IDE注释的下面会有"$JAVA_BIN"(Windows系统是"%JAVA_EXE%")字眼的:
在它后面加上刚刚新建的configuration里面的一串命令行参数,比如我的是:-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
像这样:
编辑之后保存,顺便保存刚刚新建的configuration。
好了,可以正式开始了,现在从终端里运行刚刚修改过的debug.sh,然后打开Settings -> Appearance & Behavior -> Appearance:
看到那个Background Image按钮没有?我们现在就要debug它的鼠标事件,以拿到这个按钮的对象,然后把它的listener扒出来。
现在可以回到IDEA这边,点一下那个绿色的小虫子,attach到AS进程了,成功之后会弹出debug控制台窗口并有以下字眼:
没有就是没成功,请重试上面的步骤。
成功attach之后,开始打断点。
随便新建一个类,在里面输入java.awt.Component,然后点开它的源码并找到processMouseEvent(MouseEvent e)这个方法:
在它调用listener.mousePressed方法那一行打个断点(监听鼠标按下)。接着回到AS窗口,鼠标点一下那个Background Image按钮:
会看到已经断点成功了,当前点击的Component是个JButton,熟悉Java Swing的同学会知道,这个JButton继承自JComponent,而JComponent里有个listenerList,里面的数组是专门存放EventListener的,看一下这个JButton设置了哪些listener:
看第4个listener,它里面持有一个AnAction的引用,这个AnAction的实例是SetBackgroundImageAction,还有toString的内容也是"Set Background Image"。
基本可以断定它就是这个按钮所对应的Action了,SHIFT + F4看看这个类的代码:
public class SetBackgroundImageAction extends DumbAwareAction {
...
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
Project project = e.getProject();
if (project == null) return;
VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE);
boolean image = file != null && ImageFileTypeManager.getInstance().isImage(file);
BackgroundImageDialog dialog = new BackgroundImageDialog(project, image ? file.getPath() : null);
dialog.showAndGet();
}
}
它在重写的actionPerformed方法里创建了BackgroundImageDialog对象并调用了showAndGet方法。
好,按F9让代码继续运行,果然弹出了一个这样的dialog:
随便设置一张图片,然后点OK:
emmmm,看来就是最后点击的OK按钮生效的,来看看这个BackgroundImageDialog的代码:
public class BackgroundImageDialog extends DialogWrapper {
@Override
protected void doOKAction() {
super.doOKAction();
storeRecentImages();
String value = calcNewValue();
String prop = getSystemProp();
myResults.put(prop, value);
if (value.startsWith(",")) value = null;
boolean perProject = myThisProjectOnlyCb.isSelected();
PropertiesComponent.getInstance(myProject).setValue(prop, perProject ? value : null);
if (!perProject) {
PropertiesComponent.getInstance().setValue(prop, value);
}
repaintAllWindows();
}
public static void repaintAllWindows() {
UISettings.getInstance().fireUISettingsChanged();
for (Window window : Window.getWindows()) {
window.repaint();
}
}
}
doOKAction方法,逻辑比较清晰,无非做了4件事:
保存最近使用过的图片路径;
获取当前图片路径和一些设置相关的信息;
应用当前设置的图片;
重绘所有窗口;
我们重点看第3,也就是PropertiesComponent那几句,稍微把它改造一下让它看起来更直观些:
boolean currentProjectOnly = myThisProjectOnlyCb.isSelected();
if (currentProjectOnly) {
// 只对当前项目生效
PropertiesComponent.getInstance(myProject).setValue(prop, value);
} else {
// 清除当前项目设置的背景
PropertiesComponent.getInstance(myProject).setValue(prop, null);
// 换成全局的
PropertiesComponent.getInstance().setValue(prop, value);
}
熟悉IDEA插件开发的同学会知道,这个PropertiesComponent其实只是持久化键值对的工具类,并不会直接给窗口设置背景图。也就是说,上面PropertiesComponent.getInstance().setValue(prop, value)这句代码,只是把prop=value这个键值对保存到本地而已。
先来弄清楚它这个键值对是怎么样的吧,在doOKAction方法中打个断点:
注意到这个value的格式没有?
它完整的格式其实是这样的:
图片绝对路径,透明度,绘制方式,基准点,翻转(可选项)
透明度的取值范围是0~100;
图片的绘制方式有:plain(原始尺寸)、scale(缩放到合适的大小)、tile(平铺);
基准点:top-left、top-center、top-right、middle-left、center、middle-right、bottom-left、bottom-center、bottom-right;
翻转:flipH(水平翻转)、flipV(垂直翻转)、flipHV(水平垂直翻转);
prop(key)是固定的:"idea.background.editor"。
好了,现在我们已经知道了背景图的设置方式,接下来就试着简单实现一下这个功能。
像其他插件一样,先创建一个Action:
class SetBackgroundAction : AnAction() {
override fun actionPerformed(event: AnActionEvent) {
// 固定的key
val key = "idea.background.editor"
// 图片路径
val path = "/home/wuyr/Downloads/Images/DSC00892.JPG"
// 15%透明度
val transparency = 15
// 自动缩放图片以适应屏幕
val type = "scale"
// 以图片中心区域为基点进行缩放变换
val pivot = "center"
// 拼接格式
val value = "$path,$transparency,$type,$pivot"
// 更新到本地
PropertiesComponent.getInstance().setValue(key, value)
// 重绘所有窗口
IdeBackgroundUtil.repaintAllWindows()
}
}
这个value的格式是完全按照上面的BackgroundImageDialog来定义的。
接着在plugin.xml里声明这个Action:
"SetBackgroundAction" class="SetBackgroundAction" text="Set Background">
<add-to-group group-id="ViewMenu" anchor="last"/>
id可以随便设置,保证跟其他Action不冲突就行;
class就是Action的完整类名(这个Action我没有放在任何一个package下所以这里看上去只有一个类名);
text:显示的文本;
add-to-group这一行表示我们把这个按钮放在菜单栏View的底部。
好,编译运行看一下效果:
点击这个选项,会发现背景图已经成功替换成Action里面指向的图片了。
现在,想让背景动起来非常简单,只需要周期性地更换图片的路径就行。不过当你真的这样做的时候,你会发现调用repaintAllWindows重绘界面会非常非常的慢,就跟播放PPT一样!
也许我们不应该直接使用这种方法来做动态背景效果,还是再研究一下它具体是怎么实现绘制背景图的吧,看看能不能找到粒度更小的实现方式。
到这简单收个尾,大概的探索方式就是这样的,希望大家能够有所收获。
完整的插件完成过程,后面大概还有 2/3的内容,大家可以移步:
https://blog.csdn.net/u011387817/article/details/119217486
源码地址:
https://github.com/wuyr/intellij-media-player
祝摸鱼快乐!!!(低调点,不要真的被炒鱿鱼了噢!)
-- END --
推荐:
全网最全的 Android 音视频和 OpenGL ES 干货,都在这了