以不同的形式在安卓中建立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,安卓原生就能做到,感興趣的可以自己去搜一搜。