diff --git a/CHANGELOG.md b/CHANGELOG.md index 3260d3f..d8b8b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - imageKnife支持heic测试demo独立页面展示 - drawLifeCycle支持gif图 - ImageKnife支持根据自定义key获取已缓存的图片 +- ImageKnife加载图片支持自定义网络栈和图片加载组件 ## 2.1.2-rc.12 - 新增gif播放次数功能 diff --git a/entry/src/main/ets/pages/imageknifeTestCaseIndex.ets b/entry/src/main/ets/pages/imageknifeTestCaseIndex.ets index bb3d5ec..6bb39fb 100644 --- a/entry/src/main/ets/pages/imageknifeTestCaseIndex.ets +++ b/entry/src/main/ets/pages/imageknifeTestCaseIndex.ets @@ -404,6 +404,14 @@ struct IndexFunctionDemo { router.pushUrl({ url: 'pages/testImageKnifeNetPlaceholder' }); }).margin({ top: 5, left: 3 }) }.width('100%').height(60).backgroundColor(Color.Pink) + + Text('测试加载图片自定义网络栈').fontSize(15) + Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + Button('加载图片自定义网络栈') + .onClick(() => { + router.pushUrl({ url: 'pages/testCustomDataFetchClientWithPage' }); + }).margin({ top: 5, left: 3 }) + }.width('100%').height(60).backgroundColor(Color.Pink) } } .width('100%') diff --git a/entry/src/main/ets/pages/testCustomDataFetchClientWithPage.ets b/entry/src/main/ets/pages/testCustomDataFetchClientWithPage.ets new file mode 100644 index 0000000..c2a82f0 --- /dev/null +++ b/entry/src/main/ets/pages/testCustomDataFetchClientWithPage.ets @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2024 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + CustomDataFetchClient, + DataFetchResult, + DownloadClient, + ImageKnifeComponent, + ImageKnifeGlobal, + ImageKnifeOption, + LogUtil, + ScaleType +} from '@ohos/libraryimageknife'; + +import http from '@ohos.net.http'; + +class CommonDataSource implements IDataSource { + private dataArray: T[] = [] + private listeners: DataChangeListener[] = [] + + constructor(element: []) { + this.dataArray = element + } + + public getData(index: number) { + return this.dataArray[index] + } + + public totalCount(): number { + return this.dataArray.length + } + + public addData(index: number, data: T[]): void { + this.dataArray = this.dataArray.concat(data) + this.notifyDataAdd(index) + } + + unregisterDataChangeListener(listener: DataChangeListener): void { + const pos = this.listeners.indexOf(listener); + if (pos >= 0) { + this.listeners.splice(pos, 1); + } + } + + registerDataChangeListener(listener: DataChangeListener): void { + if (this.listeners.indexOf(listener) < 0) { + this.listeners.push(listener) + } + } + + notifyDataAdd(index: number): void { + this.listeners.forEach((listener: DataChangeListener) => { + listener.onDataAdd(index) + }) + } +} + +@Entry +@Component +struct TestCustomDataFetchClientWithPage { + @State hotCommendList: CommonDataSource = new CommonDataSource([]) + @State singleImageKnifeOption: ImageKnifeOption = + { + loadSrc: $r('app.media.icon'), + placeholderSrc: $r('app.media.icon_loading'), + errorholderSrc: $r('app.media.icon_failed'), + }; + @State isSingleImageVisible: boolean = true; + @State isAllImageVisible: boolean = false; + @State isCustom: boolean = false; + private data: Array = [ + "http://img2.xkhouse.com/bbs/hfhouse/data/attachment/forum/corebbs/2009-11/2009113011534566298.jpg", + "http://a.hiphotos.baidu.com/image/pic/item/e824b899a9014c087eb617650e7b02087af4f464.jpg" + ] + private addData: Array = [ + "http://c.hiphotos.baidu.com/image/pic/item/9c16fdfaaf51f3de1e296fa390eef01f3b29795a.jpg", + "http://h.hiphotos.baidu.com/image/pic/item/902397dda144ad340668b847d4a20cf430ad851e.jpg" + + ] + private cancelData: Array = [ + "http://b.hiphotos.baidu.com/image/pic/item/359b033b5bb5c9ea5c0e3c23d139b6003bf3b374.jpg", + "http://a.hiphotos.baidu.com/image/pic/item/8d5494eef01f3a292d2472199d25bc315d607c7c.jpg" + ] + + aboutToAppear(): void { + this.hotCommendList.addData(this.hotCommendList.totalCount(), this.data) + LogUtil.log('TestCustomDataFetch about to appear.') + } + + build() { + Scroll() { + Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + + Button("单个图片").margin(16).onClick(() => { + LogUtil.log('TestCustomDataFetch click single.'); + this.isSingleImageVisible = true; + this.isAllImageVisible = false; + ImageKnifeGlobal.getInstance().getImageKnife()?.replaceDataFetch(new CustomDataFetchClient()); + + this.singleImageKnifeOption = { + loadSrc: 'http://e.hiphotos.baidu.com/image/pic/item/4e4a20a4462309f7e41f5cfe760e0cf3d6cad6ee.jpg', + placeholderSrc: $r('app.media.icon_loading'), + errorholderSrc: $r('app.media.icon_failed'), + customGetImage: custom + } + }) + Button("全部图片").margin(16).onClick(() => { + LogUtil.log('TestCustomDataFetch click all.'); + this.isSingleImageVisible = false; + this.isAllImageVisible = true; + + ImageKnifeGlobal.getInstance().getImageKnife()?.replaceDataFetch(new CustomDataFetchClient()); + this.hotCommendList.addData(this.hotCommendList.totalCount(), this.addData) + + }) + + Button("取消自定义全部图片").margin(16).onClick(() => { + LogUtil.log('TestCustomDataFetch click cancel.'); + this.isSingleImageVisible = false; + this.isAllImageVisible = true; + + ImageKnifeGlobal.getInstance().getImageKnife()?.replaceDataFetch(new DownloadClient()); + this.hotCommendList.addData(this.hotCommendList.totalCount(), this.cancelData) + }) + } + + // 单个图片使用自定义网络栈 + ImageKnifeComponent({ imageKnifeOption: this.singleImageKnifeOption }) + .width(200) + .height(200) + .margin({ top: 50 }) + .visibility(this.isSingleImageVisible ? Visibility.Visible : Visibility.None) + + // 全部图片使用自定义网络栈 + Column() { + Grid() { + LazyForEach(this.hotCommendList, (item: string) => { + GridItem() { + ImageKnifeComponent({ + imageKnifeOption: { + loadSrc: item, + placeholderSrc: $r('app.media.icon'), + errorholderSrc: $r('app.media.icon_failed'), + placeholderScaleType: ScaleType.CENTER_CROP, + mainScaleType: ScaleType.CENTER_CROP, + } + }).width('100%').height('100%') + }.width('40%').height(200) + }, (item: string) => JSON.stringify(item)) + } + .columnsTemplate('1fr 1fr') + .columnsGap(8) + .rowsGap(10) + .width('100%') + .hitTestBehavior(HitTestMode.None) + .maxCount(10) + }.margin({ top: 5 }) + .visibility(this.isAllImageVisible ? Visibility.Visible : Visibility.None) + } + } + .width('100%') + .height('100%') + } +} + +@Concurrent +async function custom(context: Context, loadSrc: string): Promise { + let result: DataFetchResult = new DataFetchResult(); + try { + let arrayBuffers = new Array(); + let httpRequest = http.createHttp() + httpRequest.on('headersReceive', (header: Object) => { + // 跟服务器连接成功准备下载 + }) + httpRequest.on('dataReceive', (data: ArrayBuffer) => { + // 下载数据流多次返回 + arrayBuffers.push(data); + }) + httpRequest.on('dataEnd', () => { + // 下载完毕 + }) + + const resultCode = await httpRequest.requestInStream(loadSrc as string, + { + method: http.RequestMethod.GET, + expectDataType: http.HttpDataType.ARRAY_BUFFER, + connectTimeout: 60000, // 可选 默认60000ms + readTimeout: 0, // 可选, 默认为60000ms + usingProtocol: http.HttpProtocol.HTTP1_1, // 可选,协议类型默认值由系统自动指定 + usingCache: false + }).catch((err: Error) => { + result.error = 'TestCustomDataFetchClientWithPage requestInStream error.' + JSON.stringify(err); + }) + if (resultCode == 200) { + //let combineArray = this.combineArrayBuffers(arrayBuffers); + // 计算多个ArrayBuffer的总字节大小 + let totalByteLength = 0; + for (const arrayBuffer of arrayBuffers) { + totalByteLength += arrayBuffer.byteLength; + } + // 创建一个新的ArrayBuffer + const combinedArrayBuffer = new ArrayBuffer(totalByteLength); + + // 创建一个Uint8Array来操作新的ArrayBuffer + const combinedUint8Array = new Uint8Array(combinedArrayBuffer); + + // 依次复制每个ArrayBuffer的内容到新的ArrayBuffer中 + let offset = 0; + for (const arrayBuffer of arrayBuffers) { + const sourceUint8Array = new Uint8Array(arrayBuffer); + combinedUint8Array.set(sourceUint8Array, offset); + offset += sourceUint8Array.length; + } + result.data = combinedArrayBuffer; + } else { + result.error = 'TestCustomDataFetchClientWithPage error. resultCode = ' + resultCode; + } + console.log('TestCustomDataFetch single onComplete, code = ' + resultCode + ',length = ' + result.data?.byteLength); + } catch (error) { + result.error = 'TestCustomDataFetchClientWithPage error' + error.stack; + } + return result; +} + + diff --git a/entry/src/main/resources/base/profile/main_pages.json b/entry/src/main/resources/base/profile/main_pages.json index f0b6f5d..6d5811e 100644 --- a/entry/src/main/resources/base/profile/main_pages.json +++ b/entry/src/main/resources/base/profile/main_pages.json @@ -54,6 +54,7 @@ "pages/testStopPlayingGifPage", "pages/testImageKnifeDataFetch", "pages/testImageKnifeHeic", - "pages/testImageKnifeNetPlaceholder" + "pages/testImageKnifeNetPlaceholder", + "pages/testCustomDataFetchClientWithPage" ] } \ No newline at end of file diff --git a/library/index.ets b/library/index.ets index f25c784..5e8d3ca 100644 --- a/library/index.ets +++ b/library/index.ets @@ -110,6 +110,8 @@ export {IDataFetch} from './src/main/ets/components/imageknife/networkmanage/IDa export {ICache} from './src/main/ets/components/imageknife/requestmanage/ICache' export { FileTypeUtil } from './src/main/ets/components/imageknife/utils/FileTypeUtil' export { ParseImageUtil } from './src/main/ets/components/imageknife/utils/ParseImageUtil' +export { DownloadClient } from './src/main/ets/components/imageknife/networkmanage/DownloadClient'; +export { CustomDataFetchClient } from './src/main/ets/components/imageknife/networkmanage/CustomDataFetchClient'; /** * svg parse diff --git a/library/src/main/ets/components/imageknife/ImageKnife.ets b/library/src/main/ets/components/imageknife/ImageKnife.ets index 34038b1..35fa987 100644 --- a/library/src/main/ets/components/imageknife/ImageKnife.ets +++ b/library/src/main/ets/components/imageknife/ImageKnife.ets @@ -680,6 +680,7 @@ export class ImageKnife { errorholderSrc: this.transformResource(request.errorholderSrc) as PixelMap | MResource | undefined, retryholderSrc:this.transformResource(request.retryholderSrc) as PixelMap | MResource | undefined, fallbackSrc: this.transformResource(request.fallbackSrc) as PixelMap | MResource | undefined, + customGetImage: request.customGetImage } return data; } @@ -940,6 +941,7 @@ async function taskExecute(sendData:SendableData,taskData:TaskParams): Promise

