以不同的形式在安卓中創建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