精华内容
下载资源
问答
  • 无意之间翻出了大一时候学JAVA GUI Swing 时候的java代码,发现了一个有趣的代码,就是用java代码跑出一个炫酷壁纸效果的动图,跑了一下,感慨颇多,当时就是因为做这个,查了好久的资料,最终将其写出来,当时还...
  • java代码实现炫酷壁纸效果

    万次阅读 多人点赞 2018-12-14 07:46:34
    今天无意之间翻出了大一时候学JAVA GUI Swing 时候的java代码,发现了一个有趣的代码,就是用java代码跑出一个炫酷壁纸效果的动图,跑了一下,感慨颇多,当时就是因为做这个,查了好久的资料,最终将其写出来,...

    前言

      今天无意之间翻出了大一时候学JAVA GUI Swing 时候的java代码,发现了一个有趣的代码,就是用java代码跑出一个炫酷的壁纸效果的动图,跑了一下,感慨颇多,当时就是因为做这个,查了好久的资料,最终将其写出来,当时还将这个导包出来给同学看下,小小的装了下X。


    敲黑板,划重点

      为了自己进行思考,将所有注释都删了


    package Screen;
    import java.awt.*;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.event.KeyAdapter;
    import java.awt.event.KeyEvent;
    import java.awt.image.MemoryImageSource;
    import java.util.Random;
    import javax.swing.JDialog;
    import javax.swing.JPanel;
    import javax.swing.Timer;
    public class hikeSCreen extends JDialog implements ActionListener {
        private static final long serialVersionUID = 1L;
        private Random random = new Random();
        private Dimension screenSize;
        private JPanel graphicsPanel;
        private final static int gap = 20;
        private int[] posArr;
        private int lines;
        private int columns;
        public static void main(String[] args) {new hikeSCreen();
        }
        public hikeSCreen() {
            initComponents();
        }
        private void initComponents() {
            setLayout(new BorderLayout());
            graphicsPanel = new GraphicsPanel();
            add(graphicsPanel, BorderLayout.CENTER);
            Toolkit defaultToolkit = Toolkit.getDefaultToolkit();
            Image image = defaultToolkit.createImage(new MemoryImageSource(0, 0,
                    null, 0, 0));
            Cursor invisibleCursor = defaultToolkit.createCustomCursor(image,
                    new Point(0, 0), "cursor");
            setCursor(invisibleCursor);
            KeyPressListener keyPressListener = new KeyPressListener();
            this.addKeyListener(keyPressListener);
            this.setAlwaysOnTop(true);
            this.setUndecorated(true);
            this.getGraphicsConfiguration().getDevice().setFullScreenWindow(this);
            this.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
            setVisible(true);
            screenSize = Toolkit.getDefaultToolkit().getScreenSize();
            lines = screenSize.height / gap;
            columns = screenSize.width / gap;
            posArr = new int[columns + 1];
            random = new Random();
            for (int i = 0; i < posArr.length; i++) {
                posArr[i] = random.nextInt(lines);
                new Timer(100, this).start();
            }
        }
        private char getChr() {
            return (char)  (random.nextInt(94) + 33);
        }
        public void actionPerformed(ActionEvent e) {
            graphicsPanel. repaint();
        }
        @SuppressWarnings("serial")
        private class GraphicsPanel extends JPanel {
            public void paint(Graphics g) {
                Graphics2D g2d = (Graphics2D) g;
                g2d. setFont (getFont (). deriveFont (Font. BOLD));
                g2d. setColor(Color. BLACK);
                g2d. fillRect (0, 0, screenSize. width, screenSize. height);
                int currentColumn = 0;
                for (int x = 0; x < screenSize.width; x += gap) {
                    int endPos = posArr[currentColumn];
                    g2d. setColor(Color. GREEN);
                    g2d.drawString(String.valueOf(getChr()), x, endPos * gap);
                    int cg = 0;
                    int length = 25;
                    for (int j = endPos -length; j < endPos; j++) {
                        cg += 255/(length + 1);
                        if (cg > 255) {
                            cg = 255;
                        }
                        g2d.setColor(new Color(0, cg, 0));
                        g2d.drawString(String.valueOf(getChr()), x, j * gap);
                    }
                    posArr[currentColumn] += random.nextInt(5);
                    if ((posArr[currentColumn] -5)* gap > getHeight()) {
                        posArr[currentColumn] = random.nextInt(lines);
                    }
                    currentColumn++;
                }
            }
        }
        private class KeyPressListener extends KeyAdapter {
            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() ==KeyEvent.VK_ESCAPE) {
                    System.exit(0);
                }
            }
        }
    }
    

    效果图

       这是效果图 ,截图下来是静态的,但是这是动态的,速度方向也可以自己更改代码参数进行调整。
    在这里插入图片描述

    链接(5月11号追更)

      估计不少同学不想敲,没关系,我懂的,我将导出来的jar包也分享出来,链接:JAR包

      看到有小伙伴在评论区咨询编写java相关的软件,估计是刚入门的,没关系,大家都是从入门走进来的,慢慢来总会好的,关键一定要坚持,不要轻言放弃,加油!!!


    jdk链接:(https://pan.baidu.com/s/1e_pTCOT-iG7owDw4btZjOg) 提取码:mgc9
    idea链接:[idea](https://pan.baidu.com/s/1mqoHPK8V9vDu2mJoDMfi_g) 提取码:m6kg
    eclipse链接:https://pan.baidu.com/s/1Sw7i6D0cCm4IvTYYZFiD8A 提取码:zq6g

    总结(2019年5月6号更)

      从去年12月份发这篇博客到现在,将近半年了,这段时间里,获得了很多荣誉,也收获了很多来自老师同学的认可,还是挺开心的,当然了,背后付出的汗与泪也只有我一个人看到,我现在一些对关于考研阶段的思考都放在了我的博客上面,对考研有兴趣的同学也可以看下,
    蓝天小家
    是我的博客,等考研结束了我再回来更一次,祝自己好运,也祝各位看到的小伙伴技术能力蒸蒸日上,最后附上自己的荣誉证书,和各位共勉!
    在这里插入图片描述

    展开全文
  • 17 张程序员壁纸推荐

    千次阅读 2019-06-25 09:58:53
    转载:17 张程序员壁纸推荐 1、三思后再写代码!!! 2、从世界上搜索喜欢你的人!!! 其他分辨率下载: 1920x1080 1920x1200 2560x1440 2560x1600 3、代码没写完,哪里有脸睡觉!!! 其他分辨率及 ...

    转载:17 张程序员壁纸推荐

    1、三思后再写代码!!!

    ä¸æååå代ç ï¼ï¼ï¼

    2、从世界上搜索喜欢你的人!!!

    ä¸æååå代ç ï¼ï¼ï¼

    其他分辨率下载:

    3、代码没写完,哪里有脸睡觉!!!

    代ç æ²¡åå®ï¼åªéæè¸ç¡è§ï¼ï¼ï¼

    其他分辨率及 PSD 文件下载:http://static.runoob.com/download/mlcf.zip

    4、程序员的 Home 键!!!

    ç¨åºåç Home é®ï¼ï¼ï¼

    5、编程是一门艺术!!!

    编程是一门艺术!!!

    6、云 ~~~~ 雨!!!

    编程是一门艺术!!!

    7、程序人生!!!

    程序人生!!!

    8、只有极客才懂!!!

    程序人生!!!

    9、黑客的世界!!!

    10、黑~~~人!!!

    11、PHP 专属!!!

    12、程序 ~ 代码!!!

    13、我就是一个极客!!!

    14、CODE!!!

    15、源代码!!!

    16、CODE PARTICLE!!!

    17、一个While 引发的人生故事!!!

    展开全文
  • 1、三思后再写代码!!! 2、从世界上搜索喜欢你的人!!! 3、代码没写完,哪里有脸睡觉!!! 其他分辨率及 PSD 文件下载:http://static.runoob.com/download/mlcf.zip 4、程序员的 Home 键!!! 5...

    电脑客户端,后有手机端

     

    1、三思后再写代码!!!

    2、从世界上搜索喜欢你的人!!!

    3、代码没写完,哪里有脸睡觉!!!

    其他分辨率及 PSD 文件下载:http://static.runoob.com/download/mlcf.zip

    4、程序员的 Home 键!!!

    5、编程是一门艺术!!!

    6、云 ~~~~ 雨!!!

    7、程序人生!!!

    8、只有极客才懂!!!

    9、黑客的世界!!!

    10、黑~~~人!!!

    11、PHP 专属!!!

    12、程序 ~ 代码!!!

    13、我就是一个极客!!!

    14、CODE!!!

    15、源代码!!!

    16、CODE PARTICLE!!!

    17、一个While 引发的人生故事!!!

    程序员专属手机壁纸来了

    先来看看效果图(是不是有点酷):

    程序媛妹子专用

    人生如 While

    CODE PARTICLE!!!

    程序 ~ 代码!!!

    PHP 专属!!!

    我是极客,我喂自己袋盐

    黑客的世界!!!

    只有极客才懂!!!

    10010010

    编程是一门艺术!!!

    Home 键!!!

    每天睡前看一看

    三思再Coding!!!

    找一个喜欢你的人?

     

     

    最后


    如果你想要学习Java的话,我给你分享一些Java的学习资料,你不用浪费时间到处搜了,从Java入门到精通的资料我都给你整理好了,一共6000G,这些资料都是我做Java这几年整理的Java最新学习路线,Java笔试题,Java面试题,Java零基础到精通视频课程,Java开发工具,Java练手项目,Java电子书,Java学习笔记,PDF文档教程,Java程序员面经,Java求职简历模板等,这些资料对你接下来学习Java一定会带来非常大的帮助,每个Java初学者都必备,请你进我的Java技术QQ交流群:127522921自行下载,所有资料都在群文件里,进去要跟大家多交流学习哦。

    展开全文
  • 我那时候是纯黑色的壁纸,视频也刚好播放到白色衣服人物在黑夜中的画面,加上若隐若现的应用程序图标,这虚实结合的效果使得画面中的人物变得立体起来了!甚至有一种身临其境的感觉! 我当时就觉得,哇这种效果好棒...

    效果演示

    先放几张效果图:

    preview

    preview

    preview

    哈哈哈,还可以吧?


    诞生背景

    去年在新电脑上看视频的时候,在触摸板上做了一个缩放的手势把程序列表call出来了:

    preview

    我那时候是纯黑色的壁纸,视频也刚好播放到白色衣服人物在黑夜中的画面,加上若隐若现的应用程序图标,这虚实结合的效果使得画面中的人物变得立体起来了!甚至有一种身临其境的感觉!

    preview

    我当时就觉得,哇这种效果好棒啊,就像在播放透明背景的视频一样。记得那时候还在鸿神的群里讨论了一下关于播放透明视频的话题,后面有群友提到Android Studio就有个自带的设置透明背景图的功能。

    第二天,好奇的我想知道Android Studio它是怎么做到把窗口下所有组件都设置成半透明并且把图片放进去的。。。


    原理探索(同样适用研究其他功能)

    这一节需要用到IntelliJ IDEA来进行debug,以Android Studio作为这次debug的目标,没有Android Studio的同学,用JetBrains系列的其他IDE也可以,都有这个功能的。

    先把IDE对应版本的源码下载下来,看下AS的Build Version:

    preview

    可以看到是202.7660开头的,然后在 JetBrains/intellij-community 这里找到对应的idea版本:

    preview

    下载、解压(我一般会把它重命名为source并放到IDEA的根目录下)。

    接着打开IDEA,创建一个Plugin项目(这一步网上教程多如牛毛,这里就不赘述了),在Project Structure -> SDKs里把刚刚解压的源码关联上;

    关联源码之后,到Run/Debug Configuration里新建一个Remote JVM Debug的configuration:

    preview

    先不要关闭页面,打开Android Studio程序目录下的bin文件夹,找到studio.sh(Windows系统的是studio.bat),复制一份,可以命名为debug.sh(Windows系统保留bat后缀),然后编辑:
    在文件的末尾,Run the IDE注释的下面会有"$JAVA_BIN"(Windows系统是"%JAVA_EXE%")字眼的:

    preview

    在它后面加上刚刚新建的configuration里面的一串命令行参数,比如我的是:-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
    像这样:

    preview

    编辑之后保存,顺便保存刚刚新建的configuration。


    好了,可以正式开始了,现在从终端里运行刚刚修改过的debug.sh,然后打开Settings -> Appearance & Behavior -> Appearance

    preview

    看到那个Background Image按钮没有?我们现在就要debug它的鼠标事件,以拿到这个按钮的对象,然后把它的listener扒出来。
    现在可以回到IDEA这边,点一下那个绿色的小虫子,attach到AS进程了,成功之后会弹出debug控制台窗口并有以下字眼:

    preview

    没有就是没成功,请重试上面的步骤。

    成功attach之后,开始打断点。
    随便新建一个类,在里面输入java.awt.Component,然后点开它的源码并找到processMouseEvent(MouseEvent e)这个方法:

    preview

    在它调用listener.mousePressed方法那一行打个断点(监听鼠标按下)。接着回到AS窗口,鼠标点一下那个Background Image按钮:

    preview

    会看到已经断点成功了,当前点击的Component是个JButton,熟悉Java Swing的同学会知道,这个JButton继承自JComponent,而JComponent里有个listenerList,里面的数组是专门存放EventListener的,看一下这个JButton设置了哪些listener:

    preview

    看第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:

    preview

    随便设置一张图片,然后点OK:

    preview

    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件事:

    1. 保存最近使用过的图片路径;
    2. 获取当前图片路径和一些设置相关的信息;
    3. 应用当前设置的图片;
    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方法中打个断点:

    preview

    注意到这个value的格式没有?
    它完整的格式其实是这样的:
    图片绝对路径,透明度,绘制方式,基准点,翻转(可选项)

    透明度的取值范围是0~100
    图片的绘制方式有:plain(原始尺寸)、scale(缩放到合适的大小)、tile(平铺);
    基准点top-lefttop-centertop-rightmiddle-leftcentermiddle-rightbottom-leftbottom-centerbottom-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:

    <actions>
        <action id="SetBackgroundAction" class="SetBackgroundAction" text="Set Background">
            <add-to-group group-id="ViewMenu" anchor="last"/>
        </action>
    </actions>
    

    id可以随便设置,保证跟其他Action不冲突就行;
    class就是Action的完整类名(这个Action我没有放在任何一个package下所以这里看上去只有一个类名);
    text:显示的文本;
    add-to-group这一行表示我们把这个按钮放在菜单栏View的底部。

    好,编译运行看一下效果:

    preview

    点击这个选项,会发现背景图已经成功替换成Action里面指向的图片了。

    现在,想让背景动起来非常简单,只需要周期性地更换图片的路径就行。不过当你真的这样做的时候,你会发现调用repaintAllWindows重绘界面会非常非常的慢,就跟播放PPT一样!
    也许我们不应该直接使用这种方法来做动态背景效果,还是再研究一下它具体是怎么实现绘制背景图的吧,看看能不能找到粒度更小的实现方式。


    再探原理

    现在已经知道背景图相关信息是通过PropertiesComponent.setValue方法来保存到本地。有set肯定还有get,我们干脆就在getValue方法打个断点,分析下调用的源头:

    preview

    直接往下面看,它是在JComponent进行绘制的时候调用的,JComponent在绘制之前,会先通过getComponentGraphics方法来获取Graphics对象(这个Graphics可以理解为Android中的Canvas,是用来绘制各种图形的),可以看到当前断点的对象(IdeStatusBarImpl)重写了这个getComponentGraphics方法,来看看它是怎么实现的:

    @Override
    protected Graphics getComponentGraphics(Graphics g) {
      return JBSwingUtilities.runGlobalCGTransform(this, super.getComponentGraphics(g));
    }
    

    很简单,就调用了JBSwingUtilities的静态方法runGlobalCGTransform,这个方法会返回一个Graphics,看下代码:

    public final class JBSwingUtilities {
    
      private static final List<BiFunction> ourGlobalTransform = new CopyOnWriteArrayList<>(Collections.emptyList());
    
      public static Disposable addGlobalCGTransform(BiFunction fun) {
        ourGlobalTransform.add(fun);
      }
    
      public static Graphics2D runGlobalCGTransform(JComponent c, Graphics g) {
        Graphics2D gg = (Graphics2D)g;
        for (BiFunction transform : ourGlobalTransform) {
          gg = transform.apply(c, gg);
        }
        return gg;
      }
    }
    

    噢,原来它是遍历一个叫ourGlobalTransform的list,并调用这个list中所有BiFunction的apply方法。
    注意看,它每次调用apply方法都会把本地变量gg这个Graphics2D对象传进去,同时apply方法又会返回一个Graphics2D对象,又赋值给了gg,看样子这就是在给Graphics2D打包装(装饰器模式 Decorator Pattern)。
    现在关键点就到了这个ourGlobalTransform里的BiFunction实例是怎么来的,全局搜一下看看有哪些地方调用了addGlobalCGTransform方法:

    preview

    太好了,就只有三个地方调用,而且都是在同一个类里面,能省不少力气。
    看第一个地方,它居然在类加载的时候就调用addGlobalCGTransform方法了,还把一个新的MyTransform实例传了进去,这个MyTransform是IdeBackgroundUtil的静态内部类:

    public final class IdeBackgroundUtil {
    
      static {
        JBSwingUtilities.addGlobalCGTransform(new MyTransform());
      }
    
      private static final class MyTransform implements BiFunction<JComponent, Graphics2D, Graphics2D> {
    
        @Override
        public Graphics2D apply(JComponent c, Graphics2D g) {
    
          .......
    
          String type = getComponentType(c);
          if (type == null) return g;
    
          .......
    
          Graphics2D gg = withEditorBackground(g, c);
    
          .......                     
    
          return gg;
        }
      }
    }
    

    它实现的apply方法也没有想象中的复杂,先是调用了getComponentType,判断返回值是否为null,如果是null的话就直接返回传进来的Graphics2D对象,不做任何包装。
    不为null就调用IdeBackgroundUtil的静态方法withEditorBackground,并返回这个方法的返回值。

    好,先看看getComponentType方法吧,看看什么情况下会返回null:

    private static @Nullable String getComponentType(JComponent component) {
      return component instanceof JTree ? "tree" :
             component instanceof JList ? "list" :
             component instanceof JTable ? "table" :
             component instanceof JViewport ? "viewport" :
             component instanceof JTabbedPane ? "tabs" :
             component instanceof JButton ? "button" :
             component instanceof ActionToolbar ? "toolbar" :
             component instanceof StatusBar ? "statusbar" :
             component instanceof JMenuBar || component instanceof JMenu? "menubar" :
             component instanceof Stripe ? "stripe" :
             component instanceof EditorsSplitters ? "frame" :
             component instanceof EditorComponentImpl ? "editor" :
             component instanceof EditorGutterComponentEx ? "editor" :
             component instanceof JBLoadingPanel ? "loading" :
             component instanceof JBTabs ? "tabs" :
             component instanceof ToolWindowHeader ? "title" :
             component instanceof JBPanelWithEmptyText ? "panel" :
             component instanceof JPanel && isKnownName(component.getName()) ? component.getName() :
             null;
    }
    

    只要当前的JComponent属于上面列出的这些类的实例,就不为null,就能进一步调用withEditorBackground方法。而现在列出来的component已经涵盖绝大部分的基础组件了。
    再来看看withEditorBackground方法:

    public final class IdeBackgroundUtil {
    
      public static @NotNull Graphics2D withEditorBackground(@NotNull Graphics g, @NotNull JComponent component) {
        return withNamedPainters(g, EDITOR_PROP, component);
      }
    
      private static Graphics2D withNamedPainters(Graphics g, String paintersName, JComponent component) {
        JRootPane rootPane = component.getRootPane();
        Component glassPane = rootPane == null ? null : rootPane.getGlassPane();
        PaintersHelper helper = glassPane instanceof IdeGlassPaneImpl? ((IdeGlassPaneImpl)glassPane).getNamedPainters(paintersName) : null;
        if (helper == null || !helper.needsRepaint()) return (Graphics2D)g;
        return MyGraphics.wrap(g, helper, component);
      }
    }
    

    它是直接调用了withNamedPainters方法,注意看,withNamedPainters里有好几层判断,先整理一下,变成以下代码:

    private static Graphics2D withNamedPainters(Graphics g, String paintersName, JComponent component) {
        JRootPane rootPane = component.getRootPane();
        if (rootPane != null) {
            Component glassPane = rootPane.getGlassPane();
            if (glassPane instanceof IdeGlassPaneImpl) {
                PaintersHelper helper = glassPane.getNamedPainters(paintersName);
                if (helper != null && helper.needsRepaint()) {
                    return MyGraphics.wrap(g, helper, component);
                }
            }
        }
        return (Graphics2D) g;
    }
    

    它一定要:

    • Component的RootPane不为null;
    • RootPane里面的GlassPane不为null;
    • GlassPane是IdeGlassPaneImpl的实例;
    • GlassPane里面有对应名字的PaintersHelper对象;
    • 对应名字的PaintersHelper对象的needsRepaint方法返回true

    符合以上所有条件,才会对传进来的Graphics进行包装。
    看它第一个return,它调用的是MyGraphics.wrap方法,这个方法会返回一个做过手脚的Graphics对象,继承自Graphics2DDelegate:

    public class Graphics2DDelegate extends Graphics2D{
      protected final Graphics2D myDelegate;
    
      public Graphics2DDelegate(Graphics2D g2d){
        myDelegate=g2d;
      }
    
      @Override
      public void fillRect(int x, int y, int width, int height) {
        myDelegate.fillRect(x, y, width, height);
      }
    
      @Override
      public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
        myDelegate.fillArc(x, y, width, height, startAngle, arcAngle);
      }
    
      @Override
      public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) {
        myDelegate.drawImage(img, op, x, y);
      }
    
      ......
      ......
    
    }
    

    看到没有,这就是很典型的静态代理写法。MyGraphics也重写了Graphics2DDelegate所有重写的方法:

    private static final class MyGraphics extends Graphics2DDelegate {
    
        static Graphics2D wrap(Graphics g, PaintersHelper helper, JComponent component) {
          MyGraphics gg = g instanceof MyGraphics ? (MyGraphics)g : null;
          return new MyGraphics(gg != null ? gg.myDelegate : g, helper, helper.computeOffsets(g, component), gg != null ? gg.preserved : null);
        }
    
        @Override
        public void fillRect(int x, int y, int width, int height) {
          super.fillRect(x, y, width, height);
          runAllPainters(x, y, width, height, null, getColor());
        }
    
        @Override
        public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
          super.fillArc(x, y, width, height, startAngle, arcAngle);
          runAllPainters(x, y, width, height, new Arc2D.Double(x, y, width, height, startAngle, arcAngle, Arc2D.PIE), getColor());
        }
    
        @Override
        public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) {
          super.drawImage(img, op, x, y);
          runAllPainters(x, y, img.getWidth(), img.getHeight(), null, img);
        }
    
        ......
        ......
    
        final PaintersHelper helper;
    
        void runAllPainters(int x, int y, int width, int height, @Nullable Shape sourceShape, @Nullable Object reason) {
          setClip(tmpClip);
          helper.runAllPainters(myDelegate, offsets);
          setClip(prevClip);
        }
    
    }
    

    相比Graphics2DDelegate,MyGraphics还多了一个runAllPainters方法,而且在每个重写的方法里都会调用它。
    这个runAllPainters里面又会调用PaintersHelperrunAllPainters方法,并将原始的Graphics对象传了进去,看下代码:

    final class PaintersHelper implements Painter.Listener {
    
      private final Set<Painter> painters = new LinkedHashSet<>();
    
      void addPainter(@NotNull Painter painter, @Nullable Component component) {
        painters.add(painter);
        ......
      }
    
      void runAllPainters(Graphics gg, @Nullable Offsets offsets) {
        ......
        Graphics2D g = (Graphics2D)gg;
        for (Painter painter : painters) {
            ......
           if (painter.needsRepaint()) {
             painter.paint(cur, g);
          }
        }
      }
    }
    

    PaintersHelper中有个成员变量painters,它是一个LinkedHashSet。
    注意看,在runAllPainters方法中,会遍历这个painters,如果painters中的元素的needsRepaint方法返回true的话,就会进一步调用它的paint方法!提醒一下! 当外面的Component调用这个被做过手脚的Graphics(MyGraphics)对象的任何一个绘制方法时,都会走到这里!

    PaintersHelper里还有一个addPainter方法,用来添加实现了Painter接口的对象实例。也就是说,如果我们能获取到对应的PaintersHelper实例,就能直接通过它的addPainter方法把自己实现的Painter添加进去,就能监听到对应Component的每一次重绘!!!

    至于这个PaintersHelper实例在哪里,要怎么获取,其实答案就藏在前面的代码里,现在往上翻,找到分析withNamedPainters方法那一段代码:

    private static Graphics2D withNamedPainters(Graphics g, String paintersName, JComponent component) {
        JRootPane rootPane = component.getRootPane();
        if (rootPane != null) {
            Component glassPane = rootPane.getGlassPane();
            if (glassPane instanceof IdeGlassPaneImpl) {
                PaintersHelper helper = glassPane.getNamedPainters(paintersName);
                ......
            }
        }
        ......
    }
    

    看到了没有,PaintersHelper就放在Window(component.getRootPane()其实获取到的是所在JFrame(JFrame就是Window)的RootPane实例)的RootPane的GlassPane里面!!!而Window的实例我们可以轻易拿到。

    好了,监听到Component重绘之后,具体要怎么做才能将带有透明度的背景图绘制出来呢?
    上面说到了Painter接口的两个重要的方法:needsRepaintpaint,看看在哪里实现了Painter接口:

    preview

    总共有四个地方,但仔细一看,后面三个都是在同一个文件里,而且看它的名字是跟分割线有关,应该不是我们要找的。
    排第一的AbstractPainter,是一个抽象类:

    public abstract class AbstractPainter implements Painter {
    
      ......
      ......
    
      @Override
      public final void paint(final Component component, final Graphics2D g) {
        myNeedsRepaint = false;
        executePaint(component, g);
      }
    
      public abstract void executePaint(final Component component, final Graphics2D g);
    }
    

    它实现了paint方法并标记为final,在里面调用了抽象方法executePaint,所以如果我们要实现它的话,只重写它的needsRepaintexecutePaint方法即可。
    来看下现在有哪些类继承了它:

    preview

    看第八条结果,是PaintersHelper的抽象静态内部类。点进去会发现,有一个叫MyImagePainter的类继承了它并重写了needsRepaintexecutePaint方法:

    private static final class MyImagePainter extends ImagePainter {
    
        ......
        
        @Override
        public boolean needsRepaint() {
          return ensureImageLoaded();
        }
    
        boolean ensureImageLoaded() {
          IdeFrame frame = ComponentUtil.getParentOfType(IdeFrame.class, rootComponent);
          Project project = frame == null ? null : frame.getProject();
          String value = IdeBackgroundUtil.getBackgroundSpec(project, propertyName);
          if (!Objects.equals(value, current)) {
            current = value;
            loadImageAsync(value);
            // keep the current image for a while
          }
          return image != null;
        }
    }
    

    先看needsRepaint,前面说过,如果它返回true,PaintersHelper就会进一步调用paint方法。
    MyImagePainter的实现是直接返回ensureImageLoaded方法的结果,这个方法会先判断是否有设置图片并且已经加载完成,如果没有设置图片那肯定返回false,有设置图片但还没加载的话,会【异步加载图片】并返回false,只有在图片加载成功之后才是true
    loadImageAsync的代码有点多,就不贴不出来了,主要是对那个value的格式:图片绝对路径,透明度,绘制方式,基准点,翻转 进行解析,加载图片并对图片进行对应的变换操作。

    好,最后就到executePaint了,也是最重要的一个方法:

    private static final class MyImagePainter extends ImagePainter {
    
        private Image image;
    
        ......
    
        @Override
        public void executePaint(Component component, Graphics2D g) {
          if (image == null) {
            // covered by needsRepaint()
            return;
          }
          executePaint(g, component, image, fillType, anchor, alpha, insets);
        }
    }
    

    它这里的做法是,如果image不为null则调用另一个executePaint,这方法有点长,但逻辑并不复杂:

    private static final class MyImagePainter extends ImagePainter {
    
        final Map<GraphicsConfiguration, Cached> cachedMap = new HashMap<>();
    
        void executePaint(Graphics2D g, Component component, Image image, IdeBackgroundUtil.Fill fillType, IdeBackgroundUtil.Anchor anchor, float alpha, Insets insets) {
          ......
          Cached cached = cachedMap.get(cfg);
          // 从缓存中取出(如果有的话)
          VolatileImage scaled = cached == null ? null : cached.image;
          ......
          int sw0 = scaled == null ? -1 : scaled.getWidth(null);
          int sh0 = scaled == null ? -1 : scaled.getHeight(null);
          // 如果图片未缓存过,或者窗口尺寸有变更,则需要加载
          boolean repaint = cached == null || !cached.src.equals(src0) || !cached.dst.equals(dst0);
          while ((scaled = validateImage(cfg, scaled)) == null || repaint) {
            int sw = Math.min(cw, dst0.width);
            int sh = Math.min(ch, dst0.height);
            if (scaled == null || sw0 < sw || sh0 < sh) {
              // 当前图片已变更,则重新加载图片并缩放到指定的尺寸
              scaled = createImage(cfg, sw, sh);
              // 缓存起来
              cachedMap.put(cfg, cached = new Cached(scaled, src0, dst0));
            } else {
              // 图片没变,只是窗口尺寸有变化的话,则直接刷新缓存的尺寸
              cached.src.setBounds(src0);
              cached.dst.setBounds(dst0);
            }
            Graphics2D gg = scaled.createGraphics();
            // 混合模式为SRC
            gg.setComposite(AlphaComposite.Src);
            // 图片绘制方式为 缩放模式
            if (fillType == IdeBackgroundUtil.Fill.SCALE) {
              // 使用双线性插值法来处理图片缩放
              gg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
              // draw到硬件加速的VolatileImage上
              StartupUiUtil.drawImage(gg, image, dst0, src0, null);
            } 
            // 图片绘制方式为 平铺模式
            else if (fillType == IdeBackgroundUtil.Fill.TILE) {
              Rectangle r = new Rectangle(0, 0, 0, 0);
              for (int x = 0; x < dst0.width; x += w) {
                for (int y = 0; y < dst0.height; y += h) {
                  r.setBounds(dst0.x + x, dst0.y + y, src0.width, src0.height);
                  // 反复绘制,直到记录的尺寸大于目标尺寸为止
                  StartupUiUtil.drawImage(gg, image, r, src0, null);
                }
              }
            } 
            // 原尺寸绘制
            else {
              // 直接draw到硬件加速的VolatileImage上,不需任何多余的操作
              StartupUiUtil.drawImage(gg, image, dst0, src0, null);
            }
            gg.dispose();
            repaint = false;
          }
    
          // 设置透明度
          GraphicsConfig gc = new GraphicsConfig(g).setAlpha(adjustedAlpha);
          // 把图片draw出来
          StartupUiUtil.drawImage(g, scaled, dst, src, null, null);
          gc.restore();
        }
    
    }
    

    它大概只做了三件事:

    1. 获取上一次缓存过的image
    2. 如果缓存为空,或者组件的当前尺寸有变更,则重新根据绘制模式(平铺、缩放)将图片绘制到支持硬件加速的VolatileImage上;
    3. 把VolatileImage的内容draw到Graphics上面;

    有同学可能会问:为什么不把加载好的图片直接绘制到Graphics上,而非要在中间加一个VolatileImage呢?,这不是多此一举嘛?
    其实不是的,这样做虽然看起来是绘制了两次,影响效率,但实际上要比直接绘制一次快得多,因为上面也强调了是支持硬件加速的VolatileImage。不信的话,等下我们可以来测试一下。


    好,现在具体的绘制方法我们也已经知道了,最后来看下这个MyImagePainter是怎么生效的吧:

    final class PaintersHelper implements Painter.Listener {
    
      private final Set<Painter> painters = new LinkedHashSet<>();
    
      void addPainter(@NotNull Painter painter, @Nullable Component component) {
        painters.add(painter);
        painterToComponent.put(painter, component == null ? rootComponent : component);
        painter.addListener(this);
      }
    
      static void initWallpaperPainter(@NotNull String propertyName, @NotNull PaintersHelper painters) {
        painters.addPainter(new MyImagePainter(painters.rootComponent, propertyName), null);
      }
    }
    

    在PaintersHelper的静态方法initWallpaperPainter里会看到,它是通过前面说到的addPainter方法来添加一个新的MyImagePainter实例。
    这个initWallpaperPainter是在IdeBackgroundUtil的initEditorPainters里面调用:

    public final class IdeBackgroundUtil {
    
      ......
    
      static void initEditorPainters(@NotNull IdeGlassPaneImpl glassPane) {
        PaintersHelper.initWallpaperPainter(EDITOR_PROP, glassPane.getNamedPainters(EDITOR_PROP));
      }
    }
    

    再上一层:

    public class IdeGlassPaneImpl extends JPanel implements IdeGlassPaneEx, IdeEventQueue.EventDispatcher {
     
      ......
     
      public IdeGlassPaneImpl(JRootPane rootPane, boolean installPainters) {
        ......
        if (installPainters) {
          IdeBackgroundUtil.initEditorPainters(this);
        }
        ......
      }
    }
    

    看到没有,GlassPane!!!
    当这个IdeGlassPaneImpl初始化时,如果参数installPainters为true的话,就会调用IdeBackgroundUtil.initEditorPainters最终把MyImagePainter的实例添加进PaintersHelper中监听Components的绘制!!!
    这就刚好跟我们前面分析的【Window、RootPane、GlassPane、PaintersHelper】的关系对应上了!!!

    好了,终于分析完了。。。
    喘口气,来把整个流程捋一下:

    • Component在进行绘制时,会通过getComponentGraphics方法来获取Graphics对象(切入点就在这里);

    • IDEA在各个相关容器里都重写了getComponentGraphics方法,并返回了【通过JBSwingUtilities.runGlobalCGTransform获取到的一个做过手脚的Graphics对象】;

    • 这个做过手脚的Graphics对象的实例是IdeBackgroundUtil的静态内部类MyGraphics,它重写了Graphics所有绘制相关的方法,并在每一个绘制方法里面都调用PaintersHelper.runAllPainters

    • PaintersHelper里面有一个集合专门存放Painter(Painter可以理解为用来监听绘制的Callback),runAllPainters方法会遍历这个集合,回调每一个Painter的paint方法(如果它的needsRepaint返回true的话);

    • IDEA自带的设置透明背景图功能,就是在IdeGlassPaneImpl初始化的时候,将自定义的Painter添加进PaintersHelper来监听Component的绘制,当Component进行绘制时,就把设置的背景图画出来;

    大概就是这样。
    那接下来可以正式动手写代码了。


    动态背景

    我们大致的思路是:
    借助现成的(毕竟这个还自己搞的话,不现实)javacv来把视频每一帧图片解析出来,然后根据视频帧率将这些图片绘制到背景板上。

    先到 https://github.com/bytedeco/javacv/releases 把最新的 javacv-platform-[version]-bin.zip 下载下来。
    接下来需要用到的jar包有:

    • ffmpeg.jar
      ffmpeg-linux-x86_64.jar
      ffmpeg-macosx-x86_64.jar
      ffmpeg-windows-x86_64.jar
    • javacpp.jar
      javacpp-linux-x86_64.jar
      javacpp-macosx-x86_64.jar
      javacpp-windows-x86_64.jar
    • javacv.jar

    后面带平台字眼的都是动态链接库,但因为现在只是在本机上测试,暂时不需要考虑跨平台,动态链接库的jar可以只取适合自己平台的,比如我的是64位的linux系统,所以只复制带linux-x86_64字眼的即可:

    preview

    复制进项目之后,到Project Structure -> Libraries里加上依赖,这个不必多说:

    preview

    OK,现在可以正常使用这些jar包了。


    先把解析视频帧这一块弄好吧。

    其实刚刚下载下来的那个javacv压缩包里就有示例代码,在samples/JavaFxPlayVideoAndAudio.java里面。

    • 我们用到javacv的类有:FFmpegFrameGrabber(提取视频帧)、Java2DFrameConverter(转换视频帧);
    • 提取和转换视频帧不可能在主线程中完成的,这种情况当然是把它放在Runnable里,然后扔进线程池最好了;
    • 我们还需要一个阻塞队列,用来缓存转换之后的图片;

    好,来看下代码怎么写:

    object MediaPlayer {
    
        @Volatile
        private var playing = false
        private val threadPool = Executors.newCachedThreadPool()
    
        private var frameGrabber: FFmpegFrameGrabber? = null
    
        fun init(url: String) {
            frameGrabber = FFmpegFrameGrabber.createDefault(url)
        }
    
        fun start() {
            frameGrabber?.let { grabber ->
                grabber.imageWidth = rootPane.width
                grabber.imageHeight = rootPane.height
                grabber.start()
                playing = true
                threadPool.execute(frameGrabTask)
            }
        }
    }
    

    frameGrabber就是用来提取视频帧的,可以看到它在init方法里面初始化,在start方法中,先是指定了视频帧尺寸为当前窗口大小,然后调用了frameGrabberstart方法,最后把frameGrabTask扔进了线程池里,看下这个frameGrabTask的代码:

    object MediaPlayer {
    
        ......
        private val imageQueue = LinkedBlockingDeque<BufferedImage>(32)
    
        private val frameGrabTask = Runnable {
            val imageConverter = Java2DFrameConverter()
            while (playing) {
                frameGrabber?.grab()?.also { frame ->
                    frame.image?.let {
                        imageConverter.getBufferedImage(frame)?.let {
                            imageQueue.put(it)
                        }
                    }
                }
            }
        }
    }
    

    大致跟javacv samples/JavaFxPlayVideoAndAudio.java里面的流程一样,都是先通过frameGrabbergrab方法获取到视频帧数据,再通过Java2DFrameConverter来转换成图片,然后put到阻塞队列imageQueue里面。
    阻塞队列的容量我们指定为32个(数值太大会消耗很多内存),也就是最多只缓存32帧,如果队列满了,就会自动让提取视频帧的线程阻塞。

    好,现在视频的每一帧图片都已经拿到了,来想想怎么把它们draw到背景板上:
    要知道,Component的paint方法,在同一次重绘流程中,可能不止被调用一次的,如果在重写的AbstractPainter.executePaint里直接从缓存队列中取出图片然后绘制,那缓存队列里的图片就会很快被清空,一旦队列空了,线程就会阻塞,而且这还是在主线程里阻塞,后果非常严重啊!!!
    所以还需要有一个独立的BufferedImage,用来表示当前帧,这样的话,就算executePaint多次被调用,也不会提前把下一帧都消耗掉,也不会因为缓存队列为空而造成主线程阻塞。

    可问题又来了:这个独立的BufferedImage,什么时候刷新呢(何时开始获取下一帧)?
    不是有线程池嘛?多开一个线程,专门负责刷新视频帧就行了,刷新周期可以根据视频帧率算出。
    当然了,还需要有一个专门负责通知重绘的线程,以保证重绘周期的稳定性。
    看下代码怎么写:

    object MediaPlayer {
    
        ......
        private lateinit var rootPane: JRootPane
        private var frameImage: BufferedImage? = null
        private var repaintInterval = 0L
    
        private val imageProcessTask = Runnable {
            while (playing) {
                imageQueue.take().let {
                    frameImage = it
                    Thread.sleep(repaintInterval)
                }
            }
        }
    
        private val repaintTask = Runnable {
            while (playing) {
                EventQueue.invokeLater {
                    rootPane.repaint()
                }
                Thread.sleep(repaintInterval)
            }
        }
    
    

    可以看到,除了刚刚说的表示当前帧的frameImage,还多了个rootPanerepaintInterval
    rootPane就是当前Window的RootPane,主要用来发起重绘。还记得不?前面分析【设置背景图功能】的时候,IDEA是通过IdeBackgroundUtil.repaintAllWindows()来重绘所有Window的,但是这个方法太重量级了,通常我们播放视频的话,只需要显示在其中一个Window上就行了,所以就用rootPane.repaint()来代替。
    repaintInterval就是 1000 / 视频帧率 得出每一帧的间隔时间(ms)。

    Runnable创建了之后,还要在start方法里面启动,改一下start方法的代码:

        fun start() {
            frameGrabber?.let { grabber ->
                grabber.imageWidth = rootPane.width
                grabber.imageHeight = rootPane.height
                grabber.start()
                repaintInterval = (1000 / grabber.frameRate).toLong()
                playing = true
                threadPool.execute(frameGrabTask)
                threadPool.execute(imageProcessTask)
                threadPool.execute(repaintTask)
            }
       }
    

    好,那现在就剩下负责绘制的Painter了。
    不过我们首先要把addPainter的调用封装好,因为这个addPainter不能直接访问到,只能通过反射来调用。

    就按照前面分析过的流程,获取Window(JFrame)的GlassPane里面的PaintersHelper实例。
    改一下init方法,加上JFrame参数:

        fun init(window: JFrame, url: String) {
            // 清除当前已设置的背景
            PropertiesComponent.getInstance().setValue("idea.background.editor", null)
            // 注入自定义Painter
            injectPainter(window)
    
            frameGrabber = FFmpegFrameGrabber.createDefault(url)
        }
    

    嗯,主要代码都在injectPainter方法里面:

    object MediaPlayer {
    
        ......
    
        private fun injectPainter(window: JFrame) {
            rootPane = window.rootPane.apply {
                val glassPane = glassPane
                if (glassPane is IdeGlassPaneImpl) {
                    // 先获取IdeGlassPaneImpl的getNamedPainters方法
                    val getNamedPaintersMethod = glassPane::class.java.getDeclaredMethod(
                            "getNamedPainters", String::class.java).apply { isAccessible = true }
                    // 通过反射调用,拿到对应的PaintersHelper对象
                    val paintersHelper = getNamedPaintersMethod.invoke(glassPane, "idea.background.editor")
    
                    // 获取PaintersHelper的addPainter方法
                    val addPainterMethod = paintersHelper::class.java.getDeclaredMethod(
                            "addPainter", com.intellij.openapi.ui.Painter::class.java, java.awt.Component::class.java)
                            .apply { isAccessible = true }
                    // 反射调用,把自己实现的Painter添加进去,最后一个参数是executePaint方法的glassPane
                    addPainterMethod.invoke(paintersHelper, painter, null)
                }
            }
        }
    }
    

    跟前面分析的流程一样,不必多说。
    最后到painter

    object MediaPlayer {
    
        ......
        private var alphaComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .15F /* 85%透明度 */)
        private val painter = object : AbstractPainter() {
    
            // 只要未停止播放,都需要绘制
            override fun needsRepaint() = playing
    
            override fun executePaint(glassPane: Component, graphics: Graphics2D) {
                frameImage?.let {
                    // 先记下原来的composite
                    val oldComposite = graphics.composite
                    // 把当前composite换成带透明度的
                    graphics.composite = alphaComposite
                    
                    // 视频帧的尺寸
                    val imageBounds = Rectangle(0, 0, it.getWidth(null), it.getHeight(null))
                    // 把视频帧draw到graphics上,绘制范围指定为glassPane的尺寸大小,也就是充满整个窗口
                    StartupUiUtil.drawImage(graphics, it, glassPane.bounds, imageBounds, null)
                    
                    // 恢复原来的composite
                    graphics.composite = oldComposite
                }
            }
        } 
    }
    

    OK,现在播放器这边是完成了。
    接着创建一个Action,用来启动播放器:

    class PlayAction : AnAction() {
        override fun actionPerformed(event: AnActionEvent) {
            // 视频路径(温馨提醒:FFMPEG是支持网络路径的哦)
            val videoUrl = "/home/wuyr/Desktop/万恶淫为首.mp4"
            // 获取当前焦点所在窗口对象
            val frame = KeyboardFocusManager.getCurrentKeyboardFocusManager().activeWindow as JFrame
            // 初始化播放器
            MediaPlayer.init(frame, videoUrl)
            // 开始播放
            MediaPlayer.start()
        }
    }
    

    记得在plugin.xml里声明一下:

    <actions>
        ......
        <action id="PlayAction" class="PlayAction" text="Play Video">
            <add-to-group group-id="ViewMenu" anchor="last"/>
        </action>
    </actions>
    

    好!运行,看看效果(首次运行可能会提示内存不足(默认是512m),把内存限制调大点就行):

    preview

    救命!这也太卡了叭!!!
    我们播放的是1080p 60帧的视频,但卡成这个样子是完全不能接受的,还是不要偷懒,老老实实优化一下性能吧。


    性能优化

    首先把常规的BufferedImage换成VolatileImage,使其支持硬件加速:
    在代码里将frameImage的类型:

        private var frameImage: BufferedImage? = null
    

    改成VolatileImage:

        private var frameImage: VolatileImage? = null
        private var frameImageGraphics: Graphics2D? = null
    

    还加了一个frameImageGraphics,这个Graphics主要负责往VolatileImage里绘制内容。
    好,最后到负责更新frameImageimageProcessTask,改成以下这样:

        private val imageProcessTask = Runnable {
            while (playing) {
                imageQueue.take().let { image ->
                    frameImage ?: initFrameImage()
                    frameImageGraphics?.let { graphics ->
                        val imageBounds = Rectangle(0, 0, image.getWidth(null), image.getHeight(null))
                        StartupUiUtil.drawImage(graphics, image, rootPane.bounds, imageBounds, null)
                    }
                    image.flush()
                    Thread.sleep(repaintInterval)
                }
            }
        }
    

    大致逻辑就是:先从缓存队列中取出视频帧,如果frameImage没有初始化就先初始化,然后通过frameImageGraphics将视频帧的内容绘制到支持硬件加速的frameImage上,最后释放视频帧的资源,周而复始。
    重点来了,看下支持硬件加速的frameImage怎么初始化:

        private fun initFrameImage() {
            val config = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.defaultConfiguration
            frameImage = try {
                config.createCompatibleVolatileImage(rootPane.width, rootPane.height, ImageCapabilities(true), 3)
            } catch (e: Exception) {
                config.createCompatibleVolatileImage(rootPane.width, rootPane.height, 3)
            }.apply {
                validate(null)
                // 最高优先级别
                accelerationPriority = 1F
                frameImageGraphics = createGraphics().apply {
                    composite = AlphaComposite.Src
                    // 关闭防抖动
                    setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE)
                    // 性能优先
                    setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED)
                    setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED)
                    setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED)
                }
            }
        }
    

    这一段代码都是在前面分析过的源码里翻出来改造的,不多作解释。
    好,再次运行:

    preview

    看到没有,现在已经比优化前好多了。
    不过还不够,虽然现在画面看上去流畅了很多,但依然能感觉到小姐姐舞扇的动作没有原视频快,而且,当播放时间一长,画面刷新的频率还会时高时低。
    也就是说:

    1. 绘制视频帧的速度还是赶不上视频原来的帧率;
    2. 画面刷新的周期不稳定;

    第一个问题产生原因可能有多个,包括解析(解析慢了会间接导致消费线程阻塞)、(从缓存队列中)取出(队列空了会阻塞)、绘制等等;
    第二个问题,因为现在的repaintTask是通过 EventQueue.invokeLater 来切换到DispatchThread(忘记了的同学赶紧翻一下代码),并使用rootPanerepaint方法来发起重绘,invokeLater本身是异步回调的,即我们传进去的Runnable会经过排队之后才执行(对,就跟Android中的Handler.post一样),这样就很有可能会出现好几个重绘任务挤在一起执行的现象,也就是我们看到的画面时快时慢的效果。
    要解决这个问题很简单,我们把invokeLater改成invokeAndWait就行了,后者会在绘制任务被执行之前一直阻塞,强行把异步变成同步。
    还没完,来看下repaint方法的代码:

    public abstract class JComponent extends Container implements Serializable, HasGetTransferHandler {
    
        ......
    
        public void repaint() {
            this.repaint(0L, 0, 0, this.width, this.height);
        }
    
        public void repaint(long tm, int x, int y, int width, int height) {
            RepaintManager.currentManager(SunToolkit.targetToAppContext(this)).addDirtyRegion(this, x, y, width, height);
        }
    }
    

    Component的repaint(long, int, int, int, int)方法被JComponent重写了,在里面调用了RepaintManager的addDirtyRegion

    public class RepaintManager {
        public void addDirtyRegion(JComponent c, int x, int y, int w, int h) {
            RepaintManager delegate = this.getDelegate(c);
            if (delegate != null) {
                delegate.addDirtyRegion(c, x, y, w, h);
            } else {
                this.addDirtyRegion0(c, x, y, w, h);
            }
        }
    
        private void addDirtyRegion0(Container c, int x, int y, int w, int h) {
            ......
            ......
            synchronized(this) {
                ......
                this.dirtyComponents.put(c, new Rectangle(x, y, w, h));
            }
            this.scheduleProcessingRunnable(SunToolkit.targetToAppContext(c));
            ......
        }
    
        private void scheduleProcessingRunnable(AppContext context) {
            Toolkit tk = Toolkit.getDefaultToolkit();
            if (tk instanceof SunToolkit) {
                SunToolkit.getSystemEventQueueImplPP(context).postEvent(new InvocationEvent(Toolkit.getDefaultToolkit(), this.processingRunnable));
            } else {
                Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(new InvocationEvent(Toolkit.getDefaultToolkit(), this.processingRunnable));
            }
        }
    }
    

    addDirtyRegion方法中,如果RepaintManager没有设置代理对象的话(我们这里假设它没有),就会调用addDirtyRegion0addDirtyRegion0方法会先把目标Component的边界信息记录到dirtyComponents里面,然后调用了scheduleProcessingRunnable方法。
    看最后的scheduleProcessingRunnable方法,那个if无论走哪个分支,都会调用EventQueue的postEvent方法,再次提交一个任务:processingRunnable,它是内部类ProcessingRunnable的实例,来看下里面做了什么:

        private final class ProcessingRunnable implements Runnable {
    
            ......
    
            public void run() {
                ......
                RepaintManager.this.scheduleHeavyWeightPaints();
                RepaintManager.this.validateInvalidComponents();
                RepaintManager.this.prePaintDirtyRegions();
            }
        }
    

    嗯,它分别调用了RepaintManager的scheduleHeavyWeightPaintsvalidateInvalidComponentsprePaintDirtyRegions,直觉告诉我们prePaintDirtyRegions是处理重绘的关键方法,看一下:

        private void prePaintDirtyRegions() {
            ......
            this.paintDirtyRegions();
            ......
        }
    
        public void paintDirtyRegions() {
            ......
            this.paintDirtyRegions(this.tmpDirtyComponents);
        }
    

    它里面直接调用了paintDirtyRegionspaintDirtyRegions又调用了paintDirtyRegions(Map<Component, Rectangle>)

        private void paintDirtyRegions(final Map<Component, Rectangle> tmpDirtyComponents) {
            if (!tmpDirtyComponents.isEmpty()) {
                ......
                for (final int j = 0; j < count.get(); ++j) {
                    // 获取到需要重绘的Component
                    final Component dirtyComponent = (Component) roots.get(j);
                    if (dirtyComponent instanceof JComponent) {
                        // 如果是JComponent则直接调用他的paintImmediately方法来进行重绘
                        ((JComponent) dirtyComponent).paintImmediately(rect.x, rect.y, rect.width, rect.height);
                    } else if (dirtyComponent.isShowing()) {
                        // 如果Component可见,则调用其paint方法来进行重绘
                        Graphics g = JComponent.safelyGetGraphics(dirtyComponent, dirtyComponent);
                        if (g != null) {
                            g.setClip(rect.x, rect.y, rect.width, rect.height);
                            try {
                                dirtyComponent.paint(g);
                            } finally {
                                g.dispose();
                            }
                        }
                    }
                    ......
                }
                ......
                tmpDirtyComponents.clear();
            }
        }
    

    看到没有,Component的重绘,就是在这个方法里发起的。这也证实了我们刚刚的猜测是对的:ProcessingRunnable.run里面调用的prePaintDirtyRegions就是处理Component重绘的关键方法。

    来思考一下:
    既然我们调用的repaint方法最终都会走到ProcessingRunnable.run,那为什么不直接用反射拿到RepaintManager的这个ProcessingRunnable对象,然后在每次重绘的时候直接调用呢?这岂不是节省了postEvent这个环节了?
    还有一点,不只是我们调用的repaint方法,是所有发起重绘的操作最终都会来到ProcessingRunnable.run这暗示着什么? 这就说明,在视频播放过程中,完全可以忽略掉其他地方发起的重绘请求!!!因为我们的重绘任务是不间断地进行的!这样做可以给画面刷新减轻不少压力。

    好,现在来看一下刚刚repaint方法获取RepaintManager对象时调用的RepaintManager.currentManager

    public class RepaintManager {
    
        private static final Object repaintManagerKey = RepaintManager.class;
    
        static RepaintManager currentManager(AppContext appContext) {
            RepaintManager rm = (RepaintManager)appContext.get(repaintManagerKey);
            ......
            return rm;
        }
    }
    

    他是调用AppContext的get(Object key)方法来获取到RepaintManager对象的,参数传的就是RepaintManager.class
    AppContext其实还有个对应的put(Object key, Object value)方法,我们可以事先把做过手脚的RepaintManager对象通过put方法放进去!!这样其他地方在发起重绘时获取到的RepaintManager就是我们做过手脚的对象了!
    我们要怎么做手脚呢?
    很简单,重写刚刚分析过的paintDirtyRegions()方法(prePaintDirtyRegions()paintDirtyRegions(Map<Component, Rectangle>)都是private的,所以最合适是paintDirtyRegions()了),并在里面根据一个标识来控制调不调用super.paintDirtyRegions()(只要不调用父类方法,本次重绘就不会生效),达到过滤多余重绘请求的效果。

    好,捋一下思路:

    1. 先把公共的RepaintManager对象替换成自己做过手脚的RepaintManager;
    2. 通过反射获取RepaintManager的processingRunnable,用来直接处理重绘操作,绕过消息队列;
    3. 在视频播放过程中,忽略掉【除刷新视频帧任务外】的所有重绘请求;

    来看看代码怎么写:

        private lateinit var repaintRunnable: Runnable
        private lateinit var repaintManager: RepaintManager
        private var repaintBarrier = false
    
        private fun replaceRepaintManager() {
            val appContextClass = Class.forName("sun.awt.AppContext")
            val appContext = appContextClass.getMethod("getAppContext").invoke(null)
    
            val putMethod = appContextClass.getMethod("put", Any::class.java, Any::class.java)
            putMethod.invoke(appContext, RepaintManager::class.java, object : RepaintManager() {
                override fun paintDirtyRegions() {
                    if (!repaintBarrier || !playing) {
                        super.paintDirtyRegions()
                    }
                }
            }.also { repaintManager = it })
            repaintRunnable = RepaintManager::class.java.getDeclaredField("processingRunnable").run {
                isAccessible = true
                get(repaintManager) as Runnable
            }
        }          
    

    先是通过反射获取到AppContext对象,然后再反射调用AppContext的put方法,把自定义的RepaintManager对象传了进去。
    跟着刚刚的思路,在自定义的RepaintManager中重写了paintDirtyRegions方法,并加上条件判断:只有关闭repaintBarrier(重绘屏障)或停止播放的情况下,才会处理重绘请求。
    最后同样是通过反射,获取到RepaintManager的processingRunnable,用于绕过消息队列,直接处理重绘请求。

    新增一个updateFrameImmediately方法:

        private fun updateFrameImmediately() {
            // 添加脏区
            repaintManager.addDirtyRegion(rootPane, 0, 0, rootPane.width, rootPane.height)
            // 关闭重绘屏障
            repaintBarrier = false
            // 同步处理重绘请求
            repaintRunnable.run()
            // 重新开启屏障
            repaintBarrier = true
        }
    

    第一行调用RepaintManager.addDirtyRegion添加脏区是必须的,因为后面都会根据这些脏区来确定哪些Component需要重绘。
    然后是关闭屏障,直接调用repaintRunnable.run()同步处理重绘请求,重绘完成后再开启屏障,这样其他地方发起的重绘请求就没用了。

    好了,现在可以把原来repaintTask中的EventQueue.invokeLater { rootPane.repaint() },替换成EventQueue.invokeAndWait { updateFrameImmediately() }了:

        private val repaintTask = Runnable {
            while (playing) {
                (repaintInterval - measureTimeMillis {
                    EventQueue.invokeAndWait { updateFrameImmediately() }
                }).let { if (it > 0) Thread.sleep(it) }
            }
        }
    

    细心的同学会发现原来的Thread.sleep(repaintInterval)变成Thread.sleep(it)了,前面还多了个repaintInterval - measureTimeMillis
    是的,我们现在把每次固定的睡眠时间改成动态计算了,如果本次重绘比较耗时,线程sleep的时长也会相应地减少,提高流畅度。
    当然了,还有imageProcessTask里面的Thread.sleep(repaintInterval)也可以替换成这种方式(代码就不贴了)。

    噢!差点忘了刚刚的replaceRepaintManager方法要在init方法里调用,赶紧加上:

        fun init(window: JFrame, url: String) {
            ......
            // 替换做过手脚的RepaintManager
            replaceRepaintManager()
            ......
        }
    

    OK,我们的优化环节已经完成了,运行看下效果如何:

    preview

    非常流畅!太棒了!!!


    收尾工作

    最后来处理一下视频播放完毕的善后工作,比如关闭FFmpegFrameGrabber、清空/释放缓存图片资源什么的。
    先加一个stop方法:

        fun stop() {
            if (playing) {
                playing = false
                Thread.sleep(repaintInterval * 2)
                removePainter()
                frameGrabber?.close()
                frameGrabber = null
                imageQueue?.clear()
                frameImage?.flush()
                frameImage = null
                frameImageGraphics = null
                rootPane.repaint()
            }
        }
    

    sleep(repaintInterval * 2)是为了保证线程池里面那几个线程能运行结束。
    rootPane.repaint()是停止后刷新一下界面,不残留最后一帧。
    removePainter方法是移除开始播放时设置的Painter:

        private fun removePainter() {
            val glassPane = rootPane.glassPane
            if (glassPane is IdeGlassPaneImpl) {
                // 先获取IdeGlassPaneImpl的getNamedPainters方法
                val getNamedPaintersMethod = glassPane::class.java.getDeclaredMethod(
                        "getNamedPainters", String::class.java).apply { isAccessible = true }
                // 通过反射调用,拿到对应的PaintersHelper对象
                val paintersHelper = getNamedPaintersMethod.invoke(glassPane, "idea.background.editor")
    
                // 获取PaintersHelper的removePainter方法
                val removePainterMethod = paintersHelper::class.java.getDeclaredMethod(
                        "removePainter", com.intellij.openapi.ui.Painter::class.java)
                        .apply { isAccessible = true }
                // 反射调用
                removePainterMethod.invoke(paintersHelper, painter)
            }
        }
    

    还没完噢,刚刚的stop是主动停止,还有播放结束自动停止的呢。
    我们的思路是:frameGrabber把视频帧都解析完了(grab方法返回null)之后,就把imageQueue置空并结束线程。然后在imageProcessTask那边,判断到imageQueue为空则主动释放资源并结束线程。
    好,先把imageQueue的类型改成可空:

        private var imageQueue: LinkedBlockingDeque<BufferedImage>? = null
    

    把初始化放在start的时候:

        fun start() {
            frameGrabber?.let { grabber ->
                ......
                playing = true
                imageQueue = LinkedBlockingDeque(32)
                ......
            }
        }
    

    跟着刚刚的思路,修改一下frameGrabTask,如果grab为空则把imageQueue置空并结束线程(当然了,put那里也要加上非空判断):

        private val frameGrabTask = Runnable {
            ......
            while (playing) {
                frameGrabber?.grab()?.also { frame ->
                    ......
                            imageQueue?.put(it)
                    ......
                } ?: run {
                    imageQueue = null
                    return@Runnable
                }
            }
        }
    

    再修改一下imageProcessTask

        private val imageProcessTask = Runnable {
            while (playing) {
                imageQueue?.take()?.also { image ->
                    ......
                    ......
                } ?: run {
                    EventQueue.invokeLater { stop() }
                    return@Runnable
                }
            }
        }
    

    如果imageQueue为空则主动调用stop方法并马上return(结束线程)。

    好,最后加上一个【Stop Play】的功能按钮,就算完美结束了:

    class StopAction : AnAction() {
        override fun actionPerformed(event: AnActionEvent) {
            MediaPlayer.stop()
        }
    }
    

    记得在plugin.xml里声明一下:

        <actions>
            ......
            ......
            <action id="StopAction" class="StopAction" text="Stop Play">
                <add-to-group group-id="ViewMenu" anchor="last"/>
            </action>
        </actions>
    

    运行:

    preview

    按钮已经出来了,现在无论是手动结束还是播放完毕后自动结束,都能正常释放资源了。

    祝摸鱼快乐!!!(低调点,不要真的被炒鱿鱼了噢!)


    本篇文章到此结束,有错误的地方请指出,谢谢大家!

    Github地址:https://github.com/wuyr/intellij-media-player 欢迎star

    展开全文
  • 岁月不饶人,时间总是从不经意的指缝间流逝,越来越觉得,朝九晚五,晚上回来,一两个钟的时间,是非常宝贵的自我增长知识的机会。 从14年到如今,加上大学4年,不知不觉已经接触8年多...全套java视频文档+企业级源码
  • 小白的答案是:Java是铜牌,Linux是银牌,Hadoop是金牌,大数据是王牌。因为Java是学大数据的基础,有基础然后就可以学后续的;最后只有学好大数据这一王牌才能出去找一份比较好的工作。在这里相信有许多想要学习...
  • * @see java.lang.Runnable#run() */ @Override public void run() { hue.setProgress(tempValue++); if (tempValue == MAX_VALUE) { tempValue = 0; } if (!runFlag) { return; } image....
  • Linux的世界2004年获得秋季产品卓越成就奖-最佳表现 Linux的世界2004年获得秋季优秀产品奖-最佳开源 Linux杂志2004年获得编辑选择奖-网络浏览器 2005年 2005年度获得最佳开放源代码项目、电脑编辑人员奖 2005年6月...
  • 在用ViewPager配合Fragment开发的模式中,想做一个类似于桌面壁纸的背景图,可以跟着ViewPager滑动。 先说一下大体思路: 在ViewPager滑动的过程中,监听滑动百分比,再通过这个滑动的百分比来控制背景图的偏移...
  • 怎样成为一位程序员大佬

    千次阅读 2019-05-27 21:27:06
    持续更新Java架构相关技术及资讯热文!!! 一、着装 一个牛X的程序员是根本没有时间打理自己外貌的,发型就要像爱因斯坦一样,顶着一脑袋鸡窝,凌乱蓬松美,给人随时能从头发里掏出一个鸡蛋的感觉。胡子一大把,彰...
  • 摸鱼王

    2021-07-04 00:27:41
    我也经常看到同事的编辑器非常炫酷代码的背景不是黑乎乎的默认主题,而是二次元、动漫图片。 当时我还觉得这位程序员还挺个性,直到我看到了这个开源项目。我才意识到,他可能在摸鱼。 安装后,你就能在编辑器里面...
  • 转载请注明本文出自Cym的博客(http://blog.csdn.net/cym492224103),谢谢支持... ...│ javaapk.com文件列表生成工具.bat ...│ 免费下载很多其它源代码.url │ 文件夹列表.txt │ ├─android web应用 │...
  • 关于代码家(干货集中营)共享android端知识点综合整理 标签: 开源项目自定义控件教程特效工具 2016-03-08 13:23 8520人阅读 评论(2) 收藏 举报  分类: 移动开发(28)  版权声明:...
  • 今天就一起用python自制一款炫酷的音乐播放器吧~ 首先一起来看看最终实现的音乐播放器效果: 下面,我们开始介绍这个音乐播放器的制作过程。 一、核心功能设计 总体来说,我们首先需要设计UI界面,对播放器的画面...
  • 轻量级支持 support-v7 中的 RecyclerView 的滑动删除(Swipe to dismiss)行为,不需要修改源代码,只要简单的绑定 onTouchListener 项目地址: https://github.com/CodeFalling/RecyclerViewSwipeDismiss 效果图: ...
  • 轻量级支持 support-v7 中的 RecyclerView 的滑动删除(Swipe to dismiss)行为,不需要修改源代码,只要简单的绑定 onTouchListener 项目地址: https://github.com/CodeFalling/RecyclerViewSwipeDismiss 效果图: ...
  • 【1】这里的山路十八弯,这里的文件排排站……【2】万有引力版桌面也非常炫酷啊【3】新功能终于上线了【4】看了很久总结出:这一定不是我写的代码【5】这一定是一位爱岗敬业的小...
  • 从不用壁纸,无任何美化,给人一种WIN98的感觉。只装文本编辑器+开发工具软件。越简朴越纯粹,代表你越牛逼。能不用IDE就不要用,实在装不了,无论IDE是什么,一定要调成DOS或linux那种黑色背景的,给人一种你随时敲...
  • Mac 开发者快速上手指南!

    千次阅读 2020-01-14 08:31:00
    迄今为止最炫酷Java编程 IDE,直接将eclipse拍在了沙滩上 9、Dash Mac专属的文档管理工具 10、Charles 抓包神器 / Mock工具  常用软件   1、Rescue Time 日常行为管理。可以分析出...
  • 曾经有一篇百万阅读量的爆文,里面出了道填空题,问:____是铜牌,____是银牌,____是金牌,____是王牌。小白的答案是:Java是铜牌,Linux是银牌,Hado...
  • 超级好用的背景图设置插件,亲测有效,改完背景,你就是最炫酷的仔。 Bracket Pair Colorizer 还在为括号太多而发愁吗,还在为找不到哪个括号是一对而烦恼吗,有了它,这个神器,你就不用再为这个问题而烦恼了,...
  • │ Android应用源码之java调用C例子.zip │ Android应用源码之JazzyViewPager-master.zip │ Android应用源码之JBox2D-src.zip │ Android应用源码之jchat4手机聊天程序.zip │ Android应用源码之JetBoy飞机游戏...
  • 每天进步一丢丢,连接梦与想 愿你走出半生,归来仍是少年 经常被麻麻说 这么大了,还看这些公仔剧 因为在看动漫时 能再当一回孩子,真心快乐,enjoy~ ...动态壁纸 有好多漂亮小姐姐(嘻嘻~) 桌面整理 就是...

空空如也

空空如也

1 2 3 4 5 ... 13
收藏数 254
精华内容 101
热门标签
关键字:

java炫酷动态壁纸代码

java 订阅