以不同的形式在安卓中建立GIF動圖
持續創作,加速成長!這是我參與「掘金日新計劃 · 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
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
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,安卓原生就能做到,感興趣的可以自己去搜一搜。
- 安卓與串列埠通訊-校驗篇
- 安卓與串列埠通訊-實踐篇
- 為 Kotlin 的函式新增作用域限制(以 Compose 為例)
- 安卓與串列埠通訊-基礎篇
- Compose For Desktop 實踐:使用 Compose-jb 做一個時間水印助手
- 初探 Compose for Wear OS:實現一個簡易選擇APP
- Compose太香了,不想再寫傳統 xml View?教你如何在已有View專案中混合使用Compose
- 在安卓中壓縮GIF的幾種方法(附例項程式碼)
- 魔改車鑰匙實現遠端控車:(1)整體思路及控制方案實現
- 跟我一起使用 compose 做一個跨平臺的黑白棋遊戲(3)狀態與遊戲控制邏輯
- 跟我一起使用 compose 做一個跨平臺的黑白棋遊戲(2)介面佈局
- 以不同的形式在安卓中建立GIF動圖
- 跟我一起使用 compose 做一個跨平臺的黑白棋遊戲(4)移植到compose-jb實現跨平臺
- 羨慕大勞星空頂?不如跟我一起使用 Jetpack compose 繪製一個星空背景(帶流星動畫)
- 魔改車鑰匙實現遠端控車:(4)基於compose和經典藍芽編寫一個控制APP