以不同的形式在安卓中創建GIF動圖

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第7天,點擊查看活動詳情

前言

在我的項目 隱雲圖解制作 中支持多種不同的方式生成 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 :

```kotlin val result = arrayOf( // …… ) // gif 文件列表

val duration = 1 // 每張圖片持續時間 val concatFile = File(cachePath, "concat.txt")

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

關於 concat 的詳細説明可以參見官方文檔:Concatenate

接下來就是生成 FFmpeg 命令和執行這個命令:

```kotlin 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 也可以修改各種參數。

對了,上面視頻中的時間點是我封裝了一個播放器,並在播放器界面放置了一個截圖按鈕,根據用户點擊按鈕的時間來獲得的,當然不能讓用户手動輸入這麼不友好了。

接下來,我們生成上述兩個命令:

```kotlin 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 ```

然後分別執行這兩個命令即可:

kotlin 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 的命令十分簡單,只需要依次指定輸入的文件後指定輸出文件即可。

在安卓中使用則為:

```kotlin val result = arrayOf( // …… ) // 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,安卓原生就能做到,感興趣的可以自己去搜一搜。