田螺收纳 app 需要拍摄衣物图片并上传服务器,研究了一波 Flutter 操作相机的细节,本文为实践笔记。
引入第三方库
编辑 pubspec.yaml 文件,添加第三库:
dependencies:
camera:
path_provider:
path:
- camera 获取手机上的相机,返回值是个数组,手机一般自带一个相机,还可以下载第三方相机如美颜相机等;
- path_provider 提供路徑來儲存照片;
- path 建立一個可以在使用任何裝置的路徑。
实现拍照功能
想一想我们如何使用 app 进行拍照,我们通常会点击拍照按钮,之后会跳转到一个拍照的页面。
编写拍照页面
如下是一个普通的拍照页面,你可以直接将它复制到您的项目中,也可以在阅读此页面源码后进行局部定制。
拍照页面接收两个参数:
- camera 您需要给拍照页面传递一个相机。如前文所述,一台手机上可能会有多个可使用的相机(手机自带相机、从应用商店下载的美颜相机);
- onOk 当您点击确认拍照时,在这个方法里编写需要做哪些事,如将拍照的图片上传到服务器或直接预览图片等。
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
class TakePictureScreen extends StatefulWidget {
final CameraDescription camera;
final Function onOk;
const TakePictureScreen({
Key key,
@required this.camera,
@required this.onOk,
}) : super(key: key);
@override
TakePictureScreenState createState() => TakePictureScreenState();
}
class TakePictureScreenState extends State<TakePictureScreen> {
CameraController _controller;
Future<void> _initializeControllerFuture;
String _imagePath;
@override
void initState() {
super.initState();
_controller = CameraController(
widget.camera,
ResolutionPreset.medium,
);
_initializeControllerFuture = _controller.initialize();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_imagePath != null) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: [
Image.file(File(_imagePath)),
Positioned(
bottom: 20.0,
left: 20.0,
child: Container(
decoration: new BoxDecoration(
border:
new Border.all(color: Colors.grey, width: 0.5), // 边色与边宽度
color: Colors.white, // 底色
shape: BoxShape.circle, // 圆形,使用圆形时不可以使用borderRadius
),
alignment: Alignment.center,
child: IconButton(
icon: Icon(Icons.replay),
onPressed: () {
setState(() {
_imagePath = null;
});
},
),
),
),
Positioned(
bottom: 20.0,
right: 20.0,
child: Container(
decoration: new BoxDecoration(
border:
new Border.all(color: Colors.grey, width: 0.5), // 边色与边宽度
color: Colors.white, // 底色
shape: BoxShape.circle, // 圆形,使用圆形时不可以使用borderRadius
),
alignment: Alignment.center,
child: IconButton(
icon: Icon(Icons.done),
onPressed: () {
widget.onOk(context, _imagePath);
},
),
),
),
],
),
);
}
return Scaffold(
body: FutureBuilder<void>(
future: _initializeControllerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return CameraPreview(_controller);
} else {
return Center(child: CircularProgressIndicator());
}
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.camera_alt),
onPressed: () async {
try {
await _initializeControllerFuture;
final String path = join(
(await getTemporaryDirectory()).path,
'${DateTime.now()}.png',
);
await _controller.takePicture(path);
setState(() {
_imagePath = path;
});
} catch (e) {
print(e);
}
},
),
);
}
}
点击按钮,获取相机,跳转到拍照页面
仅有拍照页面是无法完成拍照功能的,下面模拟在主页面点击按钮,我们需要获取手机上可用的相机,之后跳转到拍照页面。
onOk 方法接收两个参数:
- context 拍照页面(TakePictureScreen)的上下文,用于处理完业务逻辑后手动调用 Navigator.pop(context) 退出拍照页面;
- imagePath 在拍照页面进行拍摄后,照片会暂时存储在手机本地上,这个参数是存在手机上的路径;
IconButton(
icon: Icon(Icons.camera_alt),
onPressed: () async {
// 获取手机上可用的所有相机,返回值是个数组
final List<CameraDescription> cameras =
await availableCameras();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TakePictureScreen(
camera: cameras.first, // 使用数组中的第一个相机
onOk: (BuildContext context, String imagePath) async {
// ......
},
),
),
);
},
),
将拍摄的图片上传到服务器
完善前一步骤的 onOk 函数体内容,使用 dio(v3.0.0) 网络请求库上传图片。
onOk: (BuildContext context, String imagePath) async {
// 上传图片
FormData formData = FormData.fromMap({
'photo': await MultipartFile.fromFile(imagePath,
filename: '${DateTime.now()}.png'),
});
Response result = await HttpUtils.getInstance().post(
'/upload',
data: formData,
options: Options(headers: {
'Authorization': 'Bearer ' + token,
}),
);
print('result = $result');
// 业务逻辑处理完,记得退出拍照页面
Navigator.pop(context);
},
可能出现的报错
启动项目,可能会出现一些错误,总结如下:
Unhandled Exception: MissingPluginException(No implementation found for method canLaunch on channel plugins.flutter.io/url_launcher)
字面意思是插件找不到。
造成这个报错的原因是:我们先启动了 Flutter 应用,之后再去 pubspec.yaml 文件添加 camera 插件,此时是无法生效的,会报插件丢失的错误。
解决方法:关闭 Flutter 应用,重新启动即可。
uses-sdk:minSdkVersion 16 cannot be smaller than version 21 declared in library
字面意思,第三方库 camera 要求 minSdkVersion 版本最低为 21,但是我们项目里自带的 minSdkVersion 版本是 16,不兼容导致报错。
解决方法:将项目的 minSdkVersion 版本号手动改为 21 即可。
修改方法:在根目录下找到 /android/app/build.gradle 文件:
defaultConfig {
applicationId "com.example.projectname"
minSdkVersion 21 // <===================== 更改成 21,之前的默认值是 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}