在最近的开发中,需要做一个选择图片(包括拍照和相册选择)然后上传的功能,我们的项目是iOS原生和flutter混编的,首先用flutter实现这个页面,选择了第三方插件image_picker,下面先看一下效果图
下面我们开始一步一步实现这个页面的逻辑,核心是在实现一个可复用的图片选择控件,支持设置最大选择图片数maxCount,支持删除。
第一步:集成image_picker ,导入图片资源(就是导入那个相机的icon和删除的icon这里就不展开说了)
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
image_picker: 0.6.0+8
我这里用的是0.6.0+8版本,可以自行选择最新版本,然后以iOS为例,需要添加访问相册的权限
Add the following keys to your Info.plist file, located in
:
NSPhotoLibraryUsageDescription
- describe why your app needs permission for the photo library. This is called Privacy - Photo Library Usage Description in the visual editor.NSCameraUsageDescription
- describe why your app needs access to the camera. This is called Privacy - Camera Usage Description in the visual editor.NSMicrophoneUsageDescription
- describe why your app needs access to the microphone, if you intend to record videos. This is called Privacy - Microphone Usage Description in the visual editor然后再用到的页面中 import 'package:image_picker/image_picker.dart';
第二步:根据需求封装一个图片选择控件,我这里是一行三张图片的布局,删除按钮在右上角
首先分析一下需求:
1.支持两种样式,有图片或者上传样式
2.在有图片的样式时,才显示右上角的删除icon
3.在上传样式时,点击弹出一个选择相机/相册的菜单(iOS中的ActionSheet)
4.图片样式时,点击预览大图(这个还没实现,后续有时间再更新)
需求理清晰了就可以开始撸码:
class UploadImageItem extends StatelessWidget {
final GestureTapCallback onTap;
final Function callBack;
final UploadImageModel imageModel;
final Function deleteFun;
UploadImageItem({this.onTap, this.callBack, this.imageModel, this.deleteFun});
@override
Widget build(BuildContext context) {
return Container(
width: 115,
height: 115,
child: Stack(
alignment: Alignment.topRight,
children: [
Container(
margin: EdgeInsets.only(top: 8, right: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
color: Color(0xFFF0F0F0)),
child: imageModel == null
? InkWell(
onTap: onTap ??
() {
BottomActionSheet.show(context, [
'相机',
'相册',
], callBack: (i) {
callBack(i);
return;
});
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: Image.asset(
'resources/image_picker.png',
),
),
Text(
'上传',
style: TextStyle(
fontSize: 12, color: Color(0xff999999)),
)
],
))
: Image.file(
imageModel.imageFile,
width: 105,
height: 105,
)),
Offstage(
offstage: (imageModel == null),
child: InkWell(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
child: Image.asset(
'resources/删除图片.png',
width: 16.0,
height: 16.0,
),
onTap: () {
print('点击了删除');
if (imageModel != null) {
deleteFun(this);
}
},
),
),
],
));
}
}
这里没有太多难点 ,主要是布局和事件传递
第三步:实现一个ImagePicker,里面要有存储图片的数据源,初始化的时候先添加一个图片状态的item,然后每次选择图片或拍照之后再添加有图状态的item,这里需要注意
1.添加到最大count时,移除无图状态的item
2.删除的时候再添加一个无图状态的item
class _UcarImagePickerState extends State {
List _images = []; //保存添加的图片
int currentIndex = 0;
bool isDelete = false;
@override
void initState() {
// TODO: implement initState
super.initState();
_images.add(UploadImageItem(
callBack: (int i) {
if (i == 0) {
print('打开相机');
_getImage(PickImageType.camera);
} else {
print('打开相册');
_getImage(PickImageType.gallery);
}
},
));
}
_getImage(PickImageType type) async {
var image = await ImagePicker.pickImage(
source: type == PickImageType.gallery
? ImageSource.gallery
: ImageSource.camera);
UploadImageItem();
setState(() {
print('add image at $currentIndex');
_images.insert(
_images.length - 1,
UploadImageItem(
imageModel: UploadImageModel(image, currentIndex),
deleteFun: (UploadImageItem item) {
print('remove image at ${item.imageModel.imageIndex}');
bool result = _images.remove(item);
print('left is ${_images.length}');
if (_images.length == widget.maxCount -1 && isDelete == false) {
isDelete = true;
_images.add(UploadImageItem(
callBack: (int i) {
if (i == 0) {
print('打开相机');
_getImage(PickImageType.camera);
} else {
print('打开相册');
_getImage(PickImageType.gallery);
}
},
));
}
print('remove result is $result');
setState(() {});
},
));
currentIndex++;
if (_images.length == widget.maxCount + 1) {
_images.removeLast();
isDelete = false;
}
});
}
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
color: Colors.white,
padding: EdgeInsets.only(top: 14, left: 20, bottom: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
widget.title,
style: TextStyle(
fontSize: 15.0,
color: Color(0xFF666666),
),
),
SizedBox(
height: 22,
),
Wrap(
alignment: WrapAlignment.start,
runSpacing: 10,
spacing: 10,
children: List.generate(_images.length, (i) {
return _images[i];
}),
)
],
),
);
}
}
基本到这里就把上述需求完成了,其中选择相机/相册的弹框,再之前的文章里面有介绍,这里就不再赘述了。
更新了这个demo,加入了camera和cached_network_image的使用,主要介绍一下camera的使用
- 添加所需依赖
dependencies:
flutter:
sdk: flutter
camera:
path_provider:
path:
- 获取可用相机列表
// Obtain a list of the available cameras on the device.
final cameras = await availableCameras();
// Get a specific camera from the list of available cameras.
final firstCamera = cameras.first;
- 创建并初始化 CameraController
// A screen that takes in a list of cameras and the Directory to store images.
class TakePictureScreen extends StatefulWidget {
final CameraDescription camera;
const TakePictureScreen({
Key key,
@required this.camera,
}) : super(key: key);
@override
TakePictureScreenState createState() => TakePictureScreenState();
}
class TakePictureScreenState extends State {
// Add two variables to the state class to store the CameraController and
// the Future.
CameraController _controller;
Future _initializeControllerFuture;
@override
void initState() {
super.initState();
// To display the current output from the camera,
// create a CameraController.
_controller = CameraController(
// Get a specific camera from the list of available cameras.
widget.camera,
// Define the resolution to use.
ResolutionPreset.medium,
);
// Next, initialize the controller. This returns a Future.
_initializeControllerFuture = _controller.initialize();
}
@override
void dispose() {
// Dispose of the controller when the widget is disposed.
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Fill this out in the next steps.
}
}
- 使用 CameraController
拍摄一张图片
FloatingActionButton(
child: Icon(Icons.camera_alt),
// Provide an onPressed callback.
onPressed: () async {
// Take the Picture in a try / catch block. If anything goes wrong,
// catch the error.
try {
// Ensure that the camera is initialized.
await _initializeControllerFuture;
// Construct the path where the image should be saved using the path
// package.
final path = join(
// Store the picture in the temp directory.
// Find the temp directory using the `path_provider` plugin.
(await getTemporaryDirectory()).path,
'${DateTime.now()}.png',
);
// Attempt to take a picture and log where it's been saved.
await _controller.takePicture(path);
} catch (e) {
// If an error occurs, log the error to the console.
print(e);
}
},
)
- 使用 CameraPreview
展示相机的帧流
// You must wait until the controller is initialized before displaying the
// camera preview. Use a FutureBuilder to display a loading spinner until the
// controller has finished initializing.
FutureBuilder(
future: _initializeControllerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If the Future is complete, display the preview.
return CameraPreview(_controller);
} else {
// Otherwise, display a loading indicator.
return Center(child: CircularProgressIndicator());
}
},
)
- 使用 Image
组件展示图片
class DisplayPictureScreen extends StatelessWidget {
final String imagePath;
const DisplayPictureScreen({Key key, this.imagePath}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Display the Picture')),
// The image is stored as a file on the device. Use the `Image.file`
// constructor with the given path to display the image.
body: Image.file(File(imagePath)),
);
}
}
demo地址:https://github.com/AnleSu/image_picker_demo