在移动应用开发中,文件上传和预览功能是几乎所有社交应用都需要具备的重要功能,尤其是在涉及图片和视频上传时。用户对于这些功能的期望,通常要求界面流畅、操作简便。而在众多社交软件中,微信无疑是最受欢迎的应用之一,它的文件上传与预览功能可谓是做得相当完善。
那么问题来了:有没有可能使用 Flutter 完美复刻微信中的文件上传和预览功能?
答案是肯定的!接下来,我们将一起探讨如何在 Flutter 中构建一个类似微信的文件上传与预览功能,并实现用户所期望的流畅体验。
为什么选择 Flutter?
Flutter 是一个由 Google 推出的开源跨平台开发框架,它可以帮助开发者通过一套代码同时构建 Android 和 iOS 应用。Flutter 不仅支持优秀的跨平台开发体验,还具备高性能和流畅的 UI 渲染能力。在构建复杂的用户界面和交互时,Flutter 提供了更多的灵活性和控制。
对于文件上传和媒体预览功能的实现,Flutter 提供了强大的组件和丰富的第三方库支持,这使得我们可以轻松复刻出微信的文件上传和预览功能。
如何复刻微信的文件上传与预览功能?
在微信中,文件上传和预览功能不仅支持图片和视频文件的选择,还具备清晰的上传进度、文件删除以及文件预览功能。通过合理的组件组合和第三方库,我们可以在 Flutter 中完美复刻这些功能。
1. 文件选择:支持多种来源
微信中的文件选择功能支持从相册、文件管理器中选择图片和视频,还可以直接使用相机拍照或录像。要实现这一点,我们需要使用以下两款第三方库:
wechat_assets_picker
:用于从相册中选择图片、视频或其他媒体文件。wechat_camera_picker
:用于调用相机拍摄照片和录像。
这两款库的使用非常简单,我们可以通过它们来实现多种来源的文件选择功能。 同时我们了解到,微信的媒体功能是多样化的,但是首先,我们可以先实现一个弹窗,用于我们后续的媒体多样化选择
示例代码:媒体选择弹窗
class MediaPickerMenu extends StatefulWidget {
final List<uploadOptions> availableOptions; // 可选择的上传选项
final int maxFiles; // 最大上传数量
final Function takeAsset; // 拍照或录像
final Function selectAssets; // 选择图片或视频
const MediaPickerMenu(
{Key? key,
required this.availableOptions,
required this.maxFiles,
required this.takeAsset,
required this.selectAssets})
: super(key: key);
@override
_MediaPickerMenuState createState() => _MediaPickerMenuState();
}
class _MediaPickerMenuState extends State<MediaPickerMenu> {
final throttle = Throttle(milliseconds: 1000); // 1秒节流时间
// 构建选择菜单项
Widget _buildPickerOption(
{required String title,
required IconData icon,
bool isShowBorder = true,
required Function onTapAction}) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: GestureDetector(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 22,
),
SizedBox(
width: 10,
),
Text(
title,
style: TextStyle(
fontSize: 14,
),
),
],
),
onTap: () {
throttle.run(
() async {
Navigator.of(context).pop();
await onTapAction();
},
);
},
),
),
isShowBorder
? Divider(
// Divider 用于在 ListTile 下面添加一条线
color: Colors.grey.shade200,
height: 0.5, // 设置高度,控制分割线的粗细
)
: Container()
],
);
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: SafeArea(
child: Wrap(
children: <Widget>[
if (widget.availableOptions.contains(uploadOptions.camera))
_buildPickerOption(
title: '拍摄',
icon: Icons.camera_alt,
onTapAction: widget.takeAsset),
if (widget.availableOptions.contains(uploadOptions.camera))
_buildPickerOption(
title: '图库',
icon: Icons.image,
onTapAction: widget.selectAssets),
Container(
height: 10,
color: Colors.grey.shade100,
),
_buildPickerOption(
title: '取消',
icon: Icons.close,
onTapAction: () {
// Navigator.of(context).pop();
},
isShowBorder: false,
),
],
),
),
);
}
}
这段代码定义了一个自定义的Flutter控件,名为 MediaPickerMenu
,用于实现选择媒体(如拍照、从图库选择图片或视频等)功能的菜单。让我们逐步分析这段代码,帮助你理解每一部分的含义和功能。
第一部分:MediaPickerMenu
类
class MediaPickerMenu extends StatefulWidget {
final List<uploadOptions> availableOptions; // 可选择的上传选项
final int maxFiles; // 最大上传数量
final Function takeAsset; // 拍照或录像
final Function selectAssets; // 选择图片或视频
const MediaPickerMenu(
{Key? key,
required this.availableOptions,
required this.maxFiles,
required this.takeAsset,
required this.selectAssets})
: super(key: key);
@override
_MediaPickerMenuState createState() => _MediaPickerMenuState();
}
解释:
-
MediaPickerMenu
是一个StatefulWidget
,意味着它有状态,可以在运行时进行变化。 -
该控件接受四个参数:
availableOptions
:一个List<uploadOptions>
,表示用户可以选择的上传选项(例如拍照或从图库选择)。maxFiles
:一个整数,表示最大文件上传数量(这个参数在当前代码中并没有实际使用,但可以扩展功能时用到)。takeAsset
:一个回调函数,表示拍照或录像的操作。selectAssets
:另一个回调函数,表示选择图片或视频的操作。
-
createState
方法会返回一个_MediaPickerMenuState
的实例,用于管理MediaPickerMenu
的状态。
第二部分:_MediaPickerMenuState
类
class _MediaPickerMenuState extends State<MediaPickerMenu> {
final throttle = Throttle(milliseconds: 1000); // 1秒节流时间
解释:
_MediaPickerMenuState
负责管理MediaPickerMenu
的具体UI逻辑和状态。throttle
是一个节流工具,确保在1秒内不会重复触发某个操作。它的作用是防止用户快速点击菜单项时,触发过多的操作。比如,如果点击拍照或者选择图库时,它会限制1秒内只能触发一次操作。
第三部分:_buildPickerOption
方法
Widget _buildPickerOption(
{required String title,
required IconData icon,
bool isShowBorder = true,
required Function onTapAction}) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: GestureDetector(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 22,
),
SizedBox(
width: 10,
),
Text(
title,
style: TextStyle(
fontSize: 14,
),
),
],
),
onTap: () {
throttle.run(
() async {
Navigator.of(context).pop();
await onTapAction();
},
);
},
),
),
isShowBorder
? Divider(
color: Colors.grey.shade200,
height: 0.5,
)
: Container()
],
);
}
解释:
- 该方法构建了一个菜单项。每个菜单项包含一个图标、标题以及点击时的响应。
GestureDetector
是一个手势识别控件,用于识别用户的点击事件(onTap
)。throttle.run
确保点击操作不会频繁执行。- 点击后,
Navigator.of(context).pop()
会关闭当前的菜单弹窗。 onTapAction()
是传入的回调函数,它会在点击后执行对应的操作,比如拍照或选择文件。- 如果
isShowBorder
为true
,则显示一个分割线,用于视觉分隔菜单项。
第四部分:build
方法
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: SafeArea(
child: Wrap(
children: <Widget>[
if (widget.availableOptions.contains(uploadOptions.camera))
_buildPickerOption(
title: '拍摄',
icon: Icons.camera_alt,
onTapAction: widget.takeAsset),
if (widget.availableOptions.contains(uploadOptions.camera))
_buildPickerOption(
title: '图库',
icon: Icons.image,
onTapAction: widget.selectAssets),
Container(
height: 10,
color: Colors.grey.shade100,
),
_buildPickerOption(
title: '取消',
icon: Icons.close,
onTapAction: () {
// Navigator.of(context).pop();
},
isShowBorder: false,
),
],
),
),
);
}
解释:
-
build
方法是 Flutter 控件的核心构建方法,用于构建控件的UI。 -
使用
Wrap
控件可以将多个子控件包装成可自动换行的布局。 -
菜单项通过
_buildPickerOption
方法动态构建:- 如果
availableOptions
中包含uploadOptions.camera
,则显示“拍摄”项。 - 如果
availableOptions
中包含uploadOptions.image
,则显示“图库”项。 - 最后,添加一个“取消”项,它没有边框且点击后不会执行任何操作(仅关闭菜单)。
- 如果
弹窗效果:
既然我们已经实现了弹窗的样式和功能,接下来就是我们媒体选择和拍照的功能了
示例代码:拍摄/图片功能实现
拍摄和选择功能的实现
我们需要实现拍照和从相册选择文件的功能。我们分别定义两个方法 _takeAsset
和 _selectAssets
来处理拍照和选择文件的逻辑。
class ImageUploadWidget extends StatefulWidget {
final Function(List) onUploadComplete; // 上传完成的回调
final double size; // 每个文件展示框的大小
final int maxFiles; // 最大上传数量
final List<uploadOptions> availableOptions; // 可选择的上传选项
final Function(bool) onChangeStatus;
final Widget? child;
const ImageUploadWidget({
Key? key,
required this.onUploadComplete,
this.size = 80,
this.maxFiles = 9,
this.availableOptions = const [
uploadOptions.camera,
uploadOptions.gallery,
uploadOptions.video,
],
required this.onChangeStatus,
this.child,
}) : super(key: key);
@override
_ImageUploadWidgetState createState() => _ImageUploadWidgetState();
}
class _ImageUploadWidgetState extends State<ImageUploadWidget> {
List<File> _selectedFiles = []; // 当前选择的文件
Map<File, uploadStatus> _uploadStatus = {}; // 文件的上传状态
List<Map> _fileUrls = []; // 上传成功的网络地址列表
// 打开选择菜单
Future<void> _showPickerMenu() async {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return MediaPickerMenu(
availableOptions: widget.availableOptions,
maxFiles: widget.maxFiles - _selectedFiles .length,
takeAsset: _takeAsset,
selectAssets: _selectAssets,
);
},
);
}
// 上传文件
Future<void> _uploadFiles(List<File> files) async {
widget.onChangeStatus(true);
setState(() {
// 设置所有文件的上传状态为上传中
for (var file in files) {
_uploadStatus[file] = uploadStatus.uploading;
}
});
try {
// 调用 FileUploader 的上传逻辑
FileUploader().uploadFiles(
files: files,
onSuccess: (response) {
print('上传成功files: ${files[0]}');
response.data['data'].forEach((element) {
print('element:$element');
Map item = {
"type": "${element['fileType']}",
"url": "${element['url']}",
"fileName": "${element['originalFilename']}"
};
setState(() {
_fileUrls.add(item);
// 更新上传状态
for (var file in files) {
_uploadStatus[file] = uploadStatus.success;
}
});
});
// 通知父组件上传完成,返回 URL 数组
widget.onUploadComplete(_fileUrls);
widget.onChangeStatus(false);
},
onError: (error) {
setState(() {
// 更新状态为上传失败
for (var file in files) {
_uploadStatus[file] = uploadStatus.failure;
}
});
widget.onChangeStatus(false);
print('上传失败: $error');
},
);
} catch (e) {
setState(() {
// 更新状态为上传失败
for (var file in files) {
_uploadStatus[file] = uploadStatus.failure;
}
});
widget.onChangeStatus(false);
print('上传出错: $e');
}
}
// 删除文件
void _deleteFile(File file) {
setState(() {
_selectedFiles.remove(file);
_uploadStatus.remove(file);
_fileUrls.removeWhere((urlMap) {
final fileName = file.path.split('/').last;
final urlFileName = urlMap['fileName'] ?? '';
return fileName == urlFileName; // 比较文件名是否相同
});
widget.onUploadComplete(_fileUrls); // 通知父组件 URL 数组变化
});
}
// 选择图片或视频
Future<void> _selectAssets() async {
final List<AssetEntity>? assets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
maxAssets: widget.maxFiles - _selectedFiles.length,
requestType: widget.availableOptions.contains(uploadOptions.video)
? RequestType.all
: RequestType.image,
),
);
if (assets != null && assets.isNotEmpty) {
List<File> newFiles = [];
for (var asset in assets) {
final File? file = await asset.file;
if (file != null && !_selectedFiles.contains(file)) {
newFiles.add(file);
}
}
if (newFiles.isNotEmpty) {
setState(() {
_selectedFiles.addAll(newFiles);
// 为每个新文件设置初始状态为待上传
newFiles.forEach((file) {
_uploadStatus[file] = uploadStatus.pending;
});
});
// 立即上传所有选择的文件
_uploadFiles(newFiles);
}
}
}
// 拍照或录像
Future<void> _takeAsset() async {
final AssetEntity? asset = await CameraPicker.pickFromCamera(
context,
pickerConfig: CameraPickerConfig(
enableRecording: widget.availableOptions.contains(uploadOptions.video),
),
);
if (asset != null) {
final File? file = await asset.file;
if (file != null && !_selectedFiles.contains(file)) {
setState(() {
_selectedFiles.add(file);
_uploadStatus[file] = uploadStatus.pending;
});
// 立即上传
_uploadFiles([file]);
}
}
}
@override
Widget build(BuildContext context) {
return widget.child != null
? GestureDetector(
onTap: _showPickerMenu, // 点击显示选择菜单
child: widget.child ?? Container(),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 文件网格视图
_selectedFiles.isNotEmpty
? GridView.builder(
shrinkWrap: true,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: _selectedFiles.length,
itemBuilder: (context, index) {
final file = _selectedFiles[index];
return FilePreview(
file: file,
status: _uploadStatus[file] ?? uploadStatus.pending,
onDelete: () => _deleteFile(file),
);
},
)
: Container(),
// 添加文件按钮
if (_selectedFiles.length < widget.maxFiles)
GestureDetector(
onTap: _showPickerMenu,
child: Container(
width: widget.size,
height: widget.size,
margin: EdgeInsets.only(top: 10,),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.add,
color: Colors.grey,
size: 40,
),
),
),
],
);
}
}
第一部分:ImageUploadWidget
类
class ImageUploadWidget extends StatefulWidget {
final Function(List) onUploadComplete; // 上传完成的回调
final double size; // 每个文件展示框的大小
final int maxFiles; // 最大上传数量
final List<uploadOptions> availableOptions; // 可选择的上传选项
final Function(bool) onChangeStatus;
final Widget? child;
const ImageUploadWidget({
Key? key,
required this.onUploadComplete,
this.size = 80,
this.maxFiles = 9,
this.availableOptions = const [
uploadOptions.camera,
uploadOptions.gallery,
uploadOptions.video,
],
required this.onChangeStatus,
this.child,
}) : super(key: key);
@override
_ImageUploadWidgetState createState() => _ImageUploadWidgetState();
}
解释:
-
ImageUploadWidget
是一个StatefulWidget
,表示该控件有状态,UI 可以根据数据变化进行更新。 -
该控件接收以下参数:
onUploadComplete
:上传完成后回调,返回上传文件的 URL 列表。size
:每个文件展示框的大小,默认为 80。maxFiles
:最大上传数量,默认为 9。availableOptions
:一个列表,表示用户可选择的上传选项(包括拍照、从图库选择和选择视频)。onChangeStatus
:状态变化的回调,用于通知父组件上传状态的变化。child
:一个可选的子组件,允许外部传入自定义内容。
第二部分:_ImageUploadWidgetState
类
class _ImageUploadWidgetState extends State<ImageUploadWidget> {
List<File> _selectedFiles = []; // 当前选择的文件
Map<File, uploadStatus> _uploadStatus = {}; // 文件的上传状态
List<Map> _fileUrls = []; // 上传成功的网络地址列表
解释:
-
_ImageUploadWidgetState
是ImageUploadWidget
的状态类,负责处理文件选择、上传和状态更新等操作。 -
定义了以下状态变量:
_selectedFiles
:当前已选择的文件列表。_uploadStatus
:一个映射,记录每个文件的上传状态。_fileUrls
:上传成功后,存储文件的网络地址。
第三部分:打开选择菜单
Future<void> _showPickerMenu() async {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return MediaPickerMenu(
availableOptions: widget.availableOptions,
maxFiles: widget.maxFiles - _selectedFiles.length,
takeAsset: _takeAsset,
selectAssets: _selectAssets,
);
},
);
}
解释:
_showPickerMenu
方法打开底部选择菜单,显示用户可以选择的上传选项(例如拍照、从图库选择文件或视频)。MediaPickerMenu
控件接受availableOptions
(可选项)、maxFiles
(剩余上传次数)以及处理拍照和选择文件的回调takeAsset
和selectAssets
。
第四部分:上传文件
Future<void> _uploadFiles(List<File> files) async {
widget.onChangeStatus(true); // 开始上传,改变状态
setState(() {
// 设置所有文件的上传状态为上传中
for (var file in files) {
_uploadStatus[file] = uploadStatus.uploading;
}
});
try {
// 调用 FileUploader 上传文件
FileUploader().uploadFiles(
files: files,
onSuccess: (response) {
print('上传成功files: ${files[0]}');
response.data['data'].forEach((element) {
print('element:$element');
Map item = {
"type": "${element['fileType']}",
"url": "${element['url']}",
"fileName": "${element['originalFilename']}"
};
setState(() {
_fileUrls.add(item);
// 更新上传状态
for (var file in files) {
_uploadStatus[file] = uploadStatus.success;
}
});
});
widget.onUploadComplete(_fileUrls); // 通知父组件上传完成
widget.onChangeStatus(false);
},
onError: (error) {
setState(() {
// 更新状态为上传失败
for (var file in files) {
_uploadStatus[file] = uploadStatus.failure;
}
});
widget.onChangeStatus(false);
print('上传失败: $error');
},
);
} catch (e) {
setState(() {
// 更新状态为上传失败
for (var file in files) {
_uploadStatus[file] = uploadStatus.failure;
}
});
widget.onChangeStatus(false);
print('上传出错: $e');
}
}
解释:
_uploadFiles
方法负责处理文件的上传逻辑。- 使用
FileUploader
上传文件,上传过程中,文件的状态会更新为 "上传中"。 - 上传成功后,会返回文件的网络 URL 和其他信息,并将这些信息添加到
_fileUrls
列表中,同时更新文件状态为 "上传成功"。 - 上传失败时,文件的状态更新为 "上传失败"。
第五部分:删除文件
void _deleteFile(File file) {
setState(() {
_selectedFiles.remove(file);
_uploadStatus.remove(file);
_fileUrls.removeWhere((urlMap) {
final fileName = file.path.split('/').last;
final urlFileName = urlMap['fileName'] ?? '';
return fileName == urlFileName; // 比较文件名是否相同
});
widget.onUploadComplete(_fileUrls); // 通知父组件 URL 数组变化
});
}
解释:
_deleteFile
方法允许用户删除已选择的文件。- 删除文件后,更新
_selectedFiles
、_uploadStatus
和_fileUrls
,并通知父组件上传完成的 URL 列表发生变化。
效果演示:
第六部分:选择图片或视频
Future<void> _selectAssets() async {
final List<AssetEntity>? assets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
maxAssets: widget.maxFiles - _selectedFiles.length,
requestType: widget.availableOptions.contains(uploadOptions.video)
? RequestType.all
: RequestType.image,
),
);
if (assets != null && assets.isNotEmpty) {
List<File> newFiles = [];
for (var asset in assets) {
final File? file = await asset.file;
if (file != null && !_selectedFiles.contains(file)) {
newFiles.add(file);
}
}
if (newFiles.isNotEmpty) {
setState(() {
_selectedFiles.addAll(newFiles);
// 为每个新文件设置初始状态为待上传
newFiles.forEach((file) {
_uploadStatus[file] = uploadStatus.pending;
});
});
// 立即上传所有选择的文件
_uploadFiles(newFiles);
}
}
}
解释:
_selectAssets
方法允许用户选择图片或视频。选择时,调用AssetPicker
来选择文件,并根据availableOptions
设置选择类型(图片或视频)。- 选择的文件会添加到
_selectedFiles
列表中,并设置文件的上传状态为 "待上传"。 - 立即开始上传选中的文件。
效果演示:
第七部分:拍照或录像
Future<void> _takeAsset() async {
final AssetEntity? asset = await CameraPicker.pickFromCamera(
context,
pickerConfig: CameraPickerConfig(
enableRecording: widget.availableOptions.contains(uploadOptions.video),
),
);
if (asset != null) {
final File? file = await asset.file;
if (file != null && !_selectedFiles.contains(file)) {
setState(() {
_selectedFiles.add(file);
_uploadStatus[file] = uploadStatus.pending;
});
// 立即上传
_uploadFiles([file]);
}
}
}
解释:
_takeAsset
方法允许用户使用摄像头拍照或录像。- 如果用户拍照或录像成功,会将文件添加到
_selectedFiles
列表,并立即上传。
效果演示:
示例代码:缩略图
class FilePreview extends StatefulWidget {
final File file;
final uploadStatus status;
final VoidCallback onDelete;
const FilePreview({
Key? key,
required this.file,
required this.status,
required this.onDelete,
}) : super(key: key);
@override
_FilePreviewState createState() => _FilePreviewState();
}
class _FilePreviewState extends State<FilePreview> {
Future<Uint8List?>? _thumbnailFuture;
@override
void initState() {
super.initState();
// 如果是视频文件,生成视频缩略图
if (widget.file.path.endsWith('.mp4')) {
_thumbnailFuture = VideoThumbnail.thumbnailData(
video: widget.file.path,
imageFormat: ImageFormat.JPEG,
maxWidth: 128, // 缩略图最大宽度
quality: 75, // 缩略图质量
);
}
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
// 文件预览
FutureBuilder<Uint8List?>(
future: _thumbnailFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
// 判断文件类型
if (snapshot.hasData && snapshot.data != null) {
// 如果是视频文件,显示视频缩略图
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: MemoryImage(snapshot.data!),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Icon(
Icons.play_circle_fill,
color: Colors.white,
size: 40,
),
),
);
} else if (widget.file.path.endsWith('.mp4')) {
// 如果没有视频缩略图,则显示默认的“损坏图标”
return Container(
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Icon(
Icons.broken_image,
color: Colors.black54,
size: 40,
),
),
);
} else {
// 如果是图片文件,直接展示
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: FileImage(widget.file),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(8),
),
);
}
},
),
// 删除按钮
Positioned(
top: 4,
right: 4,
child: GestureDetector(
onTap: widget.onDelete,
child: const CircleAvatar(
radius: 12,
backgroundColor: Colors.black54,
child: Icon(
Icons.close,
color: Colors.white,
size: 16,
),
),
),
),
],
);
}
}
FilePreview
组件,用于展示上传的文件预览。它支持显示图片和视频文件的预览,对于视频文件还生成了缩略图。组件还提供了删除功能,允许用户删除已选择的文件。### 1. 类定义与构造函数
FilePreview
是一个 StatefulWidget
,用来展示单个文件的预览。它有以下属性:
file
: 要展示的文件,类型为File
。status
: 文件的上传状态,类型为uploadStatus
(一个枚举值)。onDelete
: 点击删除按钮时调用的回调函数,类型为VoidCallback
。
构造函数需要这三个参数,并传递给 State
类 _FilePreviewState
。
2. 状态管理 (_FilePreviewState
)
_FilePreviewState
负责文件预览的实现。主要包含:
_thumbnailFuture
: 一个Future<Uint8List?>
类型的变量,用于存储视频文件的缩略图数据。当文件是视频时,_thumbnailFuture
会被赋值为生成缩略图的异步操作。- 在
initState
方法中,检查文件是否为视频(通过文件扩展名.mp4
),如果是视频,则通过VideoThumbnail.thumbnailData
方法生成缩略图。thumbnailData
返回一个包含缩略图数据的Uint8List
。
3. 界面构建 (build
)
build
方法返回一个 Stack
小部件,堆叠显示文件的预览和删除按钮。
-
文件预览:
-
使用
FutureBuilder
异步加载视频文件的缩略图。 -
FutureBuilder
根据_thumbnailFuture
的状态渲染不同内容:- 正在加载时:显示一个圆形加载指示器(
CircularProgressIndicator
)。 - 有缩略图时:如果文件是视频且成功生成了缩略图,使用
MemoryImage
显示缩略图。 - 视频文件无缩略图时:如果没有生成缩略图,则显示一个默认的图标(
Icons.broken_image
)表示损坏的图片。 - 图片文件:如果文件是图片,直接显示图片的
FileImage
。
- 正在加载时:显示一个圆形加载指示器(
-
-
删除按钮:
- 使用
Positioned
小部件在预览的右上角展示一个删除按钮,按钮是一个圆形的CircleAvatar
,点击后调用onDelete
回调函数。
- 使用
4. 关键方法与功能
- 生成缩略图 (
_thumbnailFuture
) :对于视频文件,使用VideoThumbnail.thumbnailData
方法生成缩略图数据。FutureBuilder
会监听这个Future
并在缩略图加载完成后进行渲染。 - 删除文件 (
onDelete
) :删除按钮的onTap
事件会触发widget.onDelete
,这个回调通常会在父组件中处理文件的删除逻辑。
5. 布局与样式
-
文件预览布局:
Container
包含了DecorationImage
,用于展示文件的图像或视频缩略图。- 使用
BoxFit.cover
来确保图像或者缩略图填充容器并且保持宽高比。 - 使用
BorderRadius.circular(8)
为容器添加圆角。
-
删除按钮布局:
Positioned
小部件将删除按钮固定在预览框的右上角。- 使用
CircleAvatar
来创建圆形按钮,Icons.close
作为删除图标。
还有我开发过程中所自定义的枚举类型:
enum uploadOptions {
camera, // 拍照
gallery, // 选择图片
video, // 选择视频
file, // 选择文件
}
//上传状态
enum uploadStatus {
pending,
uploading,
success,
failure,
}
虽然FLUTTER很迪奥,但是VUE总有一天会统治世界!!
完结撒花❀