美文网首页
以不同的形式在安卓中创建GIF动图

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

作者: _Jun | 来源:发表于2022-10-15 19:53 被阅读0次

    前言

    在我的项目 隐云图解制作 中支持多种不同的方式生成 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,安卓原生就能做到,感兴趣的可以自己去搜一搜。

    作者:equationl
    链接:https://juejin.cn/post/7152063079679787022

    相关文章

      网友评论

          本文标题:以不同的形式在安卓中创建GIF动图

          本文链接:https://www.haomeiwen.com/subject/tclkzrtx.html