ImageKnife組件,讓小白也能輕鬆搞定圖片開發

本期我們給大家帶來的是開發者周黎生的分享,希望能給你的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=//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%')
  }
}

以上就是本期全部內容,恭喜大家花幾分鐘時間收穫了一個實用的組件。希望廣大開發者能利用這個強大的開源組件開發出更多精美的應用。