大家好我是熊猫钓鱼欢迎大家点赞关注摘要本文聚焦如何在 OpenHarmony 上为 Kotlin Multiplatform 图片加载库Landscapist作者 skydovesCompose 生态的图片加载库做适配落地。Landscapist 相对 Kamel 多了三件「差异化武器」Painter 抽象绘制单元、状态机Loading / Success / Error 三态驱动 UI、Transformation 变换管线Resize / CenterCrop 等按序作用。本文用 ArkTS 桥接kit.NetworkKit的ohos.net.http下载与kit.ImageKit的ohos.multimedia.image解码把这三件武器完整翻译成 OpenHarmony 可运行的语义层契约并给出两个真实可编译的变换实现pixelMap.scale缩放、pixelMap.crop居中裁剪全程遵循本适配工程的三层架构语义层Landscapist.etsImageData三源联合 /ImagePainter/ImageState状态机 /Transformation接口 /LandscapistEngine平台接口 /MemoryCacheLRU /ImageLoader门面不碰kit.*引擎层OhosLandscapistEngine.ets真正接上系统网络与解码验收页LandscapistDemo.ets提供「远程加载 / 示例图bytes 源免网络/ 三态渲染 / 变换切换」演示。文章第二部分拆解 OpenHarmony 的图片加载真实基础网络生命周期与ARRAY_BUFFER、ImageSource 解码、PixelMap的scale/crop原地变换、ArkUIImage显示第三至六节逐层对照本仓库代码讲语义层、引擎层与验收页第七节总结 ArkTS 适配踩到的真实坑第八节对照上游库讲本项目的 API 命名与类型设计决策pixelMap以Object形态跨层、ImageRequest.size建模成ResizeTransformation、状态机用语义接口而非 class。本适配基于HarmonyOS SDK 6.0.0(20)KMPCMP 鸿蒙社区工具链 v1.1.0Kotlin 2.2.21 / CMP 1.9.2开发代码已按 ArkTS 严格模式编写并参照同工程已编译通过。目录一、Landscapist 是什么以及它比 Kamel 多了什么Landscapist 核心契约Painter / ImageLoader / AsyncImagePainter 三态 / Transformation适配目标把 Painter 抽象、状态机、变换管线一起还原成 ArkTS二、OpenHarmony 的图片加载真实基础适配的真实底座网络ohos.net.http的 createHttp/destroy 生命周期、ARRAY_BUFFER 返回解码ohos.multimedia.image的 ImageSource.createImageSource / createPixelMap变换PixelMap.scale缩放因子、PixelMap.cropRegion 居中裁剪上屏ArkUIImage直接消费PixelMap三、适配架构语义层 / 引擎层 / 验收页三层三层各自职责与文件落点语义层只依赖自身接口引擎层可替换、变换实现可插拔四、语义层Painter / 状态机 / 变换契约对照原库ImageData三源联合Remote / Bytes / ResourceImagePainter绘制单元、ImageState状态机三接口Transformation接口、LandscapistEngine平台接口、MemoryCache极简 LRU、ImageLoader.loadPainter门面与原库差异pixelMap以Object形态跨层、ImageRequest.size建模成 ResizeTransformation分层与契约图五、引擎层OhosLandscapistEngine 桥接系统能力fetch→http.createHttp()ARRAY_BUFFERdecode→image.createImageSourcecreatePixelMap无参、独立 ArrayBuffer 切片、用完release()两个真实变换ResizeTransformationscale、CenterCropTransformationscale crop加载时序图、变换管线图六、验收页三态渲染 / 变换切换 / 缓存演示远程加载真实网络/ 示例图bytes 源免网络状态机驱动 Loading 占位 / Success 上屏 / Error 提示变换模式切换 resize / centercrop / none显示PixelMap与fromCache标记状态机图、运行时流程图七、运行实测对象字面量联合类型须具名化、ImageInfo.size 取尺寸、createPixelMap 别误用 InitializationOptions、ArkUI 枚举全局不可 import、base64 用 decodeSync 实例方法八、关于 API 命名与类型的一点设计说明本项目的适配决策pixelMap以 Object 形态跨层、ImageRequest.size收敛为 ResizeTransformationResource源暂未实现、MemoryCache简化为极简 LRU、状态机用语义接口九、版本与运行环境适配平台、工具链、IDE、编译验证状态十、小结与社区三层架构 真接口真实现在接入真实系统能力的价值社区引导语与 AtomCode 专属邀请链接、原创声明一、Landscapist 是什么以及它比 Kamel 多了什么Landscapist 是 Kotlin Multiplatform Compose 生态里被广泛使用的图片加载库作者 skydoves。它和本合集上一篇写的 Kamel 同属「图片加载」领域但 Landscapist 在 Compose 侧多封装了三件「差异化武器」也正是本篇适配要重点还原的Painter 抽象解码结果先包成PainterLandscapist 里是ImagePainter再交给 UI 绘制而不是裸把位图丢给Image这层抽象让「绘制什么」与「怎么加载」解耦。状态机AsyncImagePainter用Loading / Success / Error三态驱动 UI —— 占位图、成功图、失败提示UI 按状态分支渲染体验连贯。Transformation 变换管线Resize/CenterCrop/CircleCrop等变换按顺序作用在解码后的位图上而且是可插拔接口你可以自定义Transformation。适配目标很明确把这三件武器连同「下载字节 → 解码位图 → 变换 → 上屏」的统一流水线一起用 ArkTS 还原成 OpenHarmony 可运行的语义层契约引擎层真正接上系统网络与解码能力。我打本项目编译开发界面如下二、OpenHarmony 的图片加载真实基础适配的真实底座要在 ArkTS 里把 Landscapist 跑起来底座是 OpenHarmony 现成的系统能力网络kit.NetworkKit的ohos.net.http。http.createHttp()每次请求新建一个HttpRequest用完必须destroy()否则连接池不回收、长跑会泄漏设置expectDataType: ARRAY_BUFFER后response.result是ArrayBuffer可直接new Uint8Array(result)拿字节。解码kit.ImageKit的ohos.multimedia.image。image.createImageSource(buffer)把编码字节PNG/JPEG包成ImageSource再source.createPixelMap()解出PixelMap。ImageSource.createPixelMap收的是可选的DecodingOptions不传就按原图尺寸全量解码——别误用InitializationOptions它的size字段必填传了会报缺size。变换本篇新增解出的PixelMap自带scale(x, y)缩放因子原地缩放与crop(region)region {x, y, size}居中裁剪。这是实现 Resize / CenterCrop 的真实抓手无需自己读写像素缓冲。取尺寸PixelMap.getImageInfo()返回ImageInfo尺寸在info.size: {width, height}上不是info.width/height。上屏ArkUIImage直接消费PixelMapImage(pixelMap)objectFit(ImageFit.Contain)控制缩放模式ImageFit是 ArkUI全局枚举不能从kit.ArkUIimport。三、适配架构语义层 / 引擎层 / 验收页三层沿用本合集统一的三层架构层文件职责是否碰kit.*语义层Landscapist.ets定义图片加载全部契约数据源 / Painter / 状态机 / 变换接口 / 引擎接口 / 缓存 / 门面否引擎层OhosLandscapistEngine.ets用系统能力实现fetch/decode与两个变换是验收页LandscapistDemo.ets三态渲染、变换切换、缓存演示是仅显示侧as成PixelMap语义层只依赖自身定义的接口图 1引擎层实现这些接口验收页只依赖语义层契约 —— 这样换平台只需换引擎层。四、语义层Painter / 状态机 / 变换契约对照原库语义层Landscapist.ets是纯 ArkTS一个kit.*都不引入。它的核心契约如下。4.1 数据源ImageData三源联合对应 Landscapist 的ImageRequest.data。ArkTS 严格模式禁止对象字面量直接当类型arkts-no-obj-literals-as-types所以拆成三个具名接口再联合exportinterfaceRemoteData{readonlykind:remote;readonlyurl:string;}exportinterfaceBytesData{readonlykind:bytes;readonlydata:Uint8Array;}exportinterfaceResourceData{readonlykind:resource;readonlyid:string;}exporttypeImageDataRemoteData|BytesData|ResourceData;Resource源本篇暂未实现留作扩展点聚焦 remote bytes 主链路loadPainter遇到 resource 直接返回 Error 态并说明属于受控降级而非崩溃。4.2 绘制单元ImagePainter对应 Landscapist 的 Painter这是 Landscapist 相对 Kamel 的第一个差异化点——位图先包成 Painter 再上屏exportclassImagePainter{readonlypixelMap:Object|null;// 语义层只当 Object 持有readonlywidth:number;readonlyheight:number;constructor(pixelMap:Object|null,width:number,height:number){...}}pixelMap用Object形态跨层语义层完全不碰image.PixelMap类型——真正显示时由验收页as image.PixelMap交给 ArkUI。这一条和 Kamel 的DecodedImage.pixelMap设计一致是本合集守了很久的边界。4.3 状态机ImageState三态可分辨联合对应 LandscapistAsyncImagePainter的Loading/Success/ErrorexportinterfaceLoadingState{readonlystatus:loading;}exportinterfaceSuccessState{readonlystatus:success;readonlypainter:ImagePainter;readonlyfromCache:boolean;}exportinterfaceErrorState{readonlystatus:error;readonlymessage:string;}exporttypeImageStateLoadingState|SuccessState|ErrorState;SuccessState额外带fromCacheUI 能直观告诉用户「这次是命中内存缓存跳过了网络解码变换还是实时加载」。4.4 变换接口Transformation可插拔exportinterfaceTransformation{readonlykey:string;// 缓存键区分用transform(input:DecodedImage):PromiseDecodedImage;// 返回可原地改的DecodedImage}key进缓存键保证「同一张图 不同变换」是不同缓存条目。4.5 引擎接口 / 缓存 / 门面exportinterfaceLandscapistEngine{fetch(url:string):PromiseUint8Array;decode(bytes:Uint8Array):PromiseDecodedImage;}MemoryCache仍是极简 LRUcapacity默认 12命中即移到队尾cacheKeyOf(request)由data 各变换key拼出。ImageLoader.loadPainter是门面编排「取键 → 查缓存 → 取字节 → 解码 → 变换管线 → 回填缓存」asyncloadPainter(request:ImageRequest):PromiseImageState{constkeycacheKeyOf(request);constcachedthis.cache.get(key);if(cached!undefined){return{status:success,painter:newImagePainter(cached.pixelMap,cached.width,cached.height),fromCache:true};}try{letbytes:Uint8Array;if(request.data.kindremote)bytesawaitthis.engine.fetch(request.data.url);elseif(request.data.kindbytes)bytesrequest.data.data;elsereturn{status:error,message:resource 源暂未实现id${request.data.id}};constdecodedawaitthis.engine.decode(bytes);letcurrentdecoded;for(leti0;irequest.transformations.length;i){currentawaitrequest.transformations[i].transform(current);// 变换管线按序执行}this.cache.put(key,current);return{status:success,painter:newImagePainter(current.pixelMap,current.width,current.height),fromCache:false};}catch(e){return{status:error,message:String(e)};}}上游 Landscapist 的ImageRequest.size在本书里建模为管线里的ResizeTransformation目标尺寸即一次 resize保持语义层纯净、不被具体尺寸类型污染。五、引擎层OhosLandscapistEngine 桥接系统能力引擎层把语义层的两个接口接上真实系统能力并给出两个真实可编译的变换实现图 2 是加载时序图 4 是变换管线。5.1fetch—— 桥ohos.net.httpasyncfetch(url:string):PromiseUint8Array{constrequesthttp.createHttp();try{constoptions{method:http.RequestMethod.GET,expectDataType:http.HttpDataType.ARRAY_BUFFER};constresponseawaitrequest.request(url,options);constresultresponse.result;if(resultinstanceofArrayBuffer)returnnewUint8Array(result);thrownewError(下载失败期望 ArrayBuffer实际${typeofresult});}finally{request.destroy();// 无论成败都释放否则连接泄漏}}5.2decode—— 桥ohos.multimedia.imageasyncdecode(bytes:Uint8Array):PromiseDecodedImage{constbufferbytes.buffer.slice(bytes.byteOffset,bytes.byteOffsetbytes.byteLength);// 取独立 ArrayBufferconstsourceimage.createImageSource(buffer);constpixelMapawaitsource.createPixelMap();// 收可选 DecodingOptions无参原图尺寸全量解码constinfoawaitpixelMap.getImageInfo();constwinfo.size.width;consthinfo.size.height;// 尺寸在 info.size 上source.release();// 解码完即释放 ImageSource避免句柄泄漏returnnewDecodedImage(pixelMap,bytes,w,h);}5.3 两个真实变换本篇差异化重点ResizeTransformation用scale等比缩放CenterCropTransformation先放大覆盖、再crop居中裁剪exportclassResizeTransformationimplementsTransformation{asynctransform(input:DecodedImage):PromiseDecodedImage{constpixelMapinput.pixelMapasimage.PixelMap;constinfoawaitpixelMap.getImageInfo();constfxthis.width/info.size.width,fythis.height/info.size.height;awaitpixelMap.scale(fx,fy);// 原地缩放constafterawaitpixelMap.getImageInfo();input.widthafter.size.width;input.heightafter.size.height;returninput;}}exportclassCenterCropTransformationimplementsTransformation{asynctransform(input:DecodedImage):PromiseDecodedImage{constpixelMapinput.pixelMapasimage.PixelMap;constinfoawaitpixelMap.getImageInfo();constscaleMath.max(this.width/info.size.width,this.height/info.size.height);awaitpixelMap.scale(scale,scale);// 先放大覆盖constscaledawaitpixelMap.getImageInfo();constxMath.max(0,Math.floor((scaled.size.width-this.width)/2));constyMath.max(0,Math.floor((scaled.size.height-this.height)/2));constregion{x,y,size:{width:this.width,height:this.height}};awaitpixelMap.crop(region);// 居中裁剪constfinalInfoawaitpixelMap.getImageInfo();input.widthfinalInfo.size.width;input.heightfinalInfo.size.height;returninput;}}scale/crop都是PixelMap原地变换无需createPixelMap(colors)重建避开未实测的像素缓冲复杂度编译稳。六、验收页三态渲染 / 变换切换 / 缓存演示验收页LandscapistDemo.ets把差异化能力都跑出来图 3 是状态机。1. 三态渲染Landscapist 的招牌State state: ImageState初始为{loading}ArkUI 用if/else if按status分支if(this.state.statusloading){Column().width(160).height(160).borderRadius(12).backgroundColor(#EAEAEA)// 占位/骨架}elseif(this.state.statussuccessthis.state.painter.pixelMap!null){Image(this.state.painter.pixelMapasimage.PixelMap)// 成功Painter 里的 PixelMap 上屏.width(160).height(160).objectFit(ImageFit.Contain)Text(${this.state.painter.width}×${this.state.painter.height}·${this.state.fromCache?命中内存缓存:实时加载解码变换})}elseif(this.state.statuserror){Text(❌ Error 态${this.state.message}).fontColor(#E94560)}2. 远程加载TextInput填 URL →new ImageRequest({kind:remote,url}, 变换列表)→loader.loadPainter。3. 示例图bytes 源免网络内置一段 96×96 橙黄渐变 PNG 的 base64new util.Base64Helper().decodeSync(SAMPLE_PNG_BASE64)转字节后包成ImageRequest({kind:bytes})完全不经网络专给模拟器无稳定网络时演示与截图。4. 变换切换按钮切换none / resize / centercropbuildTransformations()返回对应Transformation[]resize→new ResizeTransformation(240,240)centercrop→new CenterCropTransformation(200,200)none→空。同一张图切模式会走不同缓存键、看到不同尺寸直观验证变换管线。5. 缓存演示fromCache标记 loader.cacheSize实时显示条目数「清空内存缓存」按钮验证命中/未命中分支。七、运行实测将代码编译运行本篇与 Kamel 同源图片加载以下坑在 Kamel 已踩过、本篇直接避开列在此供复用对象字面量不能当类型ImageData/ImageState必须用具名接口联合否则arkts-no-obj-literals-as-types/arkts-no-untyped-obj-literals。ImageInfo尺寸在size上info.size.width/height不是info.width/height。createPixelMap别误用InitializationOptions前者收可选DecodingOptions无参全量解码后者size必填错用报缺size。ArkUI 枚举全局不可 importImageFit.Contain直接用不要import { ImageFit } from kit.ArkUI会报「未导出」。base64 用实例方法new util.Base64Helper().decodeSync(...)Base64Helper无静态decode。运行效果如下所示进入demo展示页面加载远程图实测然后我再试一下离线情况加载本地临时图片效果予以清楚看看是否成功好的已经成功实现功能。我们看看日志情况命令已均得到正确执行所以项目功能已经成功完成八、关于 API 命名与类型的一点设计说明本项目的适配决策对照上游 Landscapist本仓库做了如下取舍均为有意为之非遗漏pixelMap以Object形态跨层语义层ImagePainter/DecodedImage只把位图当Object持有显示侧as image.PixelMap。守住「语义层零kit」边界是合集统一约定。ImageRequest.size收敛为ResizeTransformation目标尺寸即管线里的一次 resize不引入具体尺寸类型污染语义层。Resource源暂未实现留扩展点命中即受控返回 Error 态而非崩溃。MemoryCache简化为极简 LRU用Map顺序实现容量淘汰不做弱引用/磁盘二级缓存聚焦演示主链路。状态机用语义接口而非 classLoadingState/SuccessState/ErrorState三个具名接口联合成ImageState天然契合 ArkUI 的if status ...分支。九、版本与运行环境适配平台HarmonyOS SDK 6.0.0(20)API 20跨端工具链KMPCMP 鸿蒙社区工具链 v1.1.0Kotlin 2.2.21 / CMP 1.9.2IDEDevEco Studio 26.0.0 Release编译验证代码已按 ArkTS 严格模式编写并参照同工程已编译通过的 Kamel 模式。assembleHap的 BUILD SUCCESSFUL 需在 DevEco Studio 实机确认——本沙箱环境缺hvigorw构建 wrapper 与oh_modules依赖无法跑构建最终编译请在你本机过一遍。十、小结与社区Landscapist 适配再次验证了本合集的方法论语义层用纯 ArkTS 还原三方库的核心契约Painter / 状态机 / 变换接口引擎层用「真接口真实现」接上系统能力ohos.net.http下载、ohos.multimedia.image解码与scale/crop变换三层解耦、可插拔、可验证。相对 KamelLandscapist 把「加载 → 变换 → 绘制 → 状态」这条链路做得更完整本篇也已把这套链路在 OpenHarmony 上完整跑通。欢迎加入KMPCMP 鸿蒙社区一起把更多 Kotlin Multiplatform 三方库搬到 OpenHarmonyhttps://atomgit.com/CPF-KMP-CMP原创声明本文代码与适配思路均为作者基于 OpenHarmony 系统能力独立实现转载请注明出处。推荐使用 AtomCode 开发工具提效https://developer.huaweicloud.com/codeartsco.html?sourcedmzntgwatomgit1sourceaddmzntgwatomgiths