[文章]ImageKnife组件,让小白也能轻松搞定图片开发

阅读量0
1
0
本期我们给大家带来的是开发者周黎生的分享,希望能给你的HarmonyOS开发之旅带来启发~

图片是UI界面的重要元素之一, 图片加载速度及效果直接影响应用体验。ArkUI开发框架提供了丰富的图像处理能力,如图像解码、图像编码、图像编辑及基本的位图操作等,满足了开发者日常开发所需。

但随着产品需求的日益增长,基本的图像处理能力已不能胜任某些比较复杂的应用场景,如无法直接获取缓存图片、无法配置占位图、无法进行自定义PixelMap图片变换等。

为增强ArkUI开发框架的图像处理能力,ImageKnife组件应运而生。本期我们将为大家带来ImageKnife的介绍。

一、ImageKnife简介

ImageKnife是一个参考Glide框架进行设计,并基于eTS语言实现的图片处理组件。它可以让开发者能轻松且高效地进行图片开发。

注:Glide是一个快速高效的图片加载库,注重于平滑的滚动,提供了易用的API,高性能、可扩展的图片解码管道,以及自动的资源池威廉希尔官方网站 。


  • 功能方面,ImageKnife提供了自定义图片变换、占位图等图片处理能力,几乎满足了开发者进行图片处理的一切需求。
  • 性能方面,ImageKnife采用LRU策略实现二级缓存,可灵活配置,有效减少内存消耗,提升了应用性能。
  • 使用方面,ImageKnife封装了一套完整的图片加载流程,开发者只需根据ImageKnifeOption配置相关信息即可完成图片的开发,降低了开发难度,提升了开发效率。

如图1所示,是ImageKnife加载图片的整体流程。

图1 ImageKnife加载图片整体流程

二、ImageKnife实现原理

下面我们将为大家介绍ImageKnife加载图片过程中每个环节的实现原理,让大家更深刻地认识ImageKnife组件。图2是ImageKnife加载图片的时序图:

图2 ImageKnife加载图片的时序图

1. 用户配置信息

在加载图片前,用户需根据自身需求配置相应的参数,包括图片路径、图片大小、占位图及缓存策略等。ImageKnife提供了RequestOption类,用于封装用户配置信息的接口,如图3所示列举了部分接口供大家参考:

图3 用户配置参数

通过ImageKnifeExecute()方法获取用户配置信息,然后执行ImageKnife.call(request),正式启动图片加载任务。相关实现代码如下:
  1. imageKnifeExecute() {
  2.   // 首先需要确保获取ImageKnife单例对象
  3.   if(ImageKnife){
  4.   }else{
  5.     ImageKnife = globalThis.exports.default.data.imageKnife;
  6.   }
  7.   // 生成配置信息requestOption
  8.   let request = new RequestOption();
  9.   // 配置必要信息和回调
  10.   this.configNecessary(request);
  11.   // 配置缓存相关信息   
  12.   this.configCacheStrategy(request);
  13.   // 配置显示信息和回调
  14.   this.configDisplay(request);
  15.   // 启动ImageKnife执行请求
  16.   ImageKnife.call(request);
  17. }
复制代码

2. 加载图片

加载图片过程是ImageKnife组件的核心部分,如图4所示,包含占位图填充、缓存实现及图片解码三个环节。下面我们将为大家分别介绍每个环节的实现。

图4图片加载过程

(1) 占位图填充

占位图就是图片加载过程中页面上的过渡效果,通常表现形式是在页面上待加载区域填充灰色的占位图,可以使得页面框架不会因为加载失败而变形。ImageKnife提供了占位图功能,开发者可在RequestOption中配置是否启动占位图任务。

如图5所示是占位图工作流程,执行图片加载任务后,占位图会填充加载页面。如果图片解析成功则将页面上填充的占位图替换为待加载的图片。如果图片解析失败,则将页面上填充的占位图替换为“图片解析失败占位图”。

图5 占位图工作流程

相关实现代码如下:
  1. // 占位图解析成功
  2. placeholderOnComplete(imageKnifeData: ImageKnifeData) {
  3. // 主图未加载成功,并且未加载失败  显示占位图  主图加载成功或者加载失败后=>不展示占位图
  4.   if (!this.loadMainReady && !this.loadErrorReady && !this.loadThumbnailReady) {
  5.         this.placeholderFunc(imageKnifeData)
  6.   }
  7. }
  8. // 加载失败 占位图解析成功
  9. errorholderOnComplete(imageKnifeData: ImageKnifeData) {
  10.   // 如果有错误占位图 先解析并保存在RequestOption中 等到加载失败时候进行调用
  11.   this.errorholderData = imageKnifeData;
  12.   if (this.loadErrorReady) {
  13.     this.errorholderFunc(imageKnifeData)
  14.   }
  15. }
