2022-04-29 ImageKnife组件,让小白也能轻松搞定图片开发

# 开发者说 #

【开发者说】栏目是为HarmonyOS开发者提供的展示和分享平台,在这里,大家可以发表自己的技术洞察和见解,也可以展示自己的开发心得和成果。

本期我们给大家带来的是开发者周黎生的分享,希望能给你的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),正式启动图片加载任务。相关实现代码如下:

imageKnifeExecute() {

// 首先需要确保获取ImageKnife单例对象

if(ImageKnife){

}else{

ImageKnife = globalThis.exports.default.data.imageKnife;

}

// 生成配置信息requestOption

let request = new RequestOption();

// 配置必要信息和回调

this.configNecessary(request);

// 配置缓存相关信息

this.configCacheStrategy(request);

// 配置显示信息和回调

this.configDisplay(request);

// 启动ImageKnife执行请求

ImageKnife.call(request);

}


2. 加载图片

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


图4 图片加载过程

(1) 占位图填充

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

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

图5 占位图工作流程

相关实现代码如下:

// 占位图解析成功

placeholderOnComplete(imageKnifeData: ImageKnifeData) {

// 主图未加载成功,并且未加载失败 显示占位图 主图加载成功或者加载失败后=>不展示占位图

if (!this.loadMainReady && !this.loadErrorReady && !this.loadThumbnailReady) {

this.placeholderFunc(imageKnifeData)

}

}

// 加载失败 占位图解析成功

errorholderOnComplete(imageKnifeData: ImageKnifeData) {

// 如果有错误占位图 先解析并保存在RequestOption中 等到加载失败时候进行调用

this.errorholderData = imageKnifeData;

if (this.loadErrorReady) {

this.errorholderFunc(imageKnifeData)

}

}


(2) 缓存实现

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

图6 缓存策略API

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

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

图7 图片缓存过程

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

① 内存缓存

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

ImageKnife内存缓存的实现,需控制最大空间(maxsize),以及目前占用空间(size),相关实现代码如下: 

// 移除较少使用的缓存数据

trimToSize(tempsize: number) {

while (true) {

if (tempsize < 0) {

this.map.clear()

this.size = 0

break

}

if (this.size <= tempsize || this.map.isEmpty()) {

break

}

var delkey = this.map.getFirstKey()

this.map.remove(delkey)

this.size--

}

}

// 缓存数据最大值

maxSize(): number{

return this.maxsize

}

// 设置缓存数据量最大值

resize(maxsize: number) {

if (maxsize < 0) {

throw new Error('maxsize <0 & maxsize invalid');

}

this.maxsize = maxsize

this.trimToSize(maxsize)

}

// 清除缓存

evicAll() {

this.trimToSize(-1)

}


② 磁盘缓存

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

相关实现代码如下: 

//读取journal文件的缓存数据

readJournal(path: string) {

var fileReader = new FileReader(path)

var line: string = ''

while (!fileReader.isEnd()) {

line = fileReader.readLine()

line = line.replace('\n', '').replace('\r', '')

this.dealwithJournal(line)

}

this.fileUtils.deleteFile(this.journalPathTemp)

this.trimToSize()

}

//根据LRU算法删除多余缓存数据

private trimToSize() {

while (this.size > this.maxSize) {

var tempkey: string = this.cacheMap.getFirstKey()

var fileSize = this.fileUtils.getFileSize(this.dirPath + tempkey)

if (fileSize > 0) {

this.size = this.size - fileSize

}

this.fileUtils.deleteFile(this.dirPath + tempkey)

this.cacheMap.remove(tempkey)

this.fileUtils.writeData(this.journalPath, 'remove ' + tempkey + '\n')

}

}

//清除所有disk缓存数据

cleanCacheData() {

var length = this.cacheMap.size()

for (var index = 0; index < length; index++) {

this.fileUtils.deleteFile(this.dirPath + this.cacheMap[index])

}

this.fileUtils.deleteFile(this.journalPath)

this.cacheMap.clear() this.size = 0

}


(3) 图片解码

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

ImageKnife的图片解码能力依赖的是ArkUI开发框架提供的ImageSource解码能力。通过import image from '@ohos.multimedia.image'导入ArkUI开发框架的图片能力,并调用createImageSource()方法获取,实现代码如下:

import image from '@ohos.multimedia.image'

