Flutter如何读取内部存储本地目录文件?图库模块开发求助
Flutter 图库模块实现方案
完全可以实现你需求的功能,以下是具体的分步实现思路和关键代码:
一、权限申请
首先必须获取存储访问权限,推荐使用permission_handler包:
- 配置权限:
- Android:在
android/app/src/main/AndroidManifest.xml中添加对应权限(Android 13+ 需用READ_MEDIA_IMAGES和READ_MEDIA_VIDEO,低版本用READ_EXTERNAL_STORAGE) - iOS:在
Info.plist中添加NSPhotoLibraryUsageDescription权限描述
- Android:在
- 权限请求代码:
import 'package:permission_handler/permission_handler.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'dart:io'; Future<bool> requestStoragePermissions() async { var status = await Permission.storage.request(); if (Platform.isAndroid && await DeviceInfoPlugin().androidInfo.version.sdkInt >= 33) { status = await Permission.photos.request(); await Permission.videos.request(); } return status.isGranted; }
二、读取'abc'文件夹下的媒体文件
直接构造目标文件夹路径,遍历子目录筛选对应类型的媒体文件:
import 'dart:io'; import 'package:path/path.dart' as path; // 定义媒体文件类型枚举 enum MediaType { image, video } // 获取指定类型的媒体文件列表 Future<List<File>> getMediaFiles(MediaType type) async { // 构造目标文件夹路径(Android公共内部存储路径) final dirPath = '/storage/emulated/0/abc/${type == MediaType.image ? '图片' : '视频'}'; final directory = Directory(dirPath); if (!await directory.exists()) return []; final files = await directory.list().where((entity) { final ext = path.extension(entity.path).toLowerCase(); // 根据类型筛选后缀 if (type == MediaType.image) { return ['.jpg', '.jpeg', '.png', '.gif'].contains(ext); } else { return ['.mp4', '.mov', '.avi', '.mkv'].contains(ext); } }).map((entity) => File(entity.path)).toList(); // 按修改日期倒序排列 files.sort((a, b) => b.statSync().modified.compareTo(a.statSync().modified)); return files; }
三、分标签页展示媒体内容
使用TabBar+TabBarView实现标签切换,配合GridView展示文件列表:
import 'package:flutter/material.dart'; class MediaGallery extends StatefulWidget { @override _MediaGalleryState createState() => _MediaGalleryState(); } class _MediaGalleryState extends State<MediaGallery> with SingleTickerProviderStateMixin { late TabController _tabController; List<File> _imageFiles = []; List<File> _videoFiles = []; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _loadMediaFiles(); } Future<void> _loadMediaFiles() async { final hasPermission = await requestStoragePermissions(); if (!hasPermission) return; final images = await getMediaFiles(MediaType.image); final videos = await getMediaFiles(MediaType.video); setState(() { _imageFiles = images; _videoFiles = videos; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( bottom: TabBar( controller: _tabController, tabs: const [ Tab(text: '图片'), Tab(text: '视频'), ], ), ), body: TabBarView( controller: _tabController, children: [ _buildMediaGrid(_imageFiles, MediaType.image), _buildMediaGrid(_videoFiles, MediaType.video), ], ), ); } Widget _buildMediaGrid(List<File> files, MediaType type) { if (files.isEmpty) return const Center(child: Text('暂无文件')); return GridView.builder( padding: const EdgeInsets.all(8), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 8, mainAxisSpacing: 8, ), itemCount: files.length, itemBuilder: (context, index) { final file = files[index]; final stat = file.statSync(); return MediaItemWidget(file: file, stat: stat, type: type); }, ); } }
四、展示文件信息与缩略图
自定义MediaItemWidget组件,整合缩略图、文件大小和修改日期:
import 'package:video_thumbnail/video_thumbnail.dart'; class MediaItemWidget extends StatelessWidget { final File file; final FileStat stat; final MediaType type; const MediaItemWidget({required this.file, required this.stat, required this.type, Key? key}) : super(key: key); // 格式化文件大小 String _formatFileSize(int bytes) { if (bytes >= 1024 * 1024) { return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; } else if (bytes >= 1024) { return '${(bytes / 1024).toStringAsFixed(1)} KB'; } else { return '$bytes B'; } } // 格式化日期 String _formatDateTime(DateTime dateTime) { return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')}'; } @override Widget build(BuildContext context) { return Stack( children: [ // 加载缩略图 FutureBuilder<String?>( future: type == MediaType.image ? Future.value(file.path) : VideoThumbnail.thumbnailFile( video: file.path, imageFormat: ImageFormat.JPEG, maxWidth: 120, quality: 75, ), builder: (context, snapshot) { if (snapshot.hasData && snapshot.data != null) { return Image.file(File(snapshot.data!), fit: BoxFit.cover, width: double.infinity, height: double.infinity); } return const Center(child: CircularProgressIndicator()); }, ), // 文件信息栏 Positioned( bottom: 0, left: 0, right: 0, child: Container( color: Colors.black54, padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_formatFileSize(stat.size), style: const TextStyle(color: Colors.white, fontSize: 12)), Text(_formatDateTime(stat.modified), style: const TextStyle(color: Colors.white, fontSize: 10)), ], ), ), ), // 视频标识 if (type == MediaType.video) const Positioned( top: 4, right: 4, child: Icon(Icons.play_circle, color: Colors.white, size: 20), ), ], ); } }
五、视频裁剪功能
使用video_compress包实现视频裁剪,指定起止时间即可:
import 'package:video_compress/video_compress.dart'; Future<File?> cropVideo(File videoFile, int startSecond, int endSecond) async { final result = await VideoCompress.compressVideo( videoFile.path, startTime: startSecond, endTime: endSecond, quality: VideoQuality.DefaultQuality, includeAudio: true, ); if (result != null && result.file != null) { return result.file; } return null; }
最后需要在pubspec.yaml中添加依赖:
dependencies: permission_handler: ^10.2.0 path_provider: ^2.0.15 video_thumbnail: ^0.5.3 video_compress: ^3.1.1 device_info_plus: ^9.0.2
内容的提问来源于stack exchange,提问作者Dhruvil Patel
相关产品推荐
相关产品推荐