复制代码

(2) 缓存实现

缓存是图片加载过程中最关键的环节,缓存机制直接影响了图片加载速度及图片滚动效果。开发者可通过以下方法来灵活配置缓存策略。

图6 缓存策略API

为了保障图片的加载速度,ImageKnife通过使用Least Recently Used(最近最少使用)清空策略来实现内存缓存及磁盘缓存。

如图7所示,在图片加载过程中,CPU会首先读取内存缓存中的数据,如果读取到图片资源则直接显示图片,否则读取磁盘缓存数据。如果在磁盘缓存上仍然没有读取到数据,则可判定为该图片为网络图片,这时需要将网络图片解码后再进行显示(后面章节会详细介绍),并将解码后的图片文件缓存至磁盘。

图7 图片缓存过程

下面我们将分别介绍两种缓存机制的具体实现:

① 内存缓存

内存缓存,就是指当前程序运行内存分配的临时存储器,当我们使用ImageKnife加载图片时,这张图片会被缓存到内存当中,只要在它还没从内存中被清除之前,下次再加载这张图片都会直接从内存中读取,而不用重新从网络或硬盘上读取,大幅度提升图片的加载效率。

ImageKnife内存缓存的实现,需控制最大空间(maxsize),以及目前占用空间(size),相关实现代码如下:  
  1. // 移除较少使用的缓存数据
  2. trimToSize(tempsize: number) {
  3.   while (true) {
  4.     if (tempsize < 0) {
  5.       this.map.clear()
  6.       this.size = 0
  7.       break
  8.     }
  9.     if (this.size <= tempsize || this.map.isEmpty()) {
  10.       break
  11.     }
  12.     var delkey = this.map.getFirstKey()
  13.     this.map.remove(delkey)
  14.     this.size--
  15.   }
  16. }
  17. // 缓存数据最大值
  18. maxSize(): number{
  19.   return this.maxsize
  20. }
  21. // 设置缓存数据量最大值
  22. resize(maxsize: number) {
  23.   if (maxsize < 0) {
  24.     throw new Error('maxsize <0 & maxsize invalid');
  25.   }
  26.   this.maxsize = maxsize
  27.   this.trimToSize(maxsize)
  28. }
  29. // 清除缓存
  30. evicAll() {
  31.   this.trimToSize(-1)
  32. }
复制代码

② 磁盘缓存

默认情况下,磁盘缓存的是解码后的图片文件,需防止应用重复从网络或其他地方下载和读取数据。ImageKnife磁盘缓存的实现,主要依靠journal文件对缓存数据进行保存,保证程序磁盘缓存内容的持久化问题。