{ diff --git a/library/src/main/ets/components/imageknife/ImageKnifeComponent.ets b/library/src/main/ets/components/imageknife/ImageKnifeComponent.ets index c4b21f4..7265bbb 100644 --- a/library/src/main/ets/components/imageknife/ImageKnifeComponent.ets +++ b/library/src/main/ets/components/imageknife/ImageKnifeComponent.ets @@ -216,7 +216,6 @@ export struct ImageKnifeComponent { return false; } }) - let realSize:Size = { width: this.context.width, height: this.context.height @@ -312,6 +311,9 @@ export struct ImageKnifeComponent { this.runNextFunction(this.displayRetryholder,data) }}) } + if (this.imageKnifeOption.customGetImage) { + request.customGetImage = this.imageKnifeOption.customGetImage; + } } configHspContext(request: RequestOption){ diff --git a/library/src/main/ets/components/imageknife/ImageKnifeOption.ets b/library/src/main/ets/components/imageknife/ImageKnifeOption.ets index ce110be..724b166 100644 --- a/library/src/main/ets/components/imageknife/ImageKnifeOption.ets +++ b/library/src/main/ets/components/imageknife/ImageKnifeOption.ets @@ -27,6 +27,7 @@ import { RoundCorner } from './transform/RoundedCornersTransformation' import { ObjectKey } from './ObjectKey' import common from '@ohos.app.ability.common' import { Priority } from './RequestOption' +import { DataFetchResult } from './networkmanage/DataFetchResult' export interface CropCircleWithBorder{ border: number, @@ -145,6 +146,9 @@ export class ImageKnifeOption { context?: common.UIAbilityContext; // sizeAnimate?: AnimateParam 由业务自定义不再支持 + // 设置是否使用应用自定义的方式加载图片 + customGetImage?: (context: Context, src: string) => Promise; + constructor() { } diff --git a/library/src/main/ets/components/imageknife/RequestOption.ets b/library/src/main/ets/components/imageknife/RequestOption.ets index db78fc7..a01a6ad 100644 --- a/library/src/main/ets/components/imageknife/RequestOption.ets +++ b/library/src/main/ets/components/imageknife/RequestOption.ets @@ -59,6 +59,7 @@ import { DiskLruCache } from '../cache/DiskLruCache' import { SparkMD5 } from '../3rd_party/sparkmd5/spark-md5' import { FileUtils } from '../cache/FileUtils' import util from '@ohos.util' +import { DataFetchResult } from './networkmanage/DataFetchResult' export interface Size { width: number, @@ -149,6 +150,8 @@ export class RequestOption { fallbackCacheKey: string = ""; // 自定义缓存关键字 signature?: ObjectKey; + // 设置是否使用应用自定义的方式加载图片 + customGetImage?: (context: Context, src: string) => Promise; // 下载原始文件地址 downloadFilePath: string = ""; //磁盘缓存文件路径 diff --git a/library/src/main/ets/components/imageknife/TaskParams.ets b/library/src/main/ets/components/imageknife/TaskParams.ets index 4d4ddd7..51aaa2b 100644 --- a/library/src/main/ets/components/imageknife/TaskParams.ets +++ b/library/src/main/ets/components/imageknife/TaskParams.ets @@ -16,6 +16,7 @@ import { ObjectKey } from './ObjectKey'; import { Priority, Size } from '../imageknife/RequestOption' import common from '@ohos.app.ability.common' import { MResource } from './utils/MResource'; +import { DataFetchResult } from './networkmanage/DataFetchResult'; export class TaskParams { headers: Map = new Map(); @@ -27,4 +28,5 @@ export class TaskParams { errorholderSrc: PixelMap | MResource | undefined = undefined; retryholderSrc: PixelMap | MResource | undefined = undefined; fallbackSrc: PixelMap | MResource | undefined = undefined; + customGetImage?: (context: Context, loadSrc: string) => Promise; } \ No newline at end of file diff --git a/library/src/main/ets/components/imageknife/networkmanage/CustomDataFetchClient.ets b/library/src/main/ets/components/imageknife/networkmanage/CustomDataFetchClient.ets new file mode 100644 index 0000000..db50dd1 --- /dev/null +++ b/library/src/main/ets/components/imageknife/networkmanage/CustomDataFetchClient.ets @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IDataFetch } from '../networkmanage/IDataFetch' +import { RequestOption } from '../RequestOption' +import { RequestData } from './RequestData' +import { ImageKnifeGlobal } from '../ImageKnifeGlobal' +import common from '@ohos.app.ability.common' +import http from '@ohos.net.http' +import { DataFetchResult } from './DataFetchResult' + +@Sendable +export class CustomDataFetchClient implements IDataFetch { + async loadData(request: RequestOption) { + let result: DataFetchResult = new DataFetchResult(); + if (!request || typeof request.loadSrc !== 'string') { + result.error = 'CustomDataFetchClient request or loadSrc error.'; + return result; + } + // 自定义单个图片的网络栈 + if (request.customGetImage) { + return await request.customGetImage(ImageKnifeGlobal.getInstance() + .getHapContext() as common.UIAbilityContext, request.loadSrc); + } + // 所有图片的网络栈 + try { + let httpRequest = http.createHttp() + let arrayBuffers = new Array(); + httpRequest.on('headersReceive', (header: Object) => { + // 跟服务器连接成功准备下载 + if (request.progressFunc) { + // 进度条为0 + request.progressFunc.asyncSuccess(0) + } + }) + httpRequest.on('dataReceive', (data: ArrayBuffer) => { + // 下载数据流多次返回 + arrayBuffers.push(data); + }) + httpRequest.on('dataReceiveProgress', (data: RequestData) => { + // 下载进度 + if (data != undefined && (typeof data.receiveSize == 'number') && (typeof data.totalSize == 'number')) { + let percent = Math.round(((data.receiveSize * 1.0) / (data.totalSize * 1.0)) * 100) + if (request.progressFunc) { + request.progressFunc.asyncSuccess(percent) + } + } + }) + httpRequest.on('dataEnd', () => { + // 下载完毕 + }) + const headerObj: Record = {} + request.headers.forEach((value, key) => { + headerObj[key] = value + }) + const data = await httpRequest.requestInStream(request.loadSrc as string, { + header: headerObj, + method: http.RequestMethod.GET, + expectDataType: http.HttpDataType.ARRAY_BUFFER, + connectTimeout: 60000, // 可选 默认60000ms + readTimeout: 0, // 可选, 默认为60000ms + usingProtocol: http.HttpProtocol.HTTP1_1, // 可选,协议类型默认值由系统自动指定 + usingCache: false + }).catch((err: Error) => { + result.error = 'CustomDataFetchClient has error, http code = ' + JSON.stringify(err); + }) + if (data == 200) { + result.data = this.combineArrayBuffers(arrayBuffers); + } else { + result.error = 'CustomDataFetchClient resultCode error, code = ' + data; + } + console.log('TestCustomDataFetch all onComplete, code = ' + data + ',length = ' + result.data?.byteLength); + } catch (err) { + result.error = 'CustomDataFetchClient catch err request uuid =' + request.uuid; + } + return result; + } + + combineArrayBuffers(arrayBuffers: ArrayBuffer[]): ArrayBuffer { + // 计算多个ArrayBuffer的总字节大小 + let totalByteLength = 0; + for (const arrayBuffer of arrayBuffers) { + totalByteLength += arrayBuffer.byteLength; + } + + // 创建一个新的ArrayBuffer + const combinedArrayBuffer = new ArrayBuffer(totalByteLength); + + // 创建一个Uint8Array来操作新的ArrayBuffer + const combinedUint8Array = new Uint8Array(combinedArrayBuffer); + + // 依次复制每个ArrayBuffer的内容到新的ArrayBuffer中 + let offset = 0; + for (const arrayBuffer of arrayBuffers) { + const sourceUint8Array = new Uint8Array(arrayBuffer); + combinedUint8Array.set(sourceUint8Array, offset); + offset += sourceUint8Array.length; + } + return combinedArrayBuffer; + } +} \ No newline at end of file diff --git a/library/src/main/ets/components/imageknife/networkmanage/DownloadClient.ets b/library/src/main/ets/components/imageknife/networkmanage/DownloadClient.ets index ed1c4e6..c769b17 100644 --- a/library/src/main/ets/components/imageknife/networkmanage/DownloadClient.ets +++ b/library/src/main/ets/components/imageknife/networkmanage/DownloadClient.ets @@ -35,11 +35,7 @@ export class DownloadClient implements IDataFetch { async loadData(request: RequestOption): Promise { if (typeof request.loadSrc == 'string') { - let fileDir:string = (ImageKnifeGlobal.getInstance().getHapContext() as common.UIAbilityContext).filesDir as string; - let cacheDir:string = (ImageKnifeGlobal.getInstance().getHapContext() as common.UIAbilityContext).cacheDir as string - - if (request.loadSrc.startsWith(fileDir) || - request.loadSrc.startsWith(cacheDir)) { + if (this.isLocalLoadSrc(ImageKnifeGlobal.getInstance().getHapContext(), request.loadSrc)) { // 本地沙盒 return this.localFileClient.loadData(request) } else if (request.loadSrc.startsWith('datashare://') || request.loadSrc.startsWith('file://')) { @@ -54,4 +50,15 @@ export class DownloadClient implements IDataFetch { return result; } } + + isLocalLoadSrc(context: Object | undefined, loadSrc: string): boolean { + if (context != undefined) { + let fileDir: string = (context as common.UIAbilityContext).filesDir as string; + let cacheDir: string = (context as common.UIAbilityContext).cacheDir as string + if (loadSrc.startsWith(fileDir) || loadSrc.startsWith(cacheDir)) { + return true; + } + } + return false; + } } \ No newline at end of file diff --git a/library/src/main/ets/components/imageknife/networkmanage/HttpDownloadClient.ets b/library/src/main/ets/components/imageknife/networkmanage/HttpDownloadClient.ets index 1806806..c54f6e5 100644 --- a/library/src/main/ets/components/imageknife/networkmanage/HttpDownloadClient.ets +++ b/library/src/main/ets/components/imageknife/networkmanage/HttpDownloadClient.ets @@ -70,6 +70,7 @@ export class HttpDownloadClient implements IDataFetch { } else { result.error = `HttpDownloadClient has error, http code = ` + JSON.stringify(data); } + console.log('TestCustomDataFetch http onComplete, code = ' + data + ',length = ' + result.data?.byteLength); } catch (err) { result.error ='HttpDownloadClient catch err request uuid =' + request.uuid; } diff --git a/sharedlibrary/src/main/ets/Index.ets b/sharedlibrary/src/main/ets/Index.ets index 8597fed..30f935f 100644 --- a/sharedlibrary/src/main/ets/Index.ets +++ b/sharedlibrary/src/main/ets/Index.ets @@ -110,6 +110,8 @@ export {IDataFetch} from '@ohos/imageknife' export {ICache} from '@ohos/imageknife' export { FileTypeUtil } from '@ohos/imageknife' export { ParseImageUtil } from '@ohos/imageknife' +export { DownloadClient } from '@ohos/imageknife' +export { CustomDataFetchClient } from '@ohos/imageknife' /** * svg parse