flutter_integrated_whb/lib/customWidget/photo_picker_row.dart

357 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'package:photo_manager/photo_manager.dart';
import 'ItemWidgetFactory.dart';
/// 媒体选择类型
enum MediaType { image, video }
/// 纵向滚动四列的媒体添加组件,支持拍摄、全屏相册多选,以及初始地址列表展示
/// 使用示例:
/// MediaPickerGrid(
/// maxCount: 4,
/// mediaType: MediaType.video,
/// initialMediaPaths: ['https://...', '/local/path.png'],
/// onChanged: (List<File> medias) {},
/// onMediaAdded: (String path) {},
/// onMediaRemoved: (String path) {},
/// ),
class MediaPickerRow extends StatefulWidget {
final int maxCount;
final MediaType mediaType;
final List<String>? initialMediaPaths;
final ValueChanged<List<File>> onChanged;
final ValueChanged<String>? onMediaAdded;
final ValueChanged<String>? onMediaRemoved;
const MediaPickerRow({
Key? key,
this.maxCount = 4,
this.mediaType = MediaType.image,
this.initialMediaPaths,
required this.onChanged,
this.onMediaAdded,
this.onMediaRemoved,
}) : super(key: key);
@override
_MediaPickerGridState createState() => _MediaPickerGridState();
}
class _MediaPickerGridState extends State<MediaPickerRow> {
final ImagePicker _picker = ImagePicker();
late List<String> _mediaPaths;
@override
void initState() {
super.initState();
_mediaPaths = widget.initialMediaPaths != null
? widget.initialMediaPaths!.take(widget.maxCount).toList()
: [];
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onChanged(
_mediaPaths.map((p) => p.startsWith('http') ? File('') : File(p)).toList(),
);
});
}
Future<void> _showPickerOptions() async {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
builder: (_) => SafeArea(
child: Wrap(
children: [
ListTile(
titleAlignment: ListTileTitleAlignment.center,
leading: Icon(
widget.mediaType == MediaType.image
? Icons.camera_alt
: Icons.videocam,
),
title: Text(
widget.mediaType == MediaType.image ? '拍照' : '拍摄视频',
),
onTap: () {
Navigator.of(context).pop();
_pickCamera();
},
),
ListTile(
titleAlignment: ListTileTitleAlignment.center,
leading: Icon(
widget.mediaType == MediaType.image
? Icons.photo_library
: Icons.video_library,
),
title: Text(
widget.mediaType == MediaType.image
? '从相册选择'
: '从相册选择视频',
),
onTap: () {
Navigator.of(context).pop();
_pickGallery();
},
),
ListTile(
titleAlignment: ListTileTitleAlignment.center,
leading: const Icon(Icons.close),
title: const Text('取消'),
onTap: () => Navigator.of(context).pop(),
),
],
),
),
);
}
Future<void> _pickCamera() async {
if (_mediaPaths.length >= widget.maxCount) return;
try {
XFile? picked;
if (widget.mediaType == MediaType.image) {
picked = await _picker.pickImage(source: ImageSource.camera);
} else {
picked = await _picker.pickVideo(source: ImageSource.camera);
}
if (picked != null) {
final path = picked.path;
setState(() => _mediaPaths.add(path));
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
widget.onMediaAdded?.call(path);
}
} catch (e) {
debugPrint('拍摄失败: $e');
}
}
Future<void> _pickGallery() async {
if (_mediaPaths.length >= widget.maxCount) return;
final permission = await PhotoManager.requestPermissionExtend();
if (permission != PermissionState.authorized &&
permission != PermissionState.limited) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请到设置中开启相册访问权限')),
);
return;
}
try {
final remaining = widget.maxCount - _mediaPaths.length;
final List<AssetEntity>? assets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
requestType: widget.mediaType == MediaType.image
? RequestType.image
: RequestType.video,
maxAssets: remaining,
gridCount: 4,
),
);
if (assets != null) {
for (final asset in assets) {
if (_mediaPaths.length >= widget.maxCount) break;
final file = await asset.file;
if (file != null) {
final path = file.path;
_mediaPaths.add(path);
widget.onMediaAdded?.call(path);
}
}
setState(() {});
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
}
} catch (e) {
debugPrint('相册选择失败: $e');
}
}
void _removeMedia(int index) {
final removed = _mediaPaths[index];
setState(() => _mediaPaths.removeAt(index));
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
widget.onMediaRemoved?.call(removed);
}
@override
Widget build(BuildContext context) {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1,
mainAxisExtent: 80,
),
itemCount: _mediaPaths.length < widget.maxCount
? _mediaPaths.length + 1
: widget.maxCount,
itemBuilder: (context, index) {
if (index < _mediaPaths.length) {
final path = _mediaPaths[index];
final isNetwork = path.startsWith('http');
return Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: widget.mediaType == MediaType.image
? (isNetwork
? Image.network(path, fit: BoxFit.cover,width: 80, height: 80,)
: Image.file(File(path), width: 80, height: 80, fit: BoxFit.cover))
: Container(
color: Colors.black12,
child: const Center(
child: Icon(
Icons.videocam,
color: Colors.white70,
),
),
),
),
Positioned(
top: -15,
right: -15,
child: IconButton(
icon: const Icon(Icons.cancel, size: 20, color: Colors.red),
onPressed: () => _removeMedia(index),
),
),
],
);
} else {
return GestureDetector(
onTap: _showPickerOptions,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black12),
borderRadius: BorderRadius.circular(5),
),
child: const Center(
child: Icon(Icons.camera_alt, color: Colors.black26),
),
),
);
}
},
);
}
}
/// 照片上传区域组件使用纵向四列Grid展示
class RepairedPhotoSection extends StatefulWidget {
final int maxCount;
final MediaType mediaType;
final String title;
final List<String>? initialMediaPaths;
final ValueChanged<List<File>> onChanged;
final ValueChanged<String>? onMediaAdded;
final ValueChanged<String>? onMediaRemoved;
final VoidCallback onAiIdentify;
final bool isShowAI;
final double horizontalPadding;
final bool isRequired;
final bool isShowNum;
const RepairedPhotoSection({
Key? key,
this.maxCount = 4,
this.mediaType = MediaType.image,
required this.title,
this.initialMediaPaths,
this.isShowAI = false,
required this.onChanged,
required this.onAiIdentify,
this.horizontalPadding = 5,
this.onMediaAdded,
this.onMediaRemoved,
this.isRequired = false,
this.isShowNum = true,
}) : super(key: key);
@override
_RepairedPhotoSectionState createState() => _RepairedPhotoSectionState();
}
class _RepairedPhotoSectionState extends State<RepairedPhotoSection> {
late List<String> _mediaPaths;
@override
void initState() {
super.initState();
_mediaPaths = widget.initialMediaPaths?.take(widget.maxCount).toList() ?? [];
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onChanged(_mediaPaths.map((p) => File(p)).toList());
});
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
padding: const EdgeInsets.only(left: 0, right: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding),
child: ListItemFactory.createRowSpaceBetweenItem(
leftText: widget.title,
rightText: widget.isShowNum ? '${_mediaPaths.length}/${widget.maxCount}' : '',
isRequired: widget.isRequired,
),
),
const SizedBox(height: 8),
Padding(
padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding),
child: MediaPickerRow(
maxCount: widget.maxCount,
mediaType: widget.mediaType,
initialMediaPaths: _mediaPaths,
onChanged: (files) {
final newPaths = files.map((f) => f.path).toList();
setState(() {
_mediaPaths = newPaths;
});
widget.onChanged(files);
},
onMediaAdded: widget.onMediaAdded,
onMediaRemoved: widget.onMediaRemoved,
),
),
const SizedBox(height: 20),
if (widget.isShowAI)
Padding(
padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding),
child: GestureDetector(
onTap: widget.onAiIdentify,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 15),
height: 36,
decoration: BoxDecoration(
color: const Color(0xFFDFEAFF),
borderRadius: BorderRadius.circular(18),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset('assets/images/ai_img.png', width: 20),
const SizedBox(width: 5),
const Text('AI隐患识别与处理'),
],
),
),
),
),
],
),
);
}
}