相关实现代码如下:
  1. //读取journal文件的缓存数据
  2. readJournal(path: string) {
  3.   var fileReader = new FileReader(path)
  4.   var line: string = ''
  5.   while (!fileReader.isEnd()) {
  6.     line = fileReader.readLine()
  7.     line = line.replace('
  8. ', '').replace('
  9. ', '')
  10.     this.dealwithJournal(line)
  11.   }
  12.   this.fileUtils.deleteFile(this.journalPathTemp)
  13.   this.trimToSize()
  14. }
  15. //根据LRU算法删除多余缓存数据
  16. private trimToSize() {
  17.   while (this.size > this.maxSize) {
  18.     var tempkey: string = this.cacheMap.getFirstKey()
  19.     var fileSize = this.fileUtils.getFileSize(this.dirPath + tempkey)
  20.     if (fileSize > 0) {
  21.       this.size = this.size - fileSize
  22.     }
  23.     this.fileUtils.deleteFile(this.dirPath + tempkey)
  24.     this.cacheMap.remove(tempkey)
  25.     this.fileUtils.writeData(this.journalPath, 'remove ' + tempkey + '
  26. ')
  27.   }
  28. }
  29. //清除所有disk缓存数据
  30. cleanCacheData() {
  31.   var length = this.cacheMap.size()
  32.   for (var index = 0; index < length; index++) {
  33.     this.fileUtils.deleteFile(this.dirPath + this.cacheMap[index])
  34.   }
  35.   this.fileUtils.deleteFile(this.journalPath)
  36.   this.cacheMap.clear()
  37.   this.size = 0
  38. }
复制代码

(3) 图片解码

当我们使用ImageKnife去加载一张图片的时候,并不是将原始图片直接显示出来,而是会进行图片解码后再显示到页面。图片解码就是将不同格式的图片(包括JPEG、PNG、GIF、WebP、BMP)解码成统一格式的PixelMap图片文件。

ImageKnife的图片解码能力依赖的是ArkUI开发框架提供的ImageSource解码能力。通过import image from \'@ohos.multimedia.image\'导入ArkUI开发框架的图片能力,并调用createImageSource()方法获取,实现代码如下:
  1. import image from '@ohos.multimedia.image'
  2. export class TransformUtils {
  3.   static centerCrop(buf: ArrayBuffer, outWidth: number, outHeihgt: number,
  4.                     callback?: AsyncTransform<Promise<PixelMap>>) {
  5.     // 创建媒体解码imageSource
  6.     var imageSource = image.createImageSource(buf as any);
  7.     // 获取图片信息
  8.     imageSource.getImageInfo()
  9.       .then((p) => {
  10.         var sw;
  11.         var sh;
  12.         var scale;
  13.         var pw = p.size.width;
  14.         var ph = p.size.height;
  15.         // 根据centerCrop规则控制缩放比例
  16.         if (pw == outWidth && ph == outHeihgt) {
  17.           sw = outWidth;
  18.           sh = outHeihgt;
  19.         } else {
  20.           if (pw * outHeihgt > outWidth * ph) {
  21.             scale = outHeihgt / ph;
  22.           } else {
  23.             scale = outWidth / pw;
  24.           }
  25.           sw = pw * scale;
  26.           sh = ph * scale;
  27.         }
  28.         var options = {
  29.           editable: true,
  30.           rotate: 0,
  31.           desiredRegion: { size: { width: sw, height: sh },
  32.             x: pw / 2 - sw / 2,
  33.             y: ph / 2 - sh / 2,
  34.           },
  35.         }
  36.         if (callback) {
  37.           // 回调,创建相关配置pixelmap
  38.           callback('', imageSource.createPixelMap(options));
  39.         }
  40.       })
  41.       .catch((error) => {
  42.         callback(error, null);
  43.       })
  44.   }
  45. }
复制代码

3. 显示图片

获取到PixelMap解码文件后,接下来就是将它渲染到应用界面上。ImageKnife的图片渲染能力依赖的是ArkUI开发框架提供的Image组件的渲染能力。由于eTS是声明式的,我们无法直接获得Image组件的对象,需要依赖ArkUI开发框架的@State能力绑定输入参数,在改变属性对象之后,通知UI组件重新渲染,达到图片显示的效果。

相关代码如下:
  1. @Component
  2. export struct ImageKnifeComponent {
  3.   @Watch('watchImageKnifeOption') [url=home.php?mod=space&uid=41289]@Link[/url] imageKnifeOption: ImageKnifeOption;
  4.   @State imageKnifePixelMapPack: PixelMapPack = new PixelMapPack();
  5.   @State imageKnifeResource: Resource = $r('app.media.icon_loading')
  6.   @State imageKnifeString: string = ''
  7.   @State normalPixelMap: boolean = false;
  8.   @State normalResource: boolean = true;
  9.   previousData: ImageKnifeData = null;
  10.   nowData: ImageKnifeData = null;
  11.   build() {
  12.     Stack() {
  13.       //Image组件配置
  14.       Image(this.normalPixelMap ? this.imageKnifePixelMapPack.pixelMap : (this.normalResource ? this.imageKnifeResource : this.imageKnifeString))
  15.         .objectFit(this.imageKnifeOption.imageFit ? this.imageKnifeOption.imageFit : ImageFit.Fill)
  16.         .visibility(this.imageVisible)
  17.         .width(this.imageWidth)
  18.         .height(this.imageHeight)
  19.     }
  20.   }
  21.   //必要的用户配置和回调方法
  22.   configNecessary(request: RequestOption){
  23.     request.load(this.imageKnifeOption.loadSrc)
  24.       .addListener((err, data) => {
  25.         console.log('request.load callback')
  26.         this.imageKnifeChangeSource(data)
  27.         this.animateTo('image');
  28.         return false;
  29.       })
  30.     if (this.imageKnifeOption.size) {
  31.       request.setImageViewSize(this.imageKnifeOption.size)
  32.     }
  33.   }
  34.   // imageknife 第一次启动和数据刷新后重新发送请求
  35.   imageKnifeExecute() {
  36.     let request = new RequestOption();
  37.     this.configNecessary(request);
  38.     this.configCacheStrategy(request);
  39.     this.configDisplay(request);
  40.     ImageKnife.call(request);
  41.   }
  42.   //返回数据Image渲染展示图片
  43.   imageKnifeSpecialFixed(data:ImageKnifeData) {
  44.     if (data.isPixelMap()) {
  45.       this.displayPixelMap(data);
  46.     }
  47.     else if (data.isString()) {
  48.       this.displayString(data);
  49.     } else if (data.isResource()) {
  50.       this.displayResource(data);
  51.     } else {
  52.     }
  53.   }
  54. }
复制代码

注:@State装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法进行UI刷新。

三、ImageKnife实战

通过上文的介绍,相信大家对ImageKnife组件有了深刻的了解。下面我们将创建一个ImageKnife_Test项目,为大家展示ArkUI开发框架中ImageKnife组件的使用。

通过将ImageKnife组件下载至项目中,然后根据ImageKnifeOption配置相关信息,即可完成GIF图片的加载。

1. 创建项目

如图8所示,在DevEco Studio中新建ImageKnife_Test项目,项目类型选择Application,语言选择eTS,点击Finish完成创建。

图8 创建项目

2. 添加依赖

成功创建项目后,接下来就是将ImageKnife组件下载至项目中。

首先,我们需找到.npmrc 配置文件,并在文件中添加 @ohos 的scope仓库地址:@ohos:registry=https://repo.harmonyos.com/npm/,如图9所示:

图9 添加 scope仓库地址

配置好npm仓库地址后,如图10所示,在DevEco Studio的底部导航栏,点击“Terminal”(快捷键Alt+F12),键入命令:npm install @ohos/imageknife并回车,此时ImageKnife组件会被自动下载至项目中。下载完成后工程根目录下会生成node_modules/@ohos/imageknife目录。

图10 下载至项目

3. 编写逻辑代码

ImageKnife组件成功下载至项目中后,接下来就是逻辑代码编写,这里我们将为大家介绍两种使用方式:

方式一:首先初始化全局ImageKnife实例,然后在app.ets中调用ImageKnife.with()进行初始化。相关代码如下:
  1. <pre class="language-markup"><code>import {ImageKnife} from '@ohos/imageknife'
  2. export default {
  3.   data: {
  4.     imageKnife: {} // ImageKnife
  5.   },
  6.   onCreate() {
  7.     this.data.imageKnife = ImageKnife.with();
  8.   },
  9.   onDestroy() {
  10.   },
  11. }</code></pre>
  12. <p></p>
复制代码
然后在页面index.ets中使用ImageKnife,相关代码如下:
  1. @Entry
  2. @Component
  3. struct Index {
  4.   build() {
  5.   }
  6.   // 页面初始化完成,生命周期回调函数中 进行调用ImageKnife
  7.   aboutToAppear() {
  8.     let requestOption = new RequestOption();
  9.   requestOptin.load($r('app.media.IceCream'))
  10.   .addListener((err,data) => {
  11.       //加载成功/失败回调监听
  12.     })
  13.     ...
  14.   ImageKnife.call(requestOption)
  15.   }
  16. }
  17. var ImageKnife;
  18. var defaultTemp = globalThis.exports.default
  19. if (defaultTemp != undefined) {
  20.   ImageKnife = defaultTemp.data.imageKnife;
  21. }
复制代码

方式二:在index.ets中,直接使用ImageKnifeOption作为入参,并配合自定义组件ImageKnifeComponent使用。相关代码如下:
  1. import {ImageKnifeOption} from '@ohos/imageknife'
  2. @Entry
  3. @Component
  4. struct Index {
  5.   @State imageKnifeOption1: ImageKnifeOption =
  6.     {
  7.       loadSrc: $r('app.media.gifSample'),
  8.       size: { width: 300, height: 300 },
  9.       placeholderSrc: $r('app.media.icon_loading'),
  10.       errorholderSrc: $r('app.media.icon_failed')
  11.     };
  12.   build() {
  13.     Scroll() {
  14.       Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
  15.         ImageKnifeComponent({ imageKnifeOption: $imageKnifeOption1 })
  16.       }
  17.     }
  18.     .width('100%')
  19.     .height('100%')
  20.   }
  21. }
复制代码

以上就是本期全部内容,恭喜大家花几分钟时间收获了一个实用的组件。希望广大开发者能利用这个强大的开源组件开发出更多精美的应用。



回帖

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题,请联系本站作侵删。 侵权投诉
链接复制成功,分享给好友