OpenAtom OpenHarmony(以下简称“OpenHarmony”)提供了Image组件反对GIF动图的播放,然而不足扩大能力,不反对播放管制等。明天介绍一款三方库——ohos-gif-drawable三方组件,带大家一起玩转GIF的数据渲染,搞定GIF动图的各种需要。
成果演示
本文将从5个大节来率领大家应用ohos-gif-drawable这一款三方库,其中1、2、3这3个大节,次要介绍了ohos-gif-drawable的外围能力、GIF软解码和GIF绘制。4和5大节次要是扩大探讨,如何增加滤镜成果和软解码遇到的耗时问题。
1.GIF的文件格式实践根底工欲善其事必先利其器。首先咱们须要为本人打下实践根底。理解GIF的数据格式,为后续解码GIF提供实践反对。
通过学习GIF的文件格式,咱们对于GIF的组成格局有了肯定的理解,并且有助于了解前面GIF的解码。在开始介绍之前,我想让大家理解一下整体的构造思路如下图:
其中gifuct-js三方库次要实现了解码的工作。
ohos-gif-drawable三方库则是在gifuct-js的三方库之上,进行了封装。并联合了OpenHarmony的Canvas绘制能力,达到了播放和管制GIF的能力。
2.GIF软解码:gifuct-js三方库介绍
GIF解码咱们应用了gifuct-js这个库,它是一个纯JavaScript的GIF解码库。首先咱们须要理解根底用法。
2.1 参考样例将一个文件ArrayBuffer转换为GIF解码后的帧数据数组。
//javascriptvar gif = parseGIF(arraybuffer)var frames = decompressFrames(gif, true)
2.2 因为OpenHarmony的Image生成PixelMap须要的数据是BGRA数据,而2.1生成的frames所有数组中的patch字段则是RGBA数据,所以咱们须要应用
//javascriptvar gif = parseGIF(arraybuffer)var frames = decompressFrames(gif, false)
而后将frame目前还未生成的patch字段数据,通过generatePatch 函数,将RGBA的数据更换为BGRA即可,如下代码所示:
//javascriptconst generatePatch = image => { const totalPixels = image.pixels.length const patchData = new Uint8ClampedArray(totalPixels * 4) for (var i = 0; i < totalPixels; i++) { const pos = i * 4 const colorIndex = image.pixels[i] const color = image.colorTable[colorIndex] || [0, 0, 0] patchData[pos] = color[2] // B patchData[pos + 1] = color[1]// G patchData[pos + 2] = color[0] // R patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0//A } return patchData}
generatePatch函数,在这里会依据色彩表colorTable和基于色彩表的图像数据pixels以及透明度transparentIndex生成BGRA格局的patchData,这个数据和Canvas中getImageData获取的ImageData数据是统一的,都是Uint8ClampedArray类型,能够间接应用putImageData让canvas绘制。
最初,生成的patchData赋值给Frame的patch字段。
这里咱们并没有间接应用Canvas的putImageData间接绘制。为了晋升扩展性,咱们应用了Image的能力来生成PixelMap,这样解决为后续滤镜成果提供了可能,也不便后续绘制流程。
好了,到这里咱们就基本上把gifuct-js库的根底应用简略介绍完了。
如何应用GIF:ohos-gif-drawable三方库的介绍。
咱们先来看看整个ohos-gif-drawable组件的模型图,通过模型图,咱们能够看到,用户只有关注GIFComponent组件,和GIFComponent.ControllerOptions配置参数以及控制参数autoPlay和resetGif即可,非常简单!
- 反对的性能列表如下
● 反对播放GIF图片。
● 反对管制GIF播放/暂停。
● 反对重置GIF播放动画。
● 反对调节GIF播放速率。
● 反对监听GIF所有帧显示实现后的回调。
● 反对设置显示大小。
● 反对7种不同的展现类型。
● 反对设置显示区域背景色彩。 - 如何应用ohos-gif-drawable
首先须要应用npm下载ohos-gif-drawable三方库
npm install @ohos/ohos-gif-drawable --save
接下来咱们须要配置一个worker给gifuct-js解码应用。
配置worker,在利用工程的entry/src/main/ets/pages目录下新建workers文件夹,并且创立文件 gifParseWorker.ts ,文件内容如下:
import arkWorker from '@ohos.worker';import { handler } from '@ohos/ohos-gif-drawable/src/main/ets/components/gif/worker/GifWorker'// handler封装了子线程逻辑,但worker目前只能在entry中进行创立arkWorker.parentPort.onmessage = handler;
而后在entry目录的build-profile.json5文件中,增加如下内容:
"buildOption": { "sourceOption": { "workers": [ "./src/main/ets/pages/workers/gifParseWorker.ts"] }},
到这里咱们worker就配置好了。
上面就到了正式应用环节,咱们只有在UI界面须要的中央写上自定义控件GIFComponent,而后传入GIFComponent.ControllerOptions,gifAutoPlay,gifReset这三个参数就能管制gif动画。
import { GIFComponent, ResourceLoader } from '@ohos/ohos-gif-drawable'// gif绘制组件用户属性设置@State model:GIFComponent.ControllerOptions = new GIFComponent.ControllerOptions();// 是否自动播放@State gifAutoPlay:boolean = true;// 重置GIF播放,每次取反都能失效@State gifReset:boolean = true;// 在ARKUI的其余容器组件中增加该组件GIFComponent({model:$model, autoPlay:$gifAutoPlay, resetGif:this.gifReset})
举个简略的例子阐明一下
// 创立worker let worker = new ArkWorker.Worker('entry/ets/pages/workers/gifParseWorker.ts', {type: 'classic',name: 'loadUrlByWorker'})// 敞开动画 this.gifAutoPlay = false;// 销毁上一次资源this.model.destroy();// 新创建一个modelx,用于配置用户参数let modelx = new GIFComponent.ControllerOptions()modelx // 配置回调动画完结监听,和耗时监听 .setLoopFinish((loopTime) => { this.gifLoopCount++; this.loopHint = '以后gif循环了' + this.gifLoopCount + '次,耗时=' + loopTime + 'ms' }) // 设置组件大小 .setSize({ width: this.compWidth, height: this.compHeight }) // 设置图像和组件的适配类型 .setScaleType(this.scaleType) // 设置播放速率 .setSpeedFactor(this.speedFactor) // 设置背景 .setBackgroundColor(Color.Grey)// 加载网络图片,getContext(this)中的this指向page页面或者组件都能够ResourceLoader.downloadDataWithContext(getContext(this), { url: 'https://pic.ibaotu.com/gif/18/17/16/51u888piCtqj.gif!fwpaa70/fw/700' }, (sucBuffer) => { // 网络资源sucBuffer返回后处理 modelx.loadBuffer(sucBuffer, () => { console.log('网络加载解析胜利回调绘制!') // 开启自动播放 this.gifAutoPlay = true; // 给组件数据赋新的用户配置参数,达到后续gif动画成果 this.model = modelx; }, worker)}, (err) => { // 用户依据返回的错误信息,进行业务解决(展现一张失败占位图、再次加载一次、加载其余图片等)})
这里ResourceLoader内置了加载网络资源GIF,本地工程资源GIF和本地门路资源GIF文件数据的能力。
如果你曾经有了GIF文件的arraybuffer数据,也能够间接调用modelx.loadBuffer(buffer: ArrayBuffer, readyRender: (err?) => void, worker: any)进行GIF播放。
甚至你曾经生成了GIF解析数据,比方调用了2.2中的解码代码,那么你也能够间接调用modelx.setFrames(images?: GIFFrame[])来进行gif播放。
1.管制GIF的播放与暂停:
this.gifAutoPlay = true 开启动画this.gifAutoPlay = false 暂停动画
组件外部会监听该参数的变动,用户只有扭转值即可达到管制成果
重置GIF的播放
this.gifReset = !this.gifReset 每次变动都会重置gif播放。
因为重置不须要状态治理,所以组件内监听到数据变动就会重置gif播放
设置GIF动画播放速度
let modelx = new GIFComponent.ControllerOptions()modelx.setSpeedFactor(2)// 将速率晋升到2倍
调用setSpeedFactor(speed: number)即可调整播放速度speed 为比照原始速率的乘积因子,比方设置0.5即为原始速率的0.5倍,设置为2即为原始速率的2倍。
监听GIF动画播放回调(比方第一次动画完结)和获取动画理论播放总时长
let modelx = new GIFComponent.ControllerOptions()modelx.setLoopFinish((loopTime?) => {// loopTime为GIF动画一周期耗时,回调工夫为GIF动画一周期完结工夫节点})
调用setLoopFinish(fn: (loopTime?) => void)能够通过回调失去GIF动画运行一周期耗时和一周期完结工夫节点。
显示GIF任意一帧
let modelx = new GIFComponent.ControllerOptions()modelx.setSeekTo(5) // 间接展现该gif第5帧图像
调用setSeekTo(gifPosition: number)能够间接展现该gif的某一帧图像。
到这里ohos-gif-drawable三方库的次要能力都介绍完了,是不是很简略呢!
适配组件的大小
let modelx = new GIFComponent.ControllerOptions()
modelx.setScaleType(ScaleType.FIT_CENTER) // 将图像缩放适配组件大小调用setScaleType(scaletype: ScaleType)能够将图像和组件大小进行适配。
目前反对的类型如下图所示:
GIFComponent.ScaleType
为什么要配置worker
在具体实际过程中咱们会发现,当咱们按下解码按钮的时候,主界面会有一点卡顿的状况。特地是大的GIF文件进行解码的时候成果更显著。这是因为咱们在主线程中进行了CPU的密集型计算,这是一个耗时且占用CPU的操作。主线程中是不能执行耗时操作的。然而JavaScript只有一个线程啊?那么解码这一块操作该如何解决会比拟好呢?带着纳闷,我去查阅了材料发现JavaScript尽管属于单线程环境。然而通过引入Worker的能力,引入子线程worker,能够实现JavaScript的“多线程”技术。
OpenHarmony如何在子线程中解决耗时工作
为了争取良好的用户体验,咱们须要将耗时操作封装至子线程中。
这里简略形容一下worker的能力:
可能让主页面运行的JavaScript线程中加载运行另外独自的一个或者多个JavaScript线程,然而它的多线程编程能力区别于传统意义上的多线程编程。主线程和Worker线程之间,不会共享任何作用域和资源,他们的通信形式是基于事件监听机制的 message。
接下来咱们参考OpenHarmony文档下的worker能力
- OpenHarmony环境下Worker的API接口列表
- Worker的应用简略案例
通过理解之后,咱们能够把解码的耗时封装到worker中解决,防止主线程耗时操作占用CPU导致卡顿问题。晋升用户体验。
这也是应用ohos-gif-drawable三方库须要配置worker的起因。
扩大局部
GIF的滤镜成果
灰白滤镜
//javascript// 重点代码更改 let avg = (color[0] + color[1] + color[2]) / 3 patchData[pos] = avg; patchData[pos + 1] = avg; patchData[pos + 2] = avg; patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0;
反转滤镜
//javascript// 重点代码更改 patchData[pos] = 255 - color[0]; patchData[pos + 1] = 255 - color[1]; patchData[pos + 2] = 255 - color[2]; patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0;
高级滤镜
成果假如咱们这边曾经拿到了patch: Uint8ClampedArray像素数据,这里我须要先将其变换为一张PixelMap数据,参考GIFComponent中patch数据转换为PixelMap的代码。//typescriptimport image from "@ohos.multimedia.image"let colorBuffer = patch.bufferlet pixelmap = await image.createPixelMap(colorBuffer, { 'size': { 'height': frame.dims.height as number, 'width': frame.dims.width as number }})
高斯含糊
而后对PixelMap像素数据进行高斯含糊, 调用blur(pixelmap,10,true, (outPixelMap)=>{ // 含糊后的pixelmap数据})
在回调中获取含糊后的pixelmap。以下是含糊解决的算法:export async function blur(bitmap: any, radius: number, canReuseInBitmap: boolean, func: AsyncTransform<PixelMap>) { if (radius < 1) { func("error,radius must be greater than 1 ", null); return; } let imageInfo = await bitmap.getImageInfo(); let size = { width: imageInfo.size.width, height: imageInfo.size.height } if (!size) { func(new Error("fastBlur The image size does not exist."), null) return; } let w = size.width; let h = size.height; var pixEntry: Array<PixelEntry> = new Array() var pix: Array<number> = new Array() let bufferData = new ArrayBuffer(bitmap.getPixelBytesNumber()); await bitmap.readPixelsToBuffer(bufferData); let dataArray = new Uint8Array(bufferData); for (let index = 0; index < dataArray.length; index+=4) { const r = dataArray[index]; const g = dataArray[index+1]; const b = dataArray[index+2]; const f = dataArray[index+3]; let entry = new PixelEntry(); entry.a = 0; entry.b = b; entry.g = g; entry.r = r; entry.f = f; entry.pixel = ColorUtils.rgb(entry.r, entry.g, entry.b); pixEntry.push(entry); pix.push(ColorUtils.rgb(entry.r, entry.g, entry.b)); } let wm = w - 1; let hm = h - 1; let wh = w * h; let div = radius + radius + 1; let r = CalculatePixelUtils.createIntArray(wh); let g = CalculatePixelUtils.createIntArray(wh); let b = CalculatePixelUtils.createIntArray(wh); let rsum, gsum, bsum, x, y, i, p, yp, yi, yw: number; let vmin = CalculatePixelUtils.createIntArray(Math.max(w, h)); let divsum = (div + 1) >> 1; divsum *= divsum; let dv = CalculatePixelUtils.createIntArray(256 * divsum); for (i = 0; i < 256 * divsum; i++) { dv[i] = (i / divsum); } yw = yi = 0; let stack = CalculatePixelUtils.createInt2DArray(div, 3); let stackpointer, stackstart, rbs, routsum, goutsum, boutsum, rinsum, ginsum, binsum: number; let sir: Array<number>; let r1 = radius + 1; for (y = 0; y < h; y++) { rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; for (i = -radius; i <= radius; i++) { p = pix[yi + Math.min(wm, Math.max(i, 0))]; sir = stack[i + radius]; sir[0] = (p & 0xff0000) >> 16; sir[1] = (p & 0x00ff00) >> 8; sir[2] = (p & 0x0000ff); rbs = r1 - Math.abs(i); rsum += sir[0] * rbs; gsum += sir[1] * rbs; bsum += sir[2] * rbs; if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; } else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; } } stackpointer = radius; for (x = 0; x < w; x++) { r[yi] = dv[rsum]; g[yi] = dv[gsum]; b[yi] = dv[bsum]; rsum -= routsum; gsum -= goutsum; bsum -= boutsum; stackstart = stackpointer - radius + div; sir = stack[stackstart % div]; routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2]; if (y == 0) { vmin[x] = Math.min(x + radius + 1, wm); } p = pix[yw + vmin[x]]; sir[0] = (p & 0xff0000) >> 16; sir[1] = (p & 0x00ff00) >> 8; sir[2] = (p & 0x0000ff); rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; rsum += rinsum; gsum += ginsum; bsum += binsum; stackpointer = (stackpointer + 1) % div; sir = stack[(stackpointer) % div]; routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2]; yi++; } yw += w; } for (x = 0; x < w; x++) { rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; yp = -radius * w; for (i = -radius; i <= radius; i++) { yi = Math.max(0, yp) + x; sir = stack[i + radius]; sir[0] = r[yi]; sir[1] = g[yi]; sir[2] = b[yi]; rbs = r1 - Math.abs(i); rsum += r[yi] * rbs; gsum += g[yi] * rbs; bsum += b[yi] * rbs; if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; } else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; } if (i < hm) { yp += w; } } yi = x; stackpointer = radius; for (y = 0; y < h; y++) { // Preserve alpha channel: ( 0xff000000 & pix[yi] ) pix[yi] = (0xff000000 & pix[Math.round(yi)]) | (dv[Math.round(rsum)] << 16) | (dv[ Math.round(gsum)] << 8) | dv[Math.round(bsum)]; rsum -= routsum; gsum -= goutsum; bsum -= boutsum; stackstart = stackpointer - radius + div; sir = stack[stackstart % div]; routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2]; if (x == 0) { vmin[y] = Math.min(y + r1, hm) * w; } p = x + vmin[y]; sir[0] = r[p]; sir[1] = g[p]; sir[2] = b[p]; rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; rsum += rinsum; gsum += ginsum; bsum += binsum; stackpointer = (stackpointer + 1) % div; sir = stack[stackpointer]; routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2]; yi += w; } } let bufferNewData = new ArrayBuffer(bitmap.getPixelBytesNumber()); let dataNewArray = new Uint8Array(bufferNewData); let index = 0; for (let i = 0; i < dataNewArray.length; i += 4) { dataNewArray[i] = ColorUtils.red(pix[index]); dataNewArray[i+1] = ColorUtils.green(pix[index]); dataNewArray[i+2] = ColorUtils.blue(pix[index]); dataNewArray[i+3] = pixEntry[index].f; index++; } await bitmap.writeBufferToPixels(bufferNewData); if (func) { func("success", bitmap); }}
如果须要高级滤镜成果能够参考ImageKnife组件的transform局部,这里仅仅展现含糊成果。
因为滤镜成果目前ohos-gif-drawable三方库并没有开发接口提供进去,所以开发者能够依据理论需要重写自定义组件GIFComponent.,只须要在生成PixelMap的代码片段中退出滤镜代码,即可利用滤镜成果开发更多精彩的利用。
参考资料
1.《GIF文件格式解析》https://segmentfault.com/a/11...
2.GIF解码库gifuct-jshttps://github.com/matt-way/g...
3.GIF解码库底层逻辑jsBinarySchemaParserhttps://github.com/matt-way/j...
4.高级滤镜算法借鉴https://gitee.com/openharmony...
5.OpenHarmony环境下Worker的API接口列表https://gitee.com/openharmony...
6.Worker的应用简略案例https://gitee.com/wang_zhaoyo...
7.Web Worker API参考https://developer.mozilla.org...
8.OpenHarmony的Canvas文档https://gitee.com/openharmony...
9.OpenHarmony的CanvasRenderingContext2D对象文档https://gitee.com/openharmony...