以不同的形式在安卓中创建GIF动图

前言

在我的项目 隐云图解制作 中支持多种不同的方式生成 GIF 动图,例如直接录屏生成GIF、通过图片合成GIF、通过GIF合成GIF、从视频中截取任意位置时长的GIF。

本篇文章中我们将对这些方法进行拆解并附上实现代码,以供有需要的读者使用。

实现方法

我们实现生成动图的需求依旧需要依赖于使用 FFmpeg 和 Gifsicle 这两个库,不知道怎么在安卓中使用这两个库的,可以看看我之前的文章,其中有说明。

使用图片合成GIF

GIF动图可以简单的看做使用多张图片按一定顺序播放后实现的动画,所以,首当其冲的,我们可以使用多张图片合成GIF。

这个功能我们需要使用 FFmpeg 来实现。

我们先直接看一下实现图片合成 GIF 的 FFMpeg 命令: ffpemg -f concat -safe 0 -i concat.txt out.gif

上面的命令中 -f concat -safe 0 -i concat.txt 这几个参数的作用均是为了加载图片; out.gif 则是指定了输出文件。

其实可以简单的使用 ffmpeg -f image2 -i %d.jpg output.gif 来合成动图,其中 -f image2 -i %d.jpg 表示输入文件,%d.jpg 表示按照顺序读取当前路径下的所有文件,这个参数需要保证输入的所有文件已按照数字顺序规范命名,例如 1.jpg 2.jpg 3.jpg ……

但是由于我们这里的图片来自于用户选择的图片,可能分布于不同的路径,且文件名也没有规律,虽然我们也可以直接把所有图片复制到统一路径并规范重命名,但是这样用户体验不太好,所以我们使用了直接读取原文件的方式, 即 -f concat -safe 0 -i concat.txt

其中 concat concat.txt 表示读取 concat.txt 中的文件路径用于拼接,由于我们这里使用的都是绝对路径,所以需要加上 -safe 0 参数,确保读取文件正确。

concat.txt 文件内容格式形如:

file image.jpg
file xxx.jpg
file yyy.jpg

因为我们需要指定每张图片的持续时间,所以还要加上一个参数 duration ,例如我们希望每张图片持续 1s 则 concat.txt 应该为;

file image.jpg
duration 1
file xxx.jpg
duration 1
file yyy.jpg
duration 1

在安卓中我们可以这样生成 concat.txt :

val result = arrayOf<File>(
    // ……
)  // gif 文件列表

val duration = 1 // 每张图片持续时间
val concatFile = File(cachePath, "concat.txt")

for (originalFile in result) {
    concatFile.appendText("file $originalFile\nduration $duration\n")
}

关于 concat 的详细说明可以参见官方文档:Concatenate

接下来就是生成 FFmpeg 命令和执行这个命令:

val concatFile = File("concat.txt")
val saveFile = File("out.gif")

// 生成命令
val cmd = FFMpegArgumentsBuilder.Builder()
    .setFormat("concat")
    .setArgWithValue("-safe", "0")
    .setInput(concatFile.absolutePath)
    .setOutput(saveFile.absolutePath)
    .build()
    .cmd

// 开始执行
FFmpegKit.executeWithArguments(cmd)

从视频中截取GIF

从视频中截取 GIF 依然需要使用 FFmpeg。

从视频中截取 GIF 最简单的命令:ffmpeg -i xx.mp4 xx.gif 即可,但是这样只是直接将整个 mp4 文件转成了 GIF ,显然不符合我们所说的应该是可以指定任意时间节点。

所以我们需要加上参数 -ss 表示截取的开始时间, -t 表示持续时间。

例如,ffmpeg -ss 1.5 -t 2 -i xx.mp4 xx.gif 表示从 xx.mp4 视频第 1.5s 开始截取,总共截取 2s 。

但是这样并不能满足我们的需求,正如我们在上一篇如何压缩 GIF 的文章中所述,直接从视频中截取 GIF 的话由于颜色位数的限制,显示效果会非常不理想,所以我们可以通过自定义调色板的方式来提高生成的 GIF 画质。

首先,生成调色板文件: ffmpeg -y -ss 1.5 -t 2 -i xx.mp4 -vf scale=1920:1080:flags=lanczospalettegen PalettePic.png

然后,使用生成的调色板生成 GIF: ffmpeg -y -ss 1.5 -t 2 -i xx.mp4 -i PalettePic.png -r 24 -b 100k -lavfi scale=1920:1080:flags=lanczos[x];[x][1:v]paletteuse out.gif

