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

HarmonyOS開發者社群發表於2022-04-28

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


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

 

 

相關文章