You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter无第三方包实现Assets图片裁剪报错解决

问题解决:无第三方包实现Assets图片区域裁剪与跳转

问题描述

我有一张Assets图片,需要在该图片上添加可拖动的容器,点击保存按钮时裁剪容器区域内的图片并跳转到下一页面展示裁剪后的图片。尝试了以下代码,但点击保存按钮时出现报错,如何在不使用任何第三方包的情况下解决该问题?

原错误代码:

import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  GlobalKey globalKey = GlobalKey();

  Offset framePosition = Offset(100, 100); // Initial position of the frame container
  double frameSize = 150; // Initial size of the frame container
  String backgroundImage = 'images/rose1.png'; // Background image path

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Home Screen"),
      ),
      body: Stack(
        children: [
          // Background image
          Image.asset(
            backgroundImage,
            fit: BoxFit.cover,
            width: MediaQuery.of(context).size.width,
            height: MediaQuery.of(context).size.height,
          ),
          // Frame container
          Positioned(
            left: framePosition.dx,
            top: framePosition.dy,
            child: GestureDetector(
              onPanUpdate: (details) {
                setState(() {
                  framePosition += details.delta;
                });
              },
              child: RepaintBoundary(
                key: globalKey,
                child: Container(
                  width: frameSize,
                  height: frameSize,
                  decoration: BoxDecoration(
                    border: Border.all(color: Colors.red, width: 2.0),
                  ),
                ),
              ),
            ),
          ),
          // Save button
          Positioned(
            bottom: 20,
            right: 20,
            child: ElevatedButton(
              onPressed: () => _cropAndNavigate(context),
              child: Text("Save and Next"),
            ),
          ),
        ],
      ),
    );
  }

  Future<void> _cropAndNavigate(BuildContext context) async {
    // Capture the image within the frame container
    ui.Image image = await _captureImage(globalKey.currentContext!);

    // Get frame container position and size relative to the background image
    RenderBox containerBox = globalKey.currentContext!.findRenderObject() as RenderBox;
    Offset containerPosition = containerBox.localToGlobal(Offset.zero);
    double containerWidth = containerBox.size.width;
    double containerHeight = containerBox.size.height;

    // Calculate the crop coordinates relative to the background image
    double left = (framePosition.dx - containerPosition.dx) * 2;
    double top = (framePosition.dy - containerPosition.dy) * 2;
    double width = frameSize * 2;
    double height = frameSize * 2;

    // Crop the image
    Uint8List croppedImage = await _cropImage(image, left.toInt(), top.toInt(), width.toInt(), height.toInt());

    // Navigate to the next screen and pass the cropped image
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => NextScreen(croppedImage: croppedImage),
      ),
    );
  }

  Future<ui.Image> _captureImage(BuildContext context) async {
    RenderRepaintBoundary boundary = context.findRenderObject() as RenderRepaintBoundary;
    ui.Image image = await boundary.toImage(pixelRatio: 2.0);
    return image;
  }

  Future<Uint8List> _cropImage(ui.Image image, int left, int top, int width, int height) async {
    final ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
    final Uint8List pngBytes = byteData!.buffer.asUint8List();

    // Crop the image
    // Here, you should implement your cropping logic based on the given coordinates
    // For simplicity, I'll just create a copy of the original image
    Uint8List croppedImage = Uint8List.fromList(pngBytes.sublist(left, left + width));

    return croppedImage;
  }
}

class NextScreen extends StatelessWidget {
  final Uint8List croppedImage;

  NextScreen({required this.croppedImage});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Next Screen"),
      ),
      body: Center(
        child: Image.memory(
          croppedImage,
          fit: BoxFit.cover,
        ),
      ),
    );
  }
}

错误原因分析

  1. 捕获对象错误:原代码将RepaintBoundary加在红色边框容器上,导致捕获的只是空容器而非背景图的对应区域
  2. 裁剪逻辑无效:直接截取PNG字节数组的子列表不符合PNG文件格式规范,无法生成有效图片
  3. 坐标映射错误:未处理BoxFit.cover导致的图片拉伸,屏幕上的容器位置与原始图片像素位置未正确对应
  4. 容器位置计算冗余:framePosition已是Positioned组件的直接定位参数,无需额外与容器全局坐标计算

修正后的完整代码