export class TransformUtils {

static centerCrop(buf: ArrayBuffer, outWidth: number,outHeihgt: number,

callback?: AsyncTransform<Promise<PixelMap>>){

// 创建媒体解码imageSource

var imageSource = image.createImageSource(buf as any);

// 获取图片信息

imageSource.getImageInfo()

.then((p) => {

var sw;

var sh;

var scale;

var pw = p.size.width;

var ph = p.size.height;

// 根据centerCrop规则控制缩放比例

if (pw == outWidth && ph == outHeihgt) {

sw = outWidth;

sh = outHeihgt;

} else {

if (pw * outHeihgt > outWidth * ph) {

scale = outHeihgt / ph;

} else {

scale = outWidth / pw;

} sw = pw * scale;

sh = ph * scale;

}

var options = {

editable: true,

rotate: 0,

desiredRegion: { size: { width: sw, height: sh },

x: pw / 2 - sw / 2,

y: ph / 2 - sh / 2,

},

}

if (callback) {

// 回调,创建相关配置pixelmap

callback('', imageSource.createPixelMap(options));

}

})

.catch((error) => {

callback(error, null);

})

}

}


3. 显示图片

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

相关代码如下:

@Component

export struct ImageKnifeComponent {

@Watch('watchImageKnifeOption')

@Link imageKnifeOption: ImageKnifeOption;

@State imageKnifePixelMapPack: PixelMapPack = new PixelMapPack();

@State imageKnifeResource: Resource = $r('app.media.icon_loading')

@State imageKnifeString: string = ''

@State normalPixelMap: boolean = false;

@State normalResource: boolean = true;

previousData: ImageKnifeData = null;

  nowData: ImageKnifeData = null

; build() {

Stack() {

//Image组件配置

Image(this.normalPixelMap ? this.imageKnifePixelMapPack.pixelMap : (this.normalResource ? this.imageKnifeResource : this.imageKnifeString))

.objectFit(this.imageKnifeOption.imageFit ? this.imageKnifeOption.imageFit : ImageFit.Fill)

.visibility(this.imageVisible)

.width(this.imageWidth)

.height(this.imageHeight)

}

}

//必要的用户配置和回调方法

configNecessary(request: RequestOption){

request.load(this.imageKnifeOption.loadSrc)

.addListener((err, data) => {

console.log('request.load callback')

this.imageKnifeChangeSource(data)

this.animateTo('image');

return false; 

     })

if (this.imageKnifeOption.size) {

request.setImageViewSize(this.imageKnifeOption.size)

}

}

// imageknife 第一次启动和数据刷新后重新发送请求

imageKnifeExecute() {

let request = new RequestOption();

this.configNecessary(request);

this.configCacheStrategy(request);

this.configDisplay(request);

ImageKnife.call(request);

}

//返回数据Image渲染展示图片

imageKnifeSpecialFixed(data:ImageKnifeData) {

if (data.isPixelMap()) {

this.displayPixelMap(data);

}

else if (data.isString()) {

this.displayString(data);

} else if (data.isResource()) {

this.displayResource(data);

} else {

}

}

}

注:@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()进行初始化,相关代码如下:

import {ImageKnife} from '@ohos/imageknife'

export default {

data: {

imageKnife: {} // ImageKnife

},

onCreate() {

this.data.imageKnife = ImageKnife.with();

},

onDestroy() {

},

}


然后在页面index.ets中使用ImageKnife,相关代码如下:

@Entry

@Component

struct Index {

  build() {

  }

  // 页面初始化完成,生命周期回调函数中 进行调用ImageKnife

aboutToAppear() {

let requestOption = new RequestOption();

requestOptin.load($r('app.media.IceCream'))

.addListener((err,data) => {

//加载成功/失败回调监听

})

...

ImageKnife.call(requestOption)

}

}

var ImageKnife;

var defaultTemp = globalThis.exports.default

if(defaultTemp != undefined) {

ImageKnife = defaultTemp.data.imageKnife;

}


方式二: 在index.ets中,直接使用ImageKnifeOption作为入参,并配合自定义组件ImageKnifeComponent使用,相关代码如下:

import {ImageKnifeOption} from '@ohos/imageknife'

@Entry

@Component

struct Index {

@State imageKnifeOption1: ImageKnifeOption =

{

loadSrc: $r('app.media.gifSample'),

size: { width: 300, height: 300 },

placeholderSrc: $r('app.media.icon_loading'),

errorholderSrc: $r('app.media.icon_failed')

};

build() {

Scroll() {

Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {

ImageKnifeComponent({ imageKnifeOption: $imageKnifeOption1 })

}

}

.width('100%')

.height('100%')

}

}


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

END

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容