Camera是Flutter官方发布的相机插件,依赖这个库,可以完成Flutter APP里面拍照,录制视频的功能,但是如果直接把库里面的CameraPreview控件设置当前屏幕的宽高,展现出来的画面会拉伸严重、影响到图片的拍摄效果。 解决思路如下:不必给CameraPreview控件设置固定的宽高,它会自适应高度,然后用 Transform.scale方法将CameraPreview控件包裹,缩放至全屏即可。整个相机拍照代码如下:
import 'dart:async';
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_base/base_exp.dart';
import 'package:flutter_base/dialog/dialog_util.dart';
typedef TakePictureCallBack=void Function(String fileName,String filePath);
class MyCameraWidget extends StatefulWidget {
List<CameraDescription> cameras;
TakePictureCallBack takePictureCallBack;
MyCameraWidget(this.cameras,this.takePictureCallBack);
@override
MyCameraWidgetState createState() {
return MyCameraWidgetState();
}
}
IconData getCameraLensIcon(CameraLensDirection direction) {
switch (direction) {
case CameraLensDirection.back:
return Icons.camera_rear;
case CameraLensDirection.front:
return Icons.camera_front;
case CameraLensDirection.external:
return Icons.camera;
default:
throw ArgumentError('Unknown lens direction');
}
}
void logError(String code, String message) {
if (message != null) {
print('Error: $code\nError Message: $message');
} else {
print('Error: $code');
}
}
class MyCameraWidgetState extends State<MyCameraWidget>
with WidgetsBindingObserver, TickerProviderStateMixin {
CameraController controller;
XFile imageFile;
bool enableAudio = true;
int _pointers = 0;
double _minAvailableZoom = 1.0;
double _maxAvailableZoom = 1.0;
double _currentScale = 1.0;
double _baseScale = 1.0;
double _minAvailableExposureOffset = 0.0;
double _maxAvailableExposureOffset = 0.0;
double _currentExposureOffset = 0.0;
Future<void> _initializeControllerFuture;
Size mediaSize=null;
double scale;
@override
void initState(){
super.initState();
if(widget.cameras==null||widget.cameras.length==0){
DialogUtil.showMessageDialog(context,messageText: "未获取到可用的相机,请退出重试。");
}
controller = CameraController(
widget.cameras.first,
ResolutionPreset.veryHigh,
);
_initializeControllerFuture = controller.initialize();
_ambiguate(WidgetsBinding.instance)?.addObserver(this);
}
@override
void dispose() {
_ambiguate(WidgetsBinding.instance)?.removeObserver(this);
controller.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final CameraController cameraController = controller;
if (cameraController == null || !cameraController.value.isInitialized) {
return;
}
if (state == AppLifecycleState.inactive) {
cameraController.dispose();
} else if (state == AppLifecycleState.resumed) {
onNewCameraSelected(cameraController.description);
}
}
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
body: Container(
child: Column(
children: [
Expanded(child: FutureBuilder<void>(
future:_initializeControllerFuture,
builder:(context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
mediaSize = MediaQuery.of(context).size;
scale = 1 / (controller.value.aspectRatio * mediaSize.aspectRatio);
controller
.getMaxZoomLevel()
.then((double value) => _maxAvailableZoom = value);
controller
.getMinZoomLevel()
.then((double value) => _minAvailableZoom = value);
return _buildMainWidget();
} else {
return const Center(child: CircularProgressIndicator());
}
},
)
)
],
),
decoration: BoxDecoration(
color: Colors.black,
),
),
);
}
Widget _buildMainWidget()=>Container(
child: Stack(
children: [
ClipRect(
clipper: _MediaSizeClipper(mediaSize),
child: Transform.scale(
scale: scale,
alignment: Alignment.topCenter,
child: _cameraPreviewWidget(),
),
),
Positioned(
bottom: 50.w,
child: Container(
width: MediaQuery.of(context).size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('轻触拍照',style: TextStyle(color: Colors.white,fontSize: FontSizeStyle.big),),
SizedBox(height: 10.w,),
GestureDetector(
onTap: (){
if(controller == null ||
!controller.value.isInitialized){
MyToast.toast(msg: '相机控制器暂未初始化完成\n请稍等重试...');
return;
}
onTakePictureButtonPressed();
},
child: Container(
decoration: BoxDecoration(
color: Colors.transparent
),
child:Image.asset('images/icon_take_photo.png',width: 70.w,height: 70.w,),
),
)
],
),
)),
Positioned(
right: 20.w,
top: MediaQuery.of(context).padding.top+10.w,
child: GestureDetector(
onTap: (){
NavigatorUtil.pop();
},
child: Container(
decoration: BoxDecoration(
color: Colors.transparent
),
child:Icon(
Icons.close,
color: Colors.white,
size: 30.w,
),
),
))
],
),
);
Widget _cameraPreviewWidget() {
final CameraController cameraController = controller;
if (cameraController == null || !cameraController.value.isInitialized) {
return const Text(
'cameraController未初始化完成',
style: TextStyle(
color: Colors.white,
fontSize: 24.0,
fontWeight: FontWeight.w900,
),
);
} else {
return Listener(
onPointerDown: (_) => _pointers++,
onPointerUp: (_) => _pointers--,
child: CameraPreview(
controller,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onScaleStart: _handleScaleStart,
onScaleUpdate: _handleScaleUpdate,
onTapDown: (TapDownDetails details) =>
onViewFinderTap(details, constraints),
);
}),
),
);
}
}
void _handleScaleStart(ScaleStartDetails details) {
_baseScale = _currentScale;
}
Future<void> _handleScaleUpdate(ScaleUpdateDetails details) async {
if (controller == null || _pointers != 2) {
return;
}
_currentScale = (_baseScale * details.scale)
.clamp(_minAvailableZoom, _maxAvailableZoom);
await controller.setZoomLevel(_currentScale);
}
String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();
void showInSnackBar(String message) {
_scaffoldKey.currentState?.showSnackBar(SnackBar(content: Text(message)));
}
void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) {
if (controller == null) {
return;
}
final CameraController cameraController = controller;
final Offset offset = Offset(
details.localPosition.dx / constraints.maxWidth,
details.localPosition.dy / constraints.maxHeight,
);
cameraController.setExposurePoint(offset);
cameraController.setFocusPoint(offset);
}
Future<void> onNewCameraSelected(CameraDescription cameraDescription) async {
if (controller != null) {
await controller.dispose();
}
final CameraController cameraController = CameraController(
cameraDescription,
kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium,
enableAudio: enableAudio,
imageFormatGroup: ImageFormatGroup.jpeg,
);
controller = cameraController;
cameraController.addListener(() {
if (mounted) {
setState(() {});
}
if (cameraController.value.hasError) {
showInSnackBar(
'Camera error ${cameraController.value.errorDescription}');
}
});
try {
await cameraController.initialize();
await Future.wait(<Future<Object>>[
...!kIsWeb
? <Future<Object>>[
cameraController.getMinExposureOffset().then(
(double value) => _minAvailableExposureOffset = value),
cameraController
.getMaxExposureOffset()
.then((double value) => _maxAvailableExposureOffset = value)
]
: <Future<Object>>[],
cameraController
.getMaxZoomLevel()
.then((double value) => _maxAvailableZoom = value),
cameraController
.getMinZoomLevel()
.then((double value) => _minAvailableZoom = value),
]);
} on CameraException catch (e) {
_showCameraException(e);
}
if (mounted) {
setState(() {});
}
}
void onTakePictureButtonPressed() {
takePicture().then((XFile file) {
if (mounted) {
if(file!=null){
widget.takePictureCallBack(file.name,file.path);
NavigatorUtil.pop();
}else{
MyToast.toast(msg: '拍照失败!未成功获取到拍照图片');
}
}
});
}
Future<void> onCaptureOrientationLockButtonPressed() async {
try {
if (controller != null) {
final CameraController cameraController = controller;
if (cameraController.value.isCaptureOrientationLocked) {
await cameraController.unlockCaptureOrientation();
showInSnackBar('Capture orientation unlocked');
} else {
await cameraController.lockCaptureOrientation();
showInSnackBar(
'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}');
}
}
} on CameraException catch (e) {
_showCameraException(e);
}
}
void onSetFlashModeButtonPressed(FlashMode mode) {
setFlashMode(mode).then((_) {
if (mounted) {
setState(() {});
}
showInSnackBar('Flash mode set to ${mode.toString().split('.').last}');
});
}
void onSetExposureModeButtonPressed(ExposureMode mode) {
setExposureMode(mode).then((_) {
if (mounted) {
setState(() {});
}
showInSnackBar('Exposure mode set to ${mode.toString().split('.').last}');
});
}
void onSetFocusModeButtonPressed(FocusMode mode) {
setFocusMode(mode).then((_) {
if (mounted) {
setState(() {});
}
showInSnackBar('Focus mode set to ${mode.toString().split('.').last}');
});
}
Future<void> setFlashMode(FlashMode mode) async {
if (controller == null) {
return;
}
try {
await controller.setFlashMode(mode);
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
Future<void> setExposureMode(ExposureMode mode) async {
if (controller == null) {
return;
}
try {
await controller.setExposureMode(mode);
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
Future<void> setExposureOffset(double offset) async {
if (controller == null) {
return;
}
setState(() {
_currentExposureOffset = offset;
});
try {
offset = await controller.setExposureOffset(offset);
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
Future<void> setFocusMode(FocusMode mode) async {
if (controller == null) {
return;
}
try {
await controller.setFocusMode(mode);
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
Future<XFile> takePicture() async {
final CameraController cameraController = controller;
if (cameraController == null || !cameraController.value.isInitialized) {
showInSnackBar('Error: select a camera first.');
return null;
}
if (cameraController.value.isTakingPicture) {
return null;
}
try {
final XFile file = await cameraController.takePicture();
return file;
} on CameraException catch (e) {
_showCameraException(e);
return null;
}
}
void _showCameraException(CameraException e) {
logError(e.code, e.description);
showInSnackBar('Error: ${e.code}\n${e.description}');
}
}
class _MediaSizeClipper extends CustomClipper<Rect> {
final Size mediaSize;
const _MediaSizeClipper(this.mediaSize);
@override
Rect getClip(Size size) {
return Rect.fromLTWH(0, 0, mediaSize.width, mediaSize.height);
}
@override
bool shouldReclip(CustomClipper<Rect> oldClipper) {
return true;
}
}
T _ambiguate<T>(T value) => value;
上个界面的跳转到此界面的代码如下:
WidgetsFlutterBinding.ensureInitialized();
final cameras = await availableCameras();
NavigatorUtil.pushRightBack(
MyCameraWidget(cameras, (fileName, filePath) async {
File newFile =
await ImageCompressUtil.imageCompressAndGetFile(
new File(filePath));
upload_sys_saveFile(
newFile.path, fileName, widget.bizType);
}));
|