import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  Offset framePosition = const Offset(100, 100);
  double frameSize = 150;
  final String backgroundImagePath = 'images/rose1.png';
  
  // 预加载的原始图片
  ui.Image? originalImage;

  @override
  void initState() {
    super.initState();
    _loadOriginalImage();
  }

  // 从Assets加载原始图片为ui.Image
  Future<void> _loadOriginalImage() async {
    final ByteData data = await rootBundle.load(backgroundImagePath);
    final Uint8List bytes = data.buffer.asUint8List();
    final ui.Codec codec = await ui.instantiateImageCodec(bytes);
    final ui.FrameInfo frameInfo = await codec.getNextFrame();
    setState(() {
      originalImage = frameInfo.image;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("图片裁剪")),
      body: originalImage == null 
          ? const Center(child: CircularProgressIndicator())
          : Stack(
              children: [
                // 背景图片
                Image.asset(
                  backgroundImagePath,
                  fit: BoxFit.cover,
                  width: MediaQuery.of(context).size.width,
                  height: MediaQuery.of(context).size.height,
                ),
                // 可拖动的裁剪框
                Positioned(
                  left: framePosition.dx,
                  top: framePosition.dy,
                  child: GestureDetector(
                    onPanUpdate: (details) {
                      setState(() {
                        framePosition += details.delta;
                        // 限制裁剪框不超出屏幕
                        framePosition = Offset(
                          framePosition.dx.clamp(0, MediaQuery.of(context).size.width - frameSize),
                          framePosition.dy.clamp(0, MediaQuery.of(context).size.height - frameSize),
                        );
                      });
                    },
                    child: Container(
                      width: frameSize,
                      height: frameSize,
                      decoration: BoxDecoration(
                        border: Border.all(color: Colors.red, width: 2.0),
                        color: Colors.black.withOpacity(0.2),
                      ),
                    ),
                  ),
                ),
                // 保存按钮
                Positioned(
                  bottom: 20,
                  right: 20,
                  child: ElevatedButton(
                    onPressed: () => _cropAndNavigate(context),
                    child: const Text("保存并跳转"),
                  ),
                ),
              ],
            ),
    );
  }

  Future<void> _cropAndNavigate(BuildContext context) async {
    if (originalImage == null) return;

    final screenSize = MediaQuery.of(context).size;
    final originalWidth = originalImage!.width.toDouble();
    final originalHeight = originalImage!.height.toDouble();

    // 计算BoxFit.cover下的图片缩放比例和偏移
    final scale = screenSize.width / originalWidth > screenSize.height / originalHeight 
        ? screenSize.width / originalWidth 
        : screenSize.height / originalHeight;
    final scaledWidth = originalWidth * scale;
    final scaledHeight = originalHeight * scale;
    final offsetX = (screenSize.width - scaledWidth) / 2;
    final offsetY = (screenSize.height - scaledHeight) / 2;

    // 计算裁剪框在原始图片上的坐标
    final cropLeft = ((framePosition.dx - offsetX) / scale).clamp(0, originalWidth - frameSize / scale);
    final cropTop = ((framePosition.dy - offsetY) / scale).clamp(0, originalHeight - frameSize / scale);
    final cropWidth = frameSize / scale;
    final cropHeight = frameSize / scale;

    // 执行裁剪
    final croppedBytes = await _cropOriginalImage(
      originalImage!, 
      cropLeft.toInt(), 
      cropTop.toInt(), 
      cropWidth.toInt(), 
      cropHeight.toInt()
    );

    // 跳转到展示页面
    if (croppedBytes != null) {
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => NextScreen(croppedImage: croppedBytes),
        ),
      );
    }
  }

  // 裁剪原始图片
  Future<Uint8List?> _cropOriginalImage(ui.Image image, int left, int top, int width, int height) async {
    final recorder = ui.PictureRecorder();
    final canvas = Canvas(recorder, Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()));
    
    // 绘制裁剪区域
    canvas.drawImageRect(
      image,
      Rect.fromLTWH(left.toDouble(), top.toDouble(), width.toDouble(), height.toDouble()),
      Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()),
      Paint(),
    );

    final picture = recorder.endRecording();
    final croppedImage = await picture.toImage(width, height);
    final byteData = await croppedImage.toByteData(format: ui.ImageByteFormat.png);
    
    return byteData?.buffer.asUint8List();
  }
}

class NextScreen extends StatelessWidget {
  final Uint8List croppedImage;

  const NextScreen({super.key, required this.croppedImage});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("裁剪结果")),
      body: Center(
        child: Image.memory(
          croppedImage,
          fit: BoxFit.contain,
        ),
      ),
    );
  }
}

关键修改说明

  • 预加载原始图片:从Assets加载原始图片为ui.Image,避免依赖屏幕渲染的拉伸图片,保证裁剪精度
  • 修复坐标映射:计算BoxFit.cover下的图片缩放比例和偏移,将屏幕上的裁剪框位置转换为原始图片的像素坐标
  • 正确裁剪逻辑:使用PictureRecorder和Canvas绘制原始图片的指定区域,生成符合格式的裁剪图片
  • 优化交互体验:添加裁剪框边界限制,防止超出屏幕范围
  • 移除无效代码:删除了原代码中错误的RepaintBoundary和无效的字节数组截取逻辑

内容的提问来源于stack exchange,提问作者Harsh Rajpara

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 11:01:05