如何方形裁剪 Flutter 相机预览
How to square crop a Flutter camera preview
我正在尝试在我的应用程序中拍摄方形照片。我正在使用 camera 包,我正在尝试显示 CameraPreview
小部件的中心方形裁剪版本。
我的目标是显示预览的中央正方形(全宽),并从顶部和底部均匀裁剪。
我一直在努力让它工作,所以我创建了一个使用固定图像的最小示例。 (为我坐在椅子上的沉闷照片道歉):
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Example',
theme: ThemeData(),
home: Scaffold(
body: Example(),
),
);
}
}
class Example extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
CroppedCameraPreview(),
// Something to occupy the rest of the space
Expanded(
child: Container(),
)
],
);
}
}
class CroppedCameraPreview extends StatelessWidget {
@override
Widget build(BuildContext context) {
// We will pretend this is a camera preview (to make demo easier)
var cameraImage = Image.network("https://i.imgur.com/gZfg4jm.jpg");
var aspectRatio = 1280 / 720;
return Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.width,
child: ClipRect(
child: new OverflowBox(
alignment: Alignment.center,
child: FittedBox(
fit: BoxFit.fitWidth,
child: cameraImage,
),
),
),
);
}
}
效果很好 - 我得到一个全屏宽度的图像,中心被裁剪并被推到我的应用程序的顶部。
但是,如果我将此代码放入我现有的应用程序并将 cameraImage
替换为 CameraPreview
,我会遇到很多布局错误:
flutter: ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════
flutter: The following assertion was thrown during performResize():
flutter: TextureBox object was given an infinite size during layout.
flutter: This probably means that it is a render object that tries to be as big as possible, but it was put
flutter: inside another render object that allows its children to pick their own size.
flutter: The nearest ancestor providing an unbounded width constraint is:
flutter: RenderFittedBox#0bd54 NEEDS-LAYOUT NEEDS-PAINT
flutter: creator: FittedBox ← OverflowBox ← ClipRect ← ConstrainedBox ← Container ← Stack ← ConstrainedBox
flutter: ← Container ← CameraWidget ← Column ← CameraPage ← MediaQuery ← ⋯
flutter: parentData: offset=Offset(0.0, 0.0) (can use size)
flutter: constraints: BoxConstraints(w=320.0, h=320.0)
flutter: size: MISSING
flutter: fit: fitWidth
flutter: alignment: center
flutter: textDirection: ltr
flutter: The nearest ancestor providing an unbounded height constraint is:
flutter: RenderFittedBox#0bd54 NEEDS-LAYOUT NEEDS-PAINT
flutter: creator: FittedBox ← OverflowBox ← ClipRect ← ConstrainedBox ← Container ← Stack ← ConstrainedBox
flutter: ← Container ← CameraWidget ← Column ← CameraPage ← MediaQuery ← ⋯
flutter: parentData: offset=Offset(0.0, 0.0) (can use size)
flutter: constraints: BoxConstraints(w=320.0, h=320.0)
flutter: size: MISSING
flutter: fit: fitWidth
flutter: alignment: center
flutter: textDirection: ltr
flutter: The constraints that applied to the TextureBox were:
flutter: BoxConstraints(unconstrained)
flutter: The exact size it was given was:
flutter: Size(Infinity, Infinity)
flutter: See https://flutter.io/layout/ for more information.
谁能告诉我预览时出现错误的原因以及如何避免这些错误?
我通过为我的 CameraPreview
实例指定特定大小解决了这个问题,方法是将它包装在 Container
:
var size = MediaQuery.of(context).size.width;
// ...
Container(
width: size,
height: size,
child: ClipRect(
child: OverflowBox(
alignment: Alignment.center,
child: FittedBox(
fit: BoxFit.fitWidth,
child: Container(
width: size,
height:
size / widget.cameraController.value.aspectRatio,
child: camera, // this is my CameraPreview
),
),
),
),
);
为了回应 Luke 的评论,我随后使用此代码对生成的图像进行方形裁剪。 (因为虽然预览是方形的,但截取的图片还是标准比例)。
Future<String> _resizePhoto(String filePath) async {
ImageProperties properties =
await FlutterNativeImage.getImageProperties(filePath);
int width = properties.width;
var offset = (properties.height - properties.width) / 2;
File croppedFile = await FlutterNativeImage.cropImage(
filePath, 0, offset.round(), width, width);
return croppedFile.path;
}
这使用 https://github.com/btastic/flutter_native_image。我使用这段代码已经有一段时间了——认为它目前只适用于肖像图像,但应该很容易扩展以处理风景。
我有一个类似于答案中使用的代码片段。
与答案相同,支持相机纵横比与屏幕纵横比不同的情况。
虽然我的版本有一些不同:它不需要 MediaQuery 来获取设备大小,所以它适合任何父级的宽度(不仅仅是全屏宽度)
....
return AspectRatio(
aspectRatio: 1,
child: ClipRect(
child: Transform.scale(
scale: 1 / _cameraController.value.aspectRatio,
child: Center(
child: AspectRatio(
aspectRatio: _cameraController.value.aspectRatio,
child: CameraPreview(_cameraController),
),
),
),
),
);
要对图像进行中心正方形裁剪,请参阅下面的代码段。
它同样适用于纵向和横向图像。它还允许选择性地镜像图像(如果您想保留自拍相机的原始镜像外观,它会很有用)
import 'dart:io';
import 'dart:math';
import 'package:flutter/rendering.dart';
import 'package:image/image.dart' as IMG;
class ImageProcessor {
static Future cropSquare(String srcFilePath, String destFilePath, bool flip) async {
var bytes = await File(srcFilePath).readAsBytes();
IMG.Image src = IMG.decodeImage(bytes);
var cropSize = min(src.width, src.height);
int offsetX = (src.width - min(src.width, src.height)) ~/ 2;
int offsetY = (src.height - min(src.width, src.height)) ~/ 2;
IMG.Image destImage =
IMG.copyCrop(src, offsetX, offsetY, cropSize, cropSize);
if (flip) {
destImage = IMG.flipVertical(destImage);
}
var jpg = IMG.encodeJpg(destImage);
await File(destFilePath).writeAsBytes(jpg);
}
}
此代码需要 image 包。添加到 pubspec.yaml:
dependencies:
image: ^2.1.4
我就是这样解决的
SizedBox(
width: width,
height: height,
child: SingleChildScrollView(
child: AspectRatio(
aspectRatio: _cameraController.value.aspectRatio,
child: CameraPreview(_cameraController),
),
),
)
简单的裁剪方法
凭记忆:
Container(
width: 40.0,
height: 40.0,
decoration: BoxDecoration(
image: DecorationImage(
image: MemoryImage(photo),
fit: BoxFit.cover,
),
),
)
来自资产:
Container(
width: 40.0,
height: 40.0,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/photo.jpg'),
fit: BoxFit.cover,
),
),
)
经过一些测试后,我建议将 CameraPreview(不是图像本身)裁剪成任何矩形:
Widget buildCameraPreview(CameraController cameraController) {
final double previewAspectRatio = 0.7;
return AspectRatio(
aspectRatio: 1 / previewAspectRatio,
child: ClipRect(
child: Transform.scale(
scale: cameraController.value.aspectRatio / previewAspectRatio,
child: Center(
child: CameraPreview(cameraController),
),
),
),
);
}
优点类似于@VeganHunter 的解决方案,但可以通过使用 previewAspectRatio 扩展到任何矩形。
previewAspectRatio 的值为 1 将生成正方形,值为 0.5 将生成宽度为高度一半的矩形,值为 cameraController.value.aspectRatio 将生成全尺寸预览。
此外,这里解释了为什么这项工作完全有效:
Flutter 的一件事是让子项溢出其父项通常是不可能的(因为在布局阶段计算小部件大小的方式)。 Transform.scale 的使用在这里至关重要,因为根据文档:
This object applies its transformation just prior to painting, which means the transformation is not taken into account when calculating how much space this widget's child (and thus this widget) consumes.
这意味着小部件将能够在放大时溢出其容器,我们可以使用 CLipRect 剪辑(并因此隐藏)溢出的部分,将预览的整体大小限制为其父级大小。仅使用容器不可能达到相同的效果,因为它们会在布局阶段根据可用的 space 自行缩放,并且不会溢出,因此没有任何东西可以裁剪。
选择比例 (cameraController.value.aspectRatio * previewAspectRatio) 以使预览的宽度与其父项的宽度匹配。
如果此解决方案对您不起作用,请尝试用它的倒数 (1 / cameraController.value.aspectRatio) 替换所有 cameraController.value.aspectRatio。
这仅针对竖屏模式进行了测试,横向模式可能需要进行一些调整。
好吧,我想要一些更通用的东西,我们可以将相机放在任何容器中,它会使用 BoxFit
object 来定义如何 Crop/Position/Fit 在屏幕上。
因此,我没有使用 CameraPreview
小部件,而是使用 CameraController
中没有嵌入 AspectRatio
的 buildPreview
。
LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
return Container(
width: constraints.maxWidth,
height: constraints.maxHeight,
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: _cameraController!.value.deviceOrientation == DeviceOrientation.portraitUp
? _cameraController!.value.previewSize!.height
: _cameraController!.value.previewSize!.width,
height: _cameraController!.value.deviceOrientation == DeviceOrientation.portraitUp
? _cameraController!.value.previewSize!.width
: _cameraController!.value.previewSize!.height,
child: _cameraController!.buildPreview())),
);
}),
说明:
主要思想是使用具有最大约束的 LayoutBuilder + Container 获得“parent”大小(它可能是可选的,具体取决于您使用它的位置)。所以,我们有一个使用我们拥有的确切尺寸的容器。
然后,在容器内,我们使用带有我们想要的 BoxFit 的 FittedBox,它会相应地 Crop/Resize/Fit 其 child。然后,child 是相机预览组件的确切大小,因此 FittedBox
将能够正确地完成他的工作。对于 CameraPreview
小部件,它有一个 AspectRatio
,混淆了 FittedBox
。
我正在尝试在我的应用程序中拍摄方形照片。我正在使用 camera 包,我正在尝试显示 CameraPreview
小部件的中心方形裁剪版本。
我的目标是显示预览的中央正方形(全宽),并从顶部和底部均匀裁剪。
我一直在努力让它工作,所以我创建了一个使用固定图像的最小示例。 (为我坐在椅子上的沉闷照片道歉):
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Example',
theme: ThemeData(),
home: Scaffold(
body: Example(),
),
);
}
}
class Example extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
CroppedCameraPreview(),
// Something to occupy the rest of the space
Expanded(
child: Container(),
)
],
);
}
}
class CroppedCameraPreview extends StatelessWidget {
@override
Widget build(BuildContext context) {
// We will pretend this is a camera preview (to make demo easier)
var cameraImage = Image.network("https://i.imgur.com/gZfg4jm.jpg");
var aspectRatio = 1280 / 720;
return Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.width,
child: ClipRect(
child: new OverflowBox(
alignment: Alignment.center,
child: FittedBox(
fit: BoxFit.fitWidth,
child: cameraImage,
),
),
),
);
}
}
效果很好 - 我得到一个全屏宽度的图像,中心被裁剪并被推到我的应用程序的顶部。
但是,如果我将此代码放入我现有的应用程序并将 cameraImage
替换为 CameraPreview
,我会遇到很多布局错误:
flutter: ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════
flutter: The following assertion was thrown during performResize():
flutter: TextureBox object was given an infinite size during layout.
flutter: This probably means that it is a render object that tries to be as big as possible, but it was put
flutter: inside another render object that allows its children to pick their own size.
flutter: The nearest ancestor providing an unbounded width constraint is:
flutter: RenderFittedBox#0bd54 NEEDS-LAYOUT NEEDS-PAINT
flutter: creator: FittedBox ← OverflowBox ← ClipRect ← ConstrainedBox ← Container ← Stack ← ConstrainedBox
flutter: ← Container ← CameraWidget ← Column ← CameraPage ← MediaQuery ← ⋯
flutter: parentData: offset=Offset(0.0, 0.0) (can use size)
flutter: constraints: BoxConstraints(w=320.0, h=320.0)
flutter: size: MISSING
flutter: fit: fitWidth
flutter: alignment: center
flutter: textDirection: ltr
flutter: The nearest ancestor providing an unbounded height constraint is:
flutter: RenderFittedBox#0bd54 NEEDS-LAYOUT NEEDS-PAINT
flutter: creator: FittedBox ← OverflowBox ← ClipRect ← ConstrainedBox ← Container ← Stack ← ConstrainedBox
flutter: ← Container ← CameraWidget ← Column ← CameraPage ← MediaQuery ← ⋯
flutter: parentData: offset=Offset(0.0, 0.0) (can use size)
flutter: constraints: BoxConstraints(w=320.0, h=320.0)
flutter: size: MISSING
flutter: fit: fitWidth
flutter: alignment: center
flutter: textDirection: ltr
flutter: The constraints that applied to the TextureBox were:
flutter: BoxConstraints(unconstrained)
flutter: The exact size it was given was:
flutter: Size(Infinity, Infinity)
flutter: See https://flutter.io/layout/ for more information.
谁能告诉我预览时出现错误的原因以及如何避免这些错误?
我通过为我的 CameraPreview
实例指定特定大小解决了这个问题,方法是将它包装在 Container
:
var size = MediaQuery.of(context).size.width;
// ...
Container(
width: size,
height: size,
child: ClipRect(
child: OverflowBox(
alignment: Alignment.center,
child: FittedBox(
fit: BoxFit.fitWidth,
child: Container(
width: size,
height:
size / widget.cameraController.value.aspectRatio,
child: camera, // this is my CameraPreview
),
),
),
),
);
为了回应 Luke 的评论,我随后使用此代码对生成的图像进行方形裁剪。 (因为虽然预览是方形的,但截取的图片还是标准比例)。
Future<String> _resizePhoto(String filePath) async {
ImageProperties properties =
await FlutterNativeImage.getImageProperties(filePath);
int width = properties.width;
var offset = (properties.height - properties.width) / 2;
File croppedFile = await FlutterNativeImage.cropImage(
filePath, 0, offset.round(), width, width);
return croppedFile.path;
}
这使用 https://github.com/btastic/flutter_native_image。我使用这段代码已经有一段时间了——认为它目前只适用于肖像图像,但应该很容易扩展以处理风景。
我有一个类似于答案中使用的代码片段。
与答案相同,支持相机纵横比与屏幕纵横比不同的情况。
虽然我的版本有一些不同:它不需要 MediaQuery 来获取设备大小,所以它适合任何父级的宽度(不仅仅是全屏宽度)
....
return AspectRatio(
aspectRatio: 1,
child: ClipRect(
child: Transform.scale(
scale: 1 / _cameraController.value.aspectRatio,
child: Center(
child: AspectRatio(
aspectRatio: _cameraController.value.aspectRatio,
child: CameraPreview(_cameraController),
),
),
),
),
);
要对图像进行中心正方形裁剪,请参阅下面的代码段。
它同样适用于纵向和横向图像。它还允许选择性地镜像图像(如果您想保留自拍相机的原始镜像外观,它会很有用)
import 'dart:io';
import 'dart:math';
import 'package:flutter/rendering.dart';
import 'package:image/image.dart' as IMG;
class ImageProcessor {
static Future cropSquare(String srcFilePath, String destFilePath, bool flip) async {
var bytes = await File(srcFilePath).readAsBytes();
IMG.Image src = IMG.decodeImage(bytes);
var cropSize = min(src.width, src.height);
int offsetX = (src.width - min(src.width, src.height)) ~/ 2;
int offsetY = (src.height - min(src.width, src.height)) ~/ 2;
IMG.Image destImage =
IMG.copyCrop(src, offsetX, offsetY, cropSize, cropSize);
if (flip) {
destImage = IMG.flipVertical(destImage);
}
var jpg = IMG.encodeJpg(destImage);
await File(destFilePath).writeAsBytes(jpg);
}
}
此代码需要 image 包。添加到 pubspec.yaml:
dependencies:
image: ^2.1.4
我就是这样解决的
SizedBox(
width: width,
height: height,
child: SingleChildScrollView(
child: AspectRatio(
aspectRatio: _cameraController.value.aspectRatio,
child: CameraPreview(_cameraController),
),
),
)
简单的裁剪方法
凭记忆:
Container(
width: 40.0,
height: 40.0,
decoration: BoxDecoration(
image: DecorationImage(
image: MemoryImage(photo),
fit: BoxFit.cover,
),
),
)
来自资产:
Container(
width: 40.0,
height: 40.0,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/photo.jpg'),
fit: BoxFit.cover,
),
),
)
经过一些测试后,我建议将 CameraPreview(不是图像本身)裁剪成任何矩形:
Widget buildCameraPreview(CameraController cameraController) {
final double previewAspectRatio = 0.7;
return AspectRatio(
aspectRatio: 1 / previewAspectRatio,
child: ClipRect(
child: Transform.scale(
scale: cameraController.value.aspectRatio / previewAspectRatio,
child: Center(
child: CameraPreview(cameraController),
),
),
),
);
}
优点类似于@VeganHunter 的解决方案,但可以通过使用 previewAspectRatio 扩展到任何矩形。 previewAspectRatio 的值为 1 将生成正方形,值为 0.5 将生成宽度为高度一半的矩形,值为 cameraController.value.aspectRatio 将生成全尺寸预览。
此外,这里解释了为什么这项工作完全有效:
Flutter 的一件事是让子项溢出其父项通常是不可能的(因为在布局阶段计算小部件大小的方式)。 Transform.scale 的使用在这里至关重要,因为根据文档:
This object applies its transformation just prior to painting, which means the transformation is not taken into account when calculating how much space this widget's child (and thus this widget) consumes.
这意味着小部件将能够在放大时溢出其容器,我们可以使用 CLipRect 剪辑(并因此隐藏)溢出的部分,将预览的整体大小限制为其父级大小。仅使用容器不可能达到相同的效果,因为它们会在布局阶段根据可用的 space 自行缩放,并且不会溢出,因此没有任何东西可以裁剪。
选择比例 (cameraController.value.aspectRatio * previewAspectRatio) 以使预览的宽度与其父项的宽度匹配。
如果此解决方案对您不起作用,请尝试用它的倒数 (1 / cameraController.value.aspectRatio) 替换所有 cameraController.value.aspectRatio。
这仅针对竖屏模式进行了测试,横向模式可能需要进行一些调整。
好吧,我想要一些更通用的东西,我们可以将相机放在任何容器中,它会使用 BoxFit
object 来定义如何 Crop/Position/Fit 在屏幕上。
因此,我没有使用 CameraPreview
小部件,而是使用 CameraController
中没有嵌入 AspectRatio
的 buildPreview
。
LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
return Container(
width: constraints.maxWidth,
height: constraints.maxHeight,
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: _cameraController!.value.deviceOrientation == DeviceOrientation.portraitUp
? _cameraController!.value.previewSize!.height
: _cameraController!.value.previewSize!.width,
height: _cameraController!.value.deviceOrientation == DeviceOrientation.portraitUp
? _cameraController!.value.previewSize!.width
: _cameraController!.value.previewSize!.height,
child: _cameraController!.buildPreview())),
);
}),
说明:
主要思想是使用具有最大约束的 LayoutBuilder + Container 获得“parent”大小(它可能是可选的,具体取决于您使用它的位置)。所以,我们有一个使用我们拥有的确切尺寸的容器。
然后,在容器内,我们使用带有我们想要的 BoxFit 的 FittedBox,它会相应地 Crop/Resize/Fit 其 child。然后,child 是相机预览组件的确切大小,因此 FittedBox
将能够正确地完成他的工作。对于 CameraPreview
小部件,它有一个 AspectRatio
,混淆了 FittedBox
。