20.3 相册、拍摄头像

20.3 相册、拍摄头像

img img

实现步骤:


第 1 步:拍照 相册 插件

  • pubspec.yaml
1
2
3
4
dependencies:
# 媒体选择
wechat_assets_picker: 7.2.0
wechat_camera_picker: 3.1.0
  • Android 配置

android/app/src/main/AndroidManifest.xml

1
2
3
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>

如果你的目标 SDK 版本大于 29, 你必须声明在 AndroidManifest.xml 的 节点中 声明 requestLegacyExternalStorage。

1
2
3
<application
...
android:requestLegacyExternalStorage="true">

第 2 步:ActionDialog 对话框组件

lib/common/widgets/dialog.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import 'package:flutter/material.dart';
import 'package:get/get.dart';

import '../index.dart';

/// 对话框
class ActionDialog {
static Future normal({
required BuildContext context,
Widget? title, // 标题
Widget? content, // 内容
Widget? confirm, // 确认按钮
Widget? cancel, // 取消按钮
Color? confirmBackgroundColor, // 确认按钮背景色
Function()? onConfirm, // 确认按钮回调
Function()? onCancel, // 取消按钮回调
}) async {
return await showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
padding: EdgeInsets.all(AppSpace.card),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
// 标题
DefaultTextStyle(
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.onPrimary,
),
child: title != null
? Padding(
padding: const EdgeInsets.only(top: 4),
child: title,
)
: Container(),
),

// 内容
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: DefaultTextStyle(
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColors.onPrimary,
),
child: content ?? Text(LocaleKeys.commonBottomRemove.tr),
),
),
SizedBox(height: AppSpace.listRow),

// 取消 确认
Row(
children: [
Expanded(
child: ButtonWidget.textRoundFilled(
LocaleKeys.commonBottomCancel.tr,
onTap: () {
Get.back(closeOverlays: true);
if (onCancel != null) onCancel();
},
),
),
Expanded(
child: ButtonWidget.textRoundFilled(
LocaleKeys.commonBottomConfirm.tr,
bgColor:
confirmBackgroundColor ?? AppColors.surfaceVariant,
onTap: () {
Get.back(closeOverlays: true);
if (onConfirm != null) onConfirm();
},
),
),
],
),
],
),
),
);
},
);
}
}

第 3 步:ActionPicker 选取器组件

lib/common/utils/picker.dart

1
2
3
// 视频配置,秒
const videoDurationMin = 6;
const videoDurationMax = 900;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/// 相册 assets
static Future<List<AssetEntity>?> assets({
required BuildContext context,
List<AssetEntity>? selected,
RequestType type = RequestType.image,
int maxAssets = 9,
SpecialPickerType? specialPickerType,
Widget? Function(BuildContext, AssetPathEntity?, int)? specialItemBuilder,
SpecialItemPosition specialItemPosition = SpecialItemPosition.none,
}) async {
var privilege = await Privilege.photos();
if (!privilege.result) {
await ActionDialog.normal(
context: context,
content: Text(privilege.message),
confirm: const Text('Setting'),
cancel: const Text('Not allowed'),
onConfirm: () => Privilege.openSettings(),
);
return null;
}
var result = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
selectedAssets: selected,
requestType: type,
maxAssets: maxAssets,
themeColor: AppColors.surfaceVariant,
specialPickerType: specialPickerType,
filterOptions: FilterOptionGroup(
orders: [const OrderOption(type: OrderOptionType.createDate)],
videoOption: const FilterOption(
durationConstraint: DurationConstraint(
min: Duration(seconds: videoDurationMin),
max: Duration(seconds: videoDurationMax),
),
),
),
specialItemPosition: specialItemPosition,
specialItemBuilder: specialItemBuilder,
),
);
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/// 相机
static Future<AssetEntity?> camera({
required BuildContext context,
bool enableRecording = true,
}) async {
var privilege = await Privilege.camera();
if (!privilege.result) {
await ActionDialog.normal(
context: context,
content: Text(privilege.message),
confirm: const Text('Setting'),
cancel: const Text('Not allowed'),
onConfirm: () => Privilege.openSettings(),
);
return null;
}
var result = await CameraPicker.pickFromCamera(
context,
pickerConfig: CameraPickerConfig(
enableRecording: enableRecording,
enableAudio: enableRecording,
textDelegate: enableRecording
? EnglishCameraPickerTextDelegateWithRecording()
: EnglishCameraPickerTextDelegate(),
resolutionPreset: ResolutionPreset.veryHigh,
),
);
return result;
}

第 4 步:工具 PickerImageWidget

lib/common/utils/picker_image.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import 'package:flutter/material.dart';
import 'package:flutter_woo_commerce_getx_learn/common/index.dart';
import 'package:get/get.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';

/// 选取图片 view
class PickerImageWidget extends StatelessWidget {
/// 返回拍摄图片
final Function(AssetEntity? result)? onTapTake;

/// 返回相册图片
final Function(List<AssetEntity>? result)? onTapAlbum;

const PickerImageWidget({
Key? key,
this.onTapTake,
this.onTapAlbum,
}) : super(key: key);

// 主视图
_buildView() {
return <Widget>[
// 拍照
ButtonWidget.primary(
LocaleKeys.pickerTakeCamera.tr,
icon: IconWidget.icon(
Icons.photo_camera,
color: AppColors.onPrimary,
),
onTap: onTapTake == null
? null
: () async {
var result = await ActionPicker.camera(
context: Get.context!,
enableRecording: false,
);
onTapTake!(result);
Get.back();
},
).paddingBottom(AppSpace.listRow),

// 相册
ButtonWidget.secondary(
LocaleKeys.pickerSelectAlbum.tr,
icon: IconWidget.icon(
Icons.photo_library,
color: AppColors.primary,
),
onTap: onTapAlbum == null
? null
: () async {
var result = await ActionPicker.assets(
context: Get.context!,
type: RequestType.image,
);
onTapAlbum!(result);
Get.back();
},
).paddingBottom(AppSpace.listRow),

// 返回
ButtonWidget.text(
LocaleKeys.commonBottomCancel.tr,
onTap: () => Get.back(),
),
]
.toColumn(
mainAxisSize: MainAxisSize.min,
)
.paddingAll(AppSpace.card)
.backgroundColor(AppColors.background);
}

@override
Widget build(BuildContext context) {
return _buildView();
}
}

第 5 步:控制器

lib/pages/my/profile_edit/controller.dart

1
2
// 本机图片file
File? filePhoto;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 选取照片
void onSelectPhoto() {
ActionBottomSheet.popModal(
context: Get.context,
child: PickerImageWidget(
// 拍照
onTapTake: (AssetEntity? result) async {
if (result != null) {
filePhoto = await result.file;
update(["profile_edit"]);
}
},
// 相册
onTapAlbum: (List<AssetEntity>? result) async {
if (result != null && result.isNotEmpty) {
filePhoto = await result.first.file;
update(["profile_edit"]);
}
},
),
);
}

第 6 步:视图

lib/pages/my/profile_edit/view.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 头像
Widget _buildAvatar() {
return ListTileWidget(
title: TextWidget.body1(LocaleKeys.profileEditMyPhoto.tr),
trailing: [
controller.filePhoto != null
? ImageWidget.file(
controller.filePhoto?.path ?? "",
width: 50.w,
height: 50.w,
fit: BoxFit.cover,
radius: 25.w,
)
: ImageWidget.url(
// UserService.to.profile.avatarUrl,
"https://ducafecat.oss-cn-beijing.aliyuncs.com/avatar/00258VC3ly1gty0r05zh2j60ut0u0tce02.jpg",
width: 50.w,
height: 50.w,
fit: BoxFit.cover,
radius: 25.w,
),
],
padding: EdgeInsets.all(AppSpace.card),
onTap: controller.onSelectPhoto,
).card().height(120.h).paddingBottom(AppSpace.card);
}

提交代码到 git