Flutter Web头像裁剪:JS迁移图片解码操作遇异常求解决方案
解决Flutter Web端图片解码阻塞UI的问题
场景背景
- Flutter应用头像设置流程:选择图片文件(仅支持图片)→弹出对话框裁剪圆形区域作为头像
- 使用
ExtendedImage.memory(...)加载头像,Web端原裁剪逻辑中decodeImage操作会阻塞UI,已将图片编码逻辑迁移至JavaScript端并运行正常,现需解决解码操作的UI阻塞问题
现有正常运行的JS编码实现
Dart外部函数定义
@JS('encodeImageToJpegFromRaw') external JSPromise<JSString> encodeImageToJpegFromRaw( JSUint8Array rawPixels, int width, int height, );
JavaScript实现代码
function encodeImageToJpegFromRaw(rawPixels, width, height) { return new Promise((resolve, reject) => { try { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); canvas.width = width; canvas.height = height; const imageData = new ImageData( new Uint8ClampedArray(rawPixels), width, height ); ctx.putImageData(imageData, 0, 0); const jpegData = canvas.toDataURL("image/jpeg", 0.9); resolve(jpegData); } catch (e) { reject(e); } }); }
尝试的JS解码方案(存在异常)
尝试将解码操作迁移至JS端,但返回全蓝/全绿等异常图像,代码如下:
Dart调用逻辑
try { List<int> rgba = await promiseToFuture( extractRGBAFromImageData(imageData.toJS), ); int totalPixels = (imageData.lengthInBytes / 4) .toInt(); print("totalPixels: $totalPixels"); print("lengthInBytes: ${imageData.lengthInBytes}"); ByteData byteData = ByteData.sublistView(Uint8List.fromList(rgba)); int w = state.image!.width; int h = state.image!.height; print("w: $w, h: $h"); src = Image.fromBytes( width: state.image!.width, height: state.image!.height, bytes: imageData.buffer, // 这里存在逻辑错误 ); } catch (e) { print("Errore nella decodifica immagine con JS: $e"); rethrow; }
JavaScript解码实现
@JS('extractRGBAFromImageData') external JSPromise extractRGBAFromImageData(JSUint8Array imageData); function extractRGBAFromImageData(imageData) { return new Promise((resolve, reject) => { try { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const img = new Image(); const blob = new Blob([imageData], { type: "image/png" }); const url = URL.createObjectURL(blob); img.onload = function () { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const rgbaArray = imageData.data; resolve(rgbaArray); }; img.onerror = function (e) { reject(e); }; img.src = url; } catch (e) { reject(e); } }); }
可行解决思路
1. 修正现有解码逻辑的错误点
- 核心问题:Dart代码中获取JS返回的RGBA数组后,仍使用原始
imageData.buffer创建Image对象,并未使用JS解码后的RGBA数据,导致图像异常 - 修正步骤:
- 将JS返回的RGBA数组转换为Dart的
Uint8List - 使用该
Uint8List的buffer创建Image对象,同时指定格式为RGBA:Uint8List rgbaBytes = Uint8List.fromList(rgba); src = Image.fromBytes( width: state.image!.width, height: state.image!.height, bytes: rgbaBytes.buffer, format: Format.rgba, // 明确指定格式 ); - 注意:JS返回的
imageData.data是RGBA格式的Uint8ClampedArray,与Dart的Format.rgba兼容
- 将JS返回的RGBA数组转换为Dart的
2. 更优方案:将解码+裁剪合并到JS端完成
既然解码和裁剪都是耗时操作,可直接在JS端完成整个流程,减少Dart与JS的交互开销,步骤如下:
Dart端定义外部函数
@JS('cropImageInJS') external JSPromise<JSString> cropImageInJS( JSUint8Array imageData, double cropLeft, double cropTop, double cropWidth, double cropHeight, );
JavaScript实现解码+裁剪+编码
function cropImageInJS(imageData, cropLeft, cropTop, cropWidth, cropHeight) { return new Promise((resolve, reject) => { try { const img = new Image(); const blob = new Blob([imageData], { type: "image/*" }); const url = URL.createObjectURL(blob); img.onload = function () { // 创建裁剪用的canvas const cropCanvas = document.createElement("canvas"); const cropCtx = cropCanvas.getContext("2d"); cropCanvas.width = cropWidth; cropCanvas.height = cropHeight; // 绘制裁剪区域 cropCtx.drawImage( img, cropLeft, cropTop, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight ); // 直接转成JPG base64返回 const jpegData = cropCanvas.toDataURL("image/jpeg", 0.9); URL.revokeObjectURL(url); // 释放资源 resolve(jpegData); }; img.onerror = function (e) { URL.revokeObjectURL(url); reject(e); }; img.src = url; } catch (e) { reject(e); } }); }
Dart端调用
Future<Uint8List> cropImageDataWithJS( ImageEditorController imageEditorController, ) async { Rect cropRect = imageEditorController.getCropRect()!; final ExtendedImageEditorState state = imageEditorController.state!; Uint8List imageData = state.rawImageData; final jpegBase64 = await promiseToFuture( cropImageInJS( imageData.toJS, cropRect.left, cropRect.top, cropRect.width, cropRect.height, ), ); return Uint8List.fromList(base64Decode(jpegBase64.split(",").last)); }
该方案直接在JS端完成解码、裁剪、编码,避免Dart端的耗时操作,彻底解决UI阻塞问题,同时减少数据交互次数。
内容的提问来源于stack exchange,提问作者loreeemartiii
相关产品推荐
相关产品推荐