上面的命令中我们还指定了缩放生成文件分辨率为 1920:1080 ,帧率为 24,比特率为 100k(10m)。其实这些参数都是原视频的参数,我这里把它加上只是为了说明生成 GIF 也可以修改各种参数。

对了,上面视频中的时间点是我封装了一个播放器,并在播放器界面放置了一个截图按钮,根据用户点击按钮的时间来获得的,当然不能让用户手动输入这么不友好了。

接下来,我们生成上述两个命令:

val paletteCmd = FFMpegArgumentsBuilder.Builder()
    .setOverride(true)
    .setStartTime((markTime[0] / 1000.0).toString())
    .setDurationTime(((markTime[1] - markTime[0]) / 1000.0).toString())
    .setInput(videoPath)
    .setVideoFilter("scale=$gifRp:flags=lanczos,palettegen")
    .setOutput(palettePicPath)
    .build()
    .cmd

val ffMpegArgumentsBuilder = FFMpegArgumentsBuilder.Builder()
    .setOverride(true)
    .setStartTime((markTime[0] / 1000.0).toString())
    .setDurationTime(((markTime[1] - markTime[0]) / 1000.0).toString())
    .setInput(videoPath)

if (gifRp != "-1") {
    ffMpegArgumentsBuilder.setFrameSize(gifRp)
}
if (gifFrameRate != "-1") {
    ffMpegArgumentsBuilder.setFrameRate(gifFrameRate)
}

ffMpegArgumentsBuilder.setOutput(savePath)

val gifCmd = ffMpegArgumentsBuilder.build().cmd

然后分别执行这两个命令即可:

FFmpegKit.executeWithArguments(paletteCmd)
FFmpegKit.executeWithArguments(gifCmd)

录屏生成GIF

录屏生成 GIF 其实本质就是上一节中的从视频中截取 GIF,只不过此时的视频不再是本地视频,而是我们实时录制的视频。

由于录屏不是本文的重点,所以我们这里不再赘述,之后如果有时间我会把项目中有关录屏的部分单独抽出来写一个小 demo。

使用GIF合成GIF

使用GIF合成GIF,其实说成是拼接多个GIF更加准确。

这个功能需要使用 Gifsicle 实现。

老规矩,先直接看一下命令:

gifsicle gif1.gif gif2.gif gif3.gif -o out.gif

使用 Gifsicle 合成 GIF 的命令十分简单,只需要依次指定输入的文件后指定输出文件即可。

在安卓中使用则为:

val result = arrayOf<File>(
    // ……
)  // gif 文件列表
val saveFile = File("out.gif") // 输出文件
        
val gifsicle = File(File(requireActivity().applicationInfo.nativeLibraryDir), "libgifsicle.so")
var cmd = "$gifsicle "
for (file in result) { // 遍历输入文件并追加到命令中
    cmd += "${file.availablePath} "
}
cmd += "-o ${saveFile.absolutePath}"

// 开始执行
val envp = arrayOf("LD_LIBRARY_PATH=" + gifsicleFile.parent)
val process = Runtime.getRuntime().exec(cmd, envp)
if (process.waitFor() == 0) {
    Result.success(0)
} else {
    Result.failure(IllegalStateException("response code not 0"))
}

当然,上面只是最最基础的合成 GIF ,实际上我们可以自定义很多参数:

如果你不想一个文件一个文件的输入到命令行中,则可以使用 --batch-b 参数,表示输入指定目录下所有的 GIF 文件。

如果你想给每个 GIF 之间添加延迟,则可以使用 --delay [time]-d [time] 参数,该参数表示每个 GIF 之间间隔的时间,如 gifsicle --delay 50 gif1.gif gif2.gif -o out.gif 表示每个 GIF 之间会暂停 0.5 s。

如果你想指定生成的 GIF 的循环次数(当然大多数情况下都是无限循环),则可以使用 --loop[-count]-l[count] 参数,如 gifsicle --loop=3 gif1.gif gif2.gif -o out.gif 表示生成的 GIF 会循环3次。当然如果不写次数或写次数为0则为无限循环;--no-loopcount 表示不循环。

总结

自此,所有在安卓中创建 GIF 的方法已经讲解完毕。

由于这篇文章是基于我的项目的代码进行讲解的,而我的项目强依赖于 FFmpeg 和 Gifsicle,所以很多需求功能我都是直接使用 FFmpeg 去实现了,但是对于其他项目来说,可能需要考量引入 FFmpeg 对包体积大小的影响。

一个 FFmpeg 库动辄十几二十 MB,不是所有 APP 都能接受的。

如果只是简单的使用图片合成 GIF,安卓原生就能做到,感兴趣的可以自己去搜一搜。