前言
灵感来自于 : , 因为现在有了根据图片来生成gif的工具, 而且自己当时使用的一款gifGenerator并不是很完善, 再者闲来无事, 就花了一些时间来完成这个了
问题描述
一个简单的gif生成器, 傻瓜式的, 应该基本上都会用
难点主要在于 :
1 一些swing相关的效果[比如 取消边框, 鼠标拖拽, 进度条, 更新任务进度等等]
2 robot截图之后缓冲队列的设计
3 业务操作的线程关系的协调
这里 说一说缓冲队列的设计, 缓存队列集合bufferedImgQueues是使用Map< Integer, Queue< BufferedImage>>, 这个映射key是给定的缓冲队列的索引, value是对应过的缓冲队列, 假设刷出图片的阈值为flushThreshold, 如果现在截取的图片数量超过了flushThreshold 则将当前缓存队列添加到bufferedImgQueues, 然后 启动一条线程刷出当前队列的图片, 刷出所有图片之后, 将该队列移除bufferedImgQueues[for gc], 然后新建一个缓冲队列, 周而复始
1 我曾经直接使用单线程刷出图片, 但是 太慢了, 太不能忍受了, 随着图片的增加, 用户等待时间会很长
2 再考虑直接 使用一个List, 如果使用ArrayList的话, 在多线程刷出图片的场景会存在一个问题, 就是移除已经刷出的图片开销会很大, 而且 随着缓冲的图片越来越多维护并发的正确性 也需要很多的开销
如果使用LinkedList, 在多线程刷出图片的场景同样存在问题, 读取每一个线程应该刷出的图片的效率不高, 而且在并发方便, 和上面的ArrayList存在相同的问题
3 在考虑使用BlockQueue在链表头获取, 删除元素, 使用Lock相关api维护线程的安全, 但是还存在维护并发的开销啊,, 能不能省掉这一笔开销呢?
4 就是最后决定用的Map< Integer, Queue< BufferedImage>>了, 讲各个刷出图片线程刷出的图片队列分开, 就好了嘛
其他的业务逻辑, 请详见代码
依赖于AnimatedGifEncoder, 这个是一个开源的gif的生成的工具, 大家可以自行搜索下载
思路
思路 :
1 begin按钮, 启动截图线程
2 stop按钮, 停止截图线程, 并等待刷出所有的图片成功
3 save按钮, 停止截图线程, 刷出所有图片, 生成gif文件
4 newGif按钮, 更新标志位[下一次录制的时候, 清空临时文件夹], 停止截图线程
5 close按钮, 关闭相关资源, 关闭程序
参考代码
注 : 依赖于AnimatedGifEncoder
1 Test12GifGenerator .java
/**
* file name : TransparentFrame.java
* created at : 4:45:50 PM Nov 23, 2015
* created by
*/
package com.hx.test08;
// 录制gif的工具
// 将ToolFrame, ProgressFrame 该为继承自JDialog, 因为这两个不需要创建一个窗口在任务栏 -- 2015.11.25
public class Test12GifGenerator extends JFrame {
// 窗口的位置, 宽高, 工具窗口的宽高
private static int FRAME_X = 100;
private static int FRAME_Y = 100;
private static int FRAME_WIDTH = 800;
private static int FRAME_HEIGHT = 600;
private static int TOOL_FRAMEWIDTH = 100;
private static int TOOL_FRAMEHEIGHT = FRAME_HEIGHT;
// 常量配置
// 临时文件夹, 临时文件名, 临时文件后缀, GIF后缀, 临时文件格式
// 截图延迟, 缓存的图片个数, 等待线程池结束任务的时候 检查的周期
private final static String TMP_DIR = "C:\\Users\\970655147\\Desktop\\tmp\\gifTmp";
private final static String TMP_NAME = "gifTmp";
private final static String SUFFIX = Tools.PNG;
private final static String GIF = ".gif";
private final static String FORMAT = SUFFIX.substring(1);
private static int delay = 100;
private static int cachedImageSize = 60;
private static int checkInterval = 200;
private static int flushImgCheckInterval = 500;
// 工具窗口, 精度条窗口
private ToolFrame toolFrame;
private ProgressBarFrame progressBarFrame;
// 初始化
// 1. 配置当前窗口的属性
// 2. 创建ProgressBarFrame, ToolFrame, 并配置
public Test12GifGenerator() {
this.setTitle("gif generator");
this.setBounds(FRAME_X, FRAME_Y, FRAME_WIDTH, FRAME_HEIGHT);
this.setUndecorated(true);
this.setVisible(true);
this.setOpacity(0.2f);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
progressBarFrame = new ProgressBarFrame(this);
toolFrame = new ToolFrame(this);
toolFrame.setLocation(FRAME_X+FRAME_WIDTH, FRAME_Y);
MouseAdapter adater = new DragMouseAdapter(this) {
public void mousePressed(MouseEvent e) {
super.mousePressed(e);
toolFrame.requestFocus();
}
public void mouseDragged(MouseEvent e) {
super.mouseDragged(e);
java.awt.Point curPos = Test12GifGenerator.this.getLocation();
toolFrame.setLocation(curPos.x+FRAME_WIDTH, curPos.y);
}
};
this.addMouseListener(adater);
this.addMouseMotionListener(adater);
toolFrame.addMouseListener(adater);
toolFrame.addMouseMotionListener(adater);
}
// main
public static void main(String[] args) {
new Test12GifGenerator();
}
// 清理临时工作区间
// 清理掉所有getScreenShotedFiles返回的文件
private static void clearTmpDir() {
File[] files = getScreenShotedFiles();
String curFile = null;
int i = -1;
try {
for(i=0; i<files.length; i++) {
curFile = files[i].getName();
files[i].delete();
}
} catch (Exception e) {
Log.err("error while delete " + curFile + " !");
e.printStackTrace();
} finally {
Log.log("deleted " + i + " tmp files !");
}
}
// 获取截图截取到的图片
// 这里的条件为以TMP_NAME开头, 并且以SUFFIX结尾
public static File[] getScreenShotedFiles() {
File tmpDir = new File(TMP_DIR );
File[] files = tmpDir.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.startsWith(TMP_NAME) && name.endsWith(SUFFIX);
}
});
return files;
}
// 右边的小工具栏
static class ToolFrame extends JDialog {
// 父窗口, 开始, 停止, 保存, 新建gif, 关闭按钮
// 输出缓存的图片进度按钮, 生成gif进度按钮
private Test12GifGenerator parentFrame;
private JButton begin = new JButton("begin");
private JButton stop = new JButton("stop");
private JButton save = new JButton("save");
private JButton newGif = new JButton("newGif");
private JButton close = new JButton("close");
private JButton flushImgProgress = new JButton("0 / 0");
private JButton generateGifProgress = new JButton("0 / 0");
// 是否开始, 是否开始过, 是否是创建新的gif, 是否需要将缓存的gif刷出到文件
private boolean isStart = false;
private boolean haveStarted = false;
private boolean isNewGif = true;
private boolean needFlushImgs = true;
// robot 截图使用, 缓存的文件索引, 线程池的个数
// 刷新图片文件的线程池, 任务线程池, 获取图片的线程
// 刷出到磁盘的图片的个数, 刷出去的 或者正在刷的图片个数, 更新面板的进度的线程是否存在
// 对于缓存图片, 我之前的想法是将一定量的图片缓存在一个BufferedImage[]中
// 但是 从实际情况来说, 刷新图片的速度是满足不了获取图片的速度的, 所以就使用了一个Map<Integer, Queue<BufferedImage>>
// 是为了方便获取现在仍在向磁盘刷出的Image
// 如果 不需要获取在刷出的Image, 可以省略imgs这个数据结构
private Robot robot;
Map<Integer, Queue<BufferedImage>> imgses = new HashMap<>();
Queue<BufferedImage> imgs = new LinkedList<>();
private int cachedImgQueueIdx = 0;
private int flushImgsThreads = 10;
private int nThreads = 10;
private ThreadPoolExecutor flushImgsThreadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(flushImgsThreads);
private ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(nThreads);
private Thread getImageThread = null;
private AtomicInteger flushOnDisk = new AtomicInteger(0);
private int flushed = 0;
private boolean updateFlushProcessExists = false;
// 初始化
// 1. 初始化robot
// 2. 配置当前JFrame
// 3. 创建子组件, 添加listener
public ToolFrame(Test12GifGenerator parentFrame) {
try {
robot = new Robot();
} catch (AWTException e) {
e.printStackTrace();
}
this.parentFrame = parentFrame;
this.setTitle("tool");
this.setUndecorated(true);
this.setVisible(true);
this.setBounds(FRAME_X, FRAME_Y, TOOL_FRAMEWIDTH, TOOL_FRAMEHEIGHT);
this.setLayout(new GridLayout(10, 1) );
this.add(begin );
this.add(stop );
this.add(save );
this.add(newGif );
this.add(close );
this.add(flushImgProgress );
this.add(generateGifProgress );
flushImgProgress.setEnabled(false);
generateGifProgress.setEnabled(false);
checkStartOrStop();
ActionListener listener = new BtnActionListener();
begin.addActionListener(listener );
stop.addActionListener(listener );
close.addActionListener(listener );
save.addActionListener(listener );
newGif.addActionListener(listener );
}
// 配置begin, stop, close按钮的可用性
// 各个按钮的可用性由isStart, haveStarted决定
private void checkStartOrStop() {
if(isStart) {
begin.setEnabled(false);
stop.setEnabled(true);
} else {
begin.setEnabled(true);
stop.setEnabled(false);
}
if(haveStarted) {
newGif.setEnabled(true);
save.setEnabled(true);
} else {
newGif.setEnabled(false);
save.setEnabled(false);
}
close.setEnabled(true);
}
// 刷出缓存的图片
// 放入flushImgsThreadPool线程池中启动一个任务, 输出给定队列的图片
// 如果 没有启动更新刷出图片进度的线程, 则启动一条刷新图片进度的线程
private void flush(final int cachedImgQueueIdx, final Queue<BufferedImage> cachedImages) {
flushImgsThreadPool.execute(new Runnable() {
public void run() {
int size = cachedImages.size();
TmpGetter tmpGetter = new MyTmpGetter(TMP_DIR, TMP_NAME, flushed, SUFFIX);
flushed += size;
try {
while(cachedImages.size() > 0) {
BufferedImage curImg = cachedImages.poll();
if(needFlushImgs) {
ImageIO.write(curImg, FORMAT, new File(tmpGetter.getNextTmpPath()) );
}
flushOnDisk.incrementAndGet();
}
} catch (Exception e) {
Log.err("error while flush pics !");
e.printStackTrace();
} finally {
imgses.remove(cachedImgQueueIdx);
Log.log("flushed " + size + " [thread(" + cachedImgQueueIdx + ")] pics, needFlushImgs : " + needFlushImgs + " !");
}
}
});
if(! updateFlushProcessExists) {
updateFlushProcessExists = true;
threadPool.execute(new Runnable() {
public void run() {
while (! flushImgsThreadPool.isShutdown() ) {
int queueSize = flushImgsThreadPool.getQueue().size();
int activeTasks = flushImgsThreadPool.getActiveCount();
flushImgProgress.setText(getProgressStr(flushOnDisk.get(), flushed) );
// Log.log(getProgressStr(flushOnDisk.get(), flushed) );
if((queueSize == 0) && (activeTasks == 0) ) {
break ;
}
Tools.sleep(flushImgCheckInterval);
}
updateFlushProcessExists = false;
}
});
}
}
// 根据给定的录制的图片成成gif
// 首先 等待刷新图片的线程结束
// 然后 在根据AnimatedGifEncoder创建gif
// 最后 恢复按钮的可用性
private void generateGIF() {
threadPool.execute(new Runnable() {
public void run() {
try {
if(getImageThread != null) {
getImageThread.join();
}
} catch (InterruptedException e1) {
e1.printStackTrace();
} finally {
getImageThread = null;
awaitTasksEnd(flushImgsThreadPool, checkInterval, false);
}
parentFrame.progressBarFrame.setVisible(true);
parentFrame.progressBarFrame.updateProgress("generate gif !");
AnimatedGifEncoder gifEncoder = new AnimatedGifEncoder();
TmpGetter tmpGetter = new MyTmpGetter(TMP_DIR, TMP_NAME, 0, SUFFIX);
String targetPath = tmpGetter.getNextTmpPath(GIF);
while(new File(targetPath).exists()) {
targetPath = tmpGetter.getNextTmpPath(GIF);
}
try {
gifEncoder.start(targetPath);
gifEncoder.setQuality(15);
gifEncoder.setRepeat(0);
gifEncoder.setDelay(delay);
File[] files = getScreenShotedFiles();
for(int i=0; i<files.length; i++) {
gifEncoder.addFrame(ImageIO.read(files[i]) );
generateGifProgress.setText(getProgressStr(i+1, files.length) );
parentFrame.progressBarFrame.updateProgress(i+1, files.length);
}
gifEncoder.finish();
} catch (FileNotFoundException e) {
Log.err("have no this file : " + targetPath + " !");
e.printStackTrace();
} catch (Exception e) {
Log.err("error while make gif !");
e.printStackTrace();
} finally {
parentFrame.progressBarFrame.setVisible(false);
parentFrame.progressBarFrame.updateProgress(0);
Log.log("generate gif['" + targetPath + "'] success !");
checkStartOrStop();
}
}
});
}
// 如果获取图片的线程存在
// 启动一条线程等待其终止, 并恢复各个按钮的可用性
// 否则 恢复各个按钮的可用性
private void joinIfGetImgThreadExists(final boolean isShutdown) {
if(getImageThread != null) {
threadPool.execute(new Runnable() {
public void run() {
try {
getImageThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
getImageThread = null;
awaitTasksEnd(flushImgsThreadPool, checkInterval, isShutdown);
checkStartOrStop();
}
}
});
} else {
checkStartOrStop();
}
}
// 关闭当前程序
// 配置是否需要刷出剩余缓存的图片
// 如果需要生成gif的话, 生成gif
// 等待两个业务线程池结束
// 关闭三个窗口
private void closeWindow(boolean isGenerateGif) {
needFlushImgs = isGenerateGif;
if(isGenerateGif) {
generateGIF();
}
awaitTasksEnd(flushImgsThreadPool, checkInterval, true);
awaitTasksEnd(threadPool, checkInterval, true);
parentFrame.dispose();
ToolFrame.this.dispose();
parentFrame.progressBarFrame.dispose();
}
// 等待 线程池中任务结束 [并不关闭线程池]
public static void awaitTasksEnd(ThreadPoolExecutor threadPool, int checkInterval, boolean isShutdown, boolean isLog) {
while (! threadPool.isShutdown() ) {
int taskInQueue = threadPool.getQueue().size();
int activeTaskCount = threadPool.getActiveCount();
if((taskInQueue == 0) && (activeTaskCount == 0) ) {
if(isShutdown) {
threadPool.shutdown();
}
break ;
} else {
if(isLog ) {
Log.log("task in queue : " + taskInQueue + ", active task count : " + activeTaskCount + ", at : " + new Date().toString() + " !");
}
Tools.sleep(checkInterval);
}
}
}
public static void awaitTasksEnd(ThreadPoolExecutor threadPool, int checkInterval, boolean isShutdown) {
awaitTasksEnd(threadPool, checkInterval, isShutdown, false);
}
// 获取进度字符串
private static String getProgressStr(int now, int max) {
return now + " / " + max;
}
// 响应按钮的时间
// 如果是开始按钮, 启动截屏线程, 当缓冲队列中的图片达到一定的阈值的时候启动一条线程刷出图片
// 如果是stop按钮, 停止截图线程, 并等待刷出所有的图片成功
// save按钮, 停止截图线程, 刷出所有图片, 生成gif文件
// newGif按钮, 更新标志位[下一次录制的时候, 清空临时文件夹], 停止截图线程
// 如果是close按钮, 关闭相关资源, 关闭程序
class BtnActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if(e.getSource() == begin) {
isStart = true;
haveStarted = true;
needFlushImgs = true;
if(isNewGif) {
clearTmpDir();
flushOnDisk.set(0);;
flushed = 0;
isNewGif = false;
cachedImgQueueIdx = 0;
}
getImageThread = new Thread(new GetImageRunnable() );
getImageThread.start();
checkStartOrStop();
} else if(e.getSource() == stop) {
isStart = false;
needFlushImgs = true;
setEnabled(false);
joinIfGetImgThreadExists(false);
} else if(e.getSource() == save) {
isStart = false;
isNewGif = true;
haveStarted = false;
needFlushImgs = true;
setEnabled(false);
generateGIF();
} else if(e.getSource() == newGif) {
isStart = false;
isNewGif = true;
haveStarted = false;
needFlushImgs = false;
setEnabled(false);
joinIfGetImgThreadExists(false);
} else if(e.getSource() == close) {
isStart = false;
if(haveStarted) {
int res = JOptionPane.showConfirmDialog(ToolFrame.this.parentFrame, "need save gif ?");
if(res == JOptionPane.OK_OPTION ) {
closeWindow(true);
}else if(res ==JOptionPane.NO_OPTION ) {
closeWindow(false);
} else {
}
} else {
closeWindow(false);
}
checkStartOrStop();
}
}
// 配置五个按钮是否可按键
private void setEnabled(boolean isEnable) {
begin.setEnabled(isEnable);
stop.setEnabled(isEnable);
save.setEnabled(isEnable);
newGif.setEnabled(isEnable);
close.setEnabled(isEnable);
}
}
// 绘制图片的线程
class GetImageRunnable implements Runnable {
public void run() {
while(isStart) {
imgs.add(robot.createScreenCapture(ToolFrame.this.parentFrame.getBounds()) );
// Log.log(imgs.size());
if(imgs.size() >= cachedImageSize) {
imgses.put(cachedImgQueueIdx, imgs);
flush(cachedImgQueueIdx ++, imgs);
imgs = new LinkedList<>();
}
Tools.sleep(delay);
}
if(imgs.size() > 0) {
imgses.put(cachedImgQueueIdx, imgs);
flush(cachedImgQueueIdx ++, imgs);
// 这里可以不用新建imgs, 因为flush之后imgs会被清空
imgs = new LinkedList<>();
}
}
}
}
// 精度条的Frame
public static class ProgressBarFrame extends JDialog {
// 进度条
private JProgressBar progressBar = new JProgressBar();
// 初始化
// 1. 配置当前Frame
// 2. 创建子组件, 并初始化
public ProgressBarFrame(JFrame parentFrame) {
setSize(300, 50);
setUndecorated(true);
MouseAdapter adapter = new DragMouseAdapter(this);
addMouseListener(adapter);
addMouseMotionListener(adapter);
progressBar.setStringPainted(true);
progressBar.setValue(0);
progressBar.setMaximum(100);
progressBar.setString("...");
this.add(progressBar);
java.awt.Point parentLocation = parentFrame.getLocation();
parentLocation.x += FRAME_WIDTH >> 1;
parentLocation.y += FRAME_HEIGHT >> 1;
setLocation(parentLocation);
}
// 更新精度条
public void updateProgress(int now, int max) {
progressBar.setValue(now);
progressBar.setMaximum(max);
}
public void updateProgress(int now) {
progressBar.setValue(now);
}
public void updateProgress(String str) {
progressBar.setString(str);
}
}
// TmpGetter
static class MyTmpGetter extends TmpGetter {
// 初始化
public MyTmpGetter(String tmpDir, String tmpName, int tmpIdx, String suffix) {
super(tmpDir, tmpName, tmpIdx, suffix);
}
// 获取临时文件的下一个索引[生成文件名称]
@Override
protected String getNextTmpName() {
int idx = tmpIdx.getAndIncrement();
String idxStr = String.format("%06d", idx);
return TMP_NAME + (idxStr );
}
}
}
2 DragMouseAdapter.java
/**
* file name : DragMouseListener.java
* created at : 9:29:40 PM Nov 24, 2015
* created by
*/
package com.hx.util;
// 拖拽的MouseListener
public class DragMouseAdapter extends MouseAdapter {
// 操作的frame, 上一次点击按钮的时候的x
protected Component component;
private int lastX = -1;
private int lastY = -1;
// 初始化
public DragMouseAdapter(Component component) {
super();
this.component = component;
}
// 记录拖拽之前点击的坐标
@Override
public void mousePressed(MouseEvent e) {
lastX = e.getX();
lastY = e.getY();
}
// 更新给定过的Component的位置
@Override
public void mouseDragged(MouseEvent e) {
if(lastX != -1) {
java.awt.Point point = component.getLocation();
point.x += e.getX() - lastX;
point.y += e.getY() - lastY;
component.setLocation(point );
}
}
}
3 TmpGetter .java
/**
* file name : TmpGetter.java
* created at : 9:31:46 PM Nov 24, 2015
* created by
*/
package com.hx.util;
// 获取临时文件相关的数据
public class TmpGetter {
// 临时文件夹, 临时文件名, 临时文件索引, 默认的后缀
public final String tmpDir;
public final String tmpName;
public final AtomicInteger tmpIdx;
public final String suffix;
// 初始化
public TmpGetter(String tmpDir, String tmpName, int tmpIdx, String suffix) {
super();
this.tmpDir = tmpDir;
this.tmpName = tmpName;
this.tmpIdx = new AtomicInteger(tmpIdx);
this.suffix = suffix;
}
// 获取临时路径的下一个路径[返回文件路径]
public String getNextTmpPath() {
return tmpDir + "\\" + getNextTmpName() + suffix;
}
public String getNextTmpPath(String suffix) {
return tmpDir + "\\" + getNextTmpName() + suffix;
}
public String getNextTmpPath(String fileName, String suffix) {
return tmpDir + "\\" + fileName + suffix;
}
public String getTmpPath(int idx) {
return tmpDir + "\\" + tmpName + idx + suffix;
}
public String getTmpPath(int idx, String suffix) {
return tmpDir + "\\" + tmpName + idx + suffix;
}
public String getTmpPath(String name) {
return tmpDir + "\\" + name + suffix;
}
public String getTmpPath(String name, String suffix) {
return tmpDir + "\\" + name + suffix;
}
public String getNextTmpDir() {
return tmpDir + "\\" + getNextTmpName();
}
public String getTmpDir(int idx) {
return tmpDir + "\\" + tmpName + idx;
}
public String getTmpDir(String name) {
return tmpDir + "\\" + name;
}
public void setTmpIdx(int tmpIdx) {
this.tmpIdx.set(tmpIdx);
}
// 获取临时文件的下一个索引[生成文件名称]
protected String getNextTmpName() {
return tmpName + (String.valueOf(tmpIdx.getAndIncrement()) );
}
}
效果截图
newGif
总结
测试 下来, 这个 是可以用, 但是 可能相关api优化的不够吧, 导致gif占用的空间非常的大, 所以 也就只能当做写着玩玩了
还有一个功能在于, 可以截屏, 截出一系列的图片, 然后使用专业的gif生成工具来完成gif的制作, 这样的话, 这个工具就相当于是一个录屏工具了
当然 这种功能完全可以通过gifGenerator等等其他可视化工具实现, 这个不过是瞎扯淡, 写着玩..
参考 :
注 : 因为作者的水平有限,必然可能出现一些bug, 所以请大家指出!