• Flutter聊天布局之图片&视频上传、显示、保存到相册


    目录

    用到的组件

    ios和android端配置文件

    ios

    android

    主页部分代码

    显示及保存照片组件代码

    显示及保存视频组件代码

    视频演示


    接上文Flutter简单聊天界面布局及语音录制播放_chw-di的博客-CSDN博客

    本文主要对聊天布局内的图片及视频的上传、显示和保存到相册进行简单开发。

    用到的组件

    1. #相册插件
    2. image_picker: ^0.8.5+3
    3. #查看图片组件
    4. photo_view: ^0.14.0
    5. #缓存照片插件
    6. cached_network_image: ^3.2.1
    7. #视频播放
    8. video_player: ^2.4.7
    9. #视频缩略图
    10. video_thumbnail: ^0.5.2
    11. #文件目录获取
    12. path_provider: ^2.0.11
    13. #保存视频、照片到本地相册
    14. image_gallery_saver: ^1.7.1

    ios和android端配置文件

    ios

    在info.plist中添加

    1. <key>NSPhotoLibraryAddUsageDescription</key>
    2. <string>保存图片</string>
    3. <key>NSAppTransportSecurity</key>
    4. <string>http</string>

    android

    在AndroidManifest.xml中添加

    1. <uses-permission android:name="android.permission.INTERNET"/>
    1. <application ...
    2. android:requestLegacyExternalStorage="true"
    3. ...
    4. </application>

    主页部分代码

    1. //获取相册照片并上传
    2. _getPhotos() async {
    3. final XFile? pickImage =
    4. await _picker.pickImage(source: ImageSource.gallery);
    5. //上传
    6. var filePath = await _uploadFile(pickImage!, MessageType.photo);
    7. insertFile(MessageType.photo,filePath,"");
    8. }
    9. //拍照并上传
    10. _takePhotos() async {
    11. final XFile? pickImage =
    12. await _picker.pickImage(source: ImageSource.camera);
    13. //上传
    14. var filePath = await _uploadFile(pickImage!, MessageType.photo);
    15. insertFile(MessageType.photo,filePath,"");
    16. }
    17. //上传视频
    18. _getVideo() async {
    19. final XFile? pickImage = await _picker.pickVideo(source: ImageSource.gallery);
    20. //获取缩略图文件
    21. File videoThumbnailFile = await _getVideoThumbnail(pickImage!);
    22. XFile file = XFile(videoThumbnailFile.path);
    23. //上传缩略图
    24. var videoThumbnailFilePath = await _uploadFile(file, MessageType.photo);
    25. //上传视频
    26. var videoPath = await _uploadFile(pickImage, MessageType.video);
    27. insertFile(MessageType.video,videoPath,videoThumbnailFilePath);
    28. }
    29. //获取视频缩略图
    30. Future _getVideoThumbnail(XFile videoFile) async {
    31. Uint8List? thumbnail = await VideoThumbnail.thumbnailData(
    32. video: videoFile.path,
    33. imageFormat: ImageFormat.JPEG,
    34. quality: 25,
    35. );
    36. var tempDir = await getTemporaryDirectory();
    37. //生成file文件格式
    38. String videoThumbnail = '${tempDir.path}/image_${DateTime.now().millisecond}.jpg';
    39. var file = await File(videoThumbnail).create();
    40. file.writeAsBytesSync(thumbnail!);
    41. return file;
    42. }
    43. //上传文件
    44. Future<String> _uploadFile(XFile imageDir, String type) async {
    45. String filePath = imageDir.path;
    46. var list = filePath.split(".");
    47. var last = list.last;
    48. String fileName = "${const Uuid().v4()}.$last";
    49. FormData formData = FormData.fromMap({
    50. "file": await MultipartFile.fromFile(imageDir.path, filename: fileName),
    51. });
    52. Dio dio = Dio();
    53. var response = await dio.post("http://192.168.9.253:8091/sc/file/upload", data: formData);
    54. var path = response.data["data"]["detail"]["filePath"];
    55. return path;
    56. }
    57. //写入文件
    58. void insertFile(String type,String path,String thumbnail) {
    59. Map data = {};
    60. data['messageId'] = const Uuid().v4();
    61. data['message'] = "图片";
    62. data['messageType'] = type;
    63. data['messageTime'] =
    64. TimeUtils.getFormatDataString(DateTime.now(), "yyyy-MM-dd HH:mm:ss");
    65. data['isMe'] = Random.secure().nextBool();
    66. data['fileUrl'] = path;
    67. if(thumbnail.isNotEmpty){
    68. data['thumbnail'] = thumbnail;
    69. }
    70. setState(() {
    71. _messageData.insert(0, data);
    72. });
    73. }
    74. //照片显示组件:
    75. GestureDetector(
    76. onTap: () {
    77. Navigator.push(
    78. context,
    79. PanPageRouteBuilder(
    80. builder: (context) =>
    81. FullImageWidget(imageUrl: data['fileUrl']),
    82. popDirection: AxisDirection.up));
    83. },
    84. child: Container(
    85. clipBehavior: Clip.hardEdge,
    86. width: ScreenAdapter.width(300),
    87. height: ScreenAdapter.height(400),
    88. decoration: const BoxDecoration(
    89. color: Colors.white,
    90. borderRadius: BorderRadius.all(Radius.circular(10))),
    91. child: CachedNetworkImage(
    92. imageUrl: data['fileUrl'],
    93. fit: BoxFit.cover,
    94. )),
    95. );
    96. //视频显示组件
    97. GestureDetector(
    98. onTap: () {
    99. Navigator.push(
    100. context,
    101. PanPageRouteBuilder(
    102. builder: (context) =>
    103. FullVideoWidget(videoUrl: data['fileUrl']),
    104. popDirection: AxisDirection.up));
    105. },
    106. child: Stack(
    107. alignment: Alignment.center,
    108. children: [
    109. Container(
    110. color: Colors.white,
    111. width: ScreenAdapter.width(300),
    112. height: ScreenAdapter.height(400),
    113. child: Image.network(data['thumbnail'],fit: BoxFit.cover,),),
    114. Icon(Icons.play_circle,color: Colors.white,size: ScreenAdapter.size(70),)
    115. ],
    116. )
    117. );

    显示及保存照片组件代码

    1. import 'dart:typed_data';
    2. import 'package:dio/dio.dart';
    3. import 'package:flutter/material.dart';
    4. import 'package:fluttertoast/fluttertoast.dart';
    5. import 'package:image_gallery_saver/image_gallery_saver.dart';
    6. import 'package:new_chat/service/screen_adapter.dart';
    7. import 'package:new_chat/widget/toast_widget.dart';
    8. import 'package:photo_view/photo_view.dart';
    9. class FullImageWidget extends StatelessWidget {
    10. final String imageUrl;
    11. const FullImageWidget({Key? key, required this.imageUrl}) : super(key: key);
    12. //保存照片
    13. _saveImage() async {
    14. var response = await Dio().get(
    15. imageUrl,
    16. options: Options(responseType: ResponseType.bytes));
    17. final result = await ImageGallerySaver.saveImage(
    18. Uint8List.fromList(response.data),
    19. name: "hello");
    20. if(result['isSuccess']){
    21. ToastWidget.showToast("照片保存成功", ToastGravity.CENTER);
    22. }
    23. }
    24. //长摁保存照片组件
    25. _showSaveVideoWidget(BuildContext context) async {
    26. showModalBottomSheet(
    27. context: context,
    28. isDismissible: true,
    29. isScrollControlled: false,
    30. shape: const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15))),
    31. builder: (BuildContext context) {
    32. return Container(
    33. decoration: const BoxDecoration(
    34. color: Colors.white,
    35. borderRadius: BorderRadius.only(topLeft: Radius.circular(15),topRight: Radius.circular(15))
    36. ),
    37. height: ScreenAdapter.height(400),
    38. child: Column(
    39. children: [
    40. Container(padding: EdgeInsets.all(ScreenAdapter.height(40)),child: Text(textAlign:TextAlign.center,"保存照片到相册",maxLines:2,style: TextStyle(color: const Color.fromRGBO(102, 102, 102, 1),fontSize: ScreenAdapter.size(25)),),),
    41. Divider(height: ScreenAdapter.height(0.5)),
    42. InkWell(
    43. child:Padding(padding: EdgeInsets.all(ScreenAdapter.height(18)),child: Center(child: Text("保存照片",style: TextStyle(color: Colors.red,fontSize: ScreenAdapter.size(30)),),),),
    44. onTap: () async{
    45. _saveImage();
    46. Navigator.pop(context);
    47. },
    48. ),
    49. Container(color: const Color.fromRGBO(245, 245, 245,1),height: ScreenAdapter.height(15)),
    50. InkWell(
    51. onTap: (){
    52. Navigator.pop(context);
    53. },
    54. child:Padding(padding: EdgeInsets.fromLTRB(ScreenAdapter.width(0),ScreenAdapter.height(10),ScreenAdapter.width(0),ScreenAdapter.height(15)),child: Text("取消",style: TextStyle(color: const Color.fromRGBO(51, 51, 51, 1),fontSize: ScreenAdapter.size(30)),)),),
    55. ],
    56. ),
    57. );
    58. });
    59. }
    60. @override
    61. Widget build(BuildContext context) {
    62. return Scaffold(
    63. backgroundColor: Colors.black87,
    64. body: GestureDetector(
    65. child: Center(
    66. child: PhotoView(
    67. imageProvider: NetworkImage(imageUrl),
    68. )),
    69. onTap: () {
    70. Navigator.pop(context);
    71. },
    72. //长摁弹出保存照片界面
    73. onLongPress: (){
    74. _showSaveVideoWidget(context);
    75. },
    76. ),
    77. );
    78. }
    79. }

    显示及保存视频组件代码

    1. import 'package:dio/dio.dart';
    2. import 'package:flutter/material.dart';
    3. import 'package:fluttertoast/fluttertoast.dart';
    4. import 'package:image_gallery_saver/image_gallery_saver.dart';
    5. import 'package:new_chat/service/screen_adapter.dart';
    6. import 'package:new_chat/widget/toast_widget.dart';
    7. import 'package:path_provider/path_provider.dart';
    8. import 'package:uuid/uuid.dart';
    9. import 'package:video_player/video_player.dart';
    10. class FullVideoWidget extends StatefulWidget {
    11. final String videoUrl;
    12. const FullVideoWidget({Key? key, required this.videoUrl}) : super(key: key);
    13. @override
    14. State createState() => _FullVideoWidgetState();
    15. }
    16. class _FullVideoWidgetState extends State<FullVideoWidget> {
    17. late VideoPlayerController _controller;
    18. //视频总时长
    19. String videoPlayerEndTime = "";
    20. //视频正在播放的时长
    21. String videoPlayerTime = "";
    22. @override
    23. void initState() {
    24. _controller = VideoPlayerController.network(widget.videoUrl)
    25. ..initialize().then((_) {
    26. setState(() {
    27. _controller.play();
    28. });
    29. });
    30. _controller.addListener(() {
    31. setState(() {
    32. //拼接视频总时长
    33. int endMinutes = _controller.value.duration.inMinutes;
    34. //不足2位补0
    35. var endMinutesPadLeft = endMinutes.toString().padLeft(2,"0");
    36. int endSeconds = _controller.value.duration.inSeconds;
    37. var endSecondsPadLeft = endSeconds.toString().padLeft(2,"0");
    38. videoPlayerEndTime = "$endMinutesPadLeft:$endSecondsPadLeft";
    39. int videoPlayerMinutes = _controller.value.position.inMinutes;
    40. var videoPlayerMinutesPadLeft = videoPlayerMinutes.toString().padLeft(2,"0");
    41. int videoPlayerSeconds = _controller.value.position.inSeconds;
    42. var videoPlayerSecondsPadLeft = videoPlayerSeconds.toString().padLeft(2,"0");
    43. videoPlayerTime = "$videoPlayerMinutesPadLeft:$videoPlayerSecondsPadLeft";
    44. });
    45. });
    46. super.initState();
    47. }
    48. @override
    49. void dispose() {
    50. _controller.dispose();
    51. super.dispose();
    52. }
    53. //保存视频
    54. _saveVideo() async {
    55. var appDocDir = await getTemporaryDirectory();
    56. String savePath = "${appDocDir.path}+${const Uuid().v4()}/temp.mp4";
    57. await Dio().download(widget.videoUrl, savePath);
    58. final result = await ImageGallerySaver.saveFile(savePath);
    59. if(result['isSuccess']){
    60. ToastWidget.showToast("视频保存成功", ToastGravity.CENTER);
    61. }
    62. }
    63. //长摁保存视频组件
    64. _showSaveVideoWidget(BuildContext context) async {
    65. showModalBottomSheet(
    66. context: context,
    67. isDismissible: true,
    68. isScrollControlled: false,
    69. shape: const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15))),
    70. builder: (BuildContext context) {
    71. return Container(
    72. decoration: const BoxDecoration(
    73. color: Colors.white,
    74. borderRadius: BorderRadius.only(topLeft: Radius.circular(15),topRight: Radius.circular(15))
    75. ),
    76. height: ScreenAdapter.height(400),
    77. child: Column(
    78. children: [
    79. Container(padding: EdgeInsets.all(ScreenAdapter.height(40)),child: Text(textAlign:TextAlign.center,"保存视频到相册",maxLines:2,style: TextStyle(color: const Color.fromRGBO(102, 102, 102, 1),fontSize: ScreenAdapter.size(25)),),),
    80. Divider(height: ScreenAdapter.height(0.5)),
    81. InkWell(
    82. child:Padding(padding: EdgeInsets.all(ScreenAdapter.height(18)),child: Center(child: Text("保存视频",style: TextStyle(color: Colors.red,fontSize: ScreenAdapter.size(30)),),),),
    83. onTap: () async{
    84. _saveVideo();
    85. Navigator.pop(context);
    86. },
    87. ),
    88. Container(color: const Color.fromRGBO(245, 245, 245,1),height: ScreenAdapter.height(15)),
    89. InkWell(
    90. onTap: (){
    91. Navigator.pop(context);
    92. },
    93. child:Padding(padding: EdgeInsets.fromLTRB(ScreenAdapter.width(0),ScreenAdapter.height(10),ScreenAdapter.width(0),ScreenAdapter.height(15)),child: Text("取消",style: TextStyle(color: const Color.fromRGBO(51, 51, 51, 1),fontSize: ScreenAdapter.size(30)),)),),
    94. ],
    95. ),
    96. );
    97. });
    98. }
    99. @override
    100. Widget build(BuildContext context) {
    101. ScreenAdapter.init(context);
    102. return Scaffold(
    103. backgroundColor: Colors.black87,
    104. body: GestureDetector(
    105. onLongPress: ()async {
    106. //长摁弹出保存视频界面
    107. await _showSaveVideoWidget(context);
    108. },
    109. child: Stack(
    110. children: [
    111. //视频内容
    112. Align(
    113. child: Container(
    114. child: _controller.value.isInitialized
    115. ? AspectRatio(
    116. aspectRatio: _controller.value.aspectRatio,
    117. child: VideoPlayer(_controller),
    118. )
    119. : Container(),
    120. ),
    121. ),
    122. //播放暂定和播放进度条和视频时间
    123. Container(
    124. margin: EdgeInsets.only(bottom: ScreenAdapter.height(200)),
    125. child: Align(
    126. alignment: Alignment.bottomCenter,
    127. child: Row(
    128. mainAxisAlignment: MainAxisAlignment.start,
    129. children: [
    130. Container(
    131. padding:
    132. EdgeInsets.only(left: ScreenAdapter.width(20)),
    133. child: GestureDetector(
    134. onTap: () {
    135. _controller.value.isPlaying
    136. ? _controller.pause()
    137. : _controller.play();
    138. },
    139. child: Icon(
    140. _controller.value.isPlaying
    141. ? Icons.pause_outlined
    142. : Icons.play_arrow,
    143. color: Colors.white,
    144. size: ScreenAdapter.size(60),
    145. ),
    146. ),
    147. ),
    148. Container(
    149. padding:
    150. EdgeInsets.only(left: ScreenAdapter.width(40)),
    151. child: Text(
    152. videoPlayerTime,
    153. style: const TextStyle(color: Colors.white),
    154. ),
    155. ),
    156. Expanded(
    157. flex: 1,
    158. child: VideoProgressIndicator(
    159. _controller,
    160. allowScrubbing: true,
    161. colors: const VideoProgressColors(
    162. playedColor: Colors.white,
    163. bufferedColor: Colors.white10,
    164. backgroundColor: Colors.black26),
    165. padding: EdgeInsets.fromLTRB(
    166. ScreenAdapter.width(20),
    167. 0,
    168. ScreenAdapter.width(20),
    169. 0),
    170. ),
    171. ),
    172. Container(
    173. padding:
    174. EdgeInsets.only(right: ScreenAdapter.width(50)),
    175. child: Text(
    176. videoPlayerEndTime,
    177. style: const TextStyle(color: Colors.white),
    178. ),
    179. ),
    180. ],
    181. )),
    182. ),
    183. //关闭视频按钮
    184. Align(
    185. alignment: Alignment.bottomLeft,
    186. child:Container(
    187. padding: EdgeInsets.fromLTRB(ScreenAdapter.width(20),0,0,ScreenAdapter.height(100)),
    188. child: GestureDetector(
    189. onTap: (){
    190. Navigator.pop(context);
    191. },
    192. child: Icon(Icons.cancel,color: Colors.white,size: ScreenAdapter.size(60),),
    193. ),),),
    194. //视频保存按钮
    195. Align(
    196. alignment: Alignment.bottomRight,
    197. child:Container(
    198. padding: EdgeInsets.fromLTRB(0,0,ScreenAdapter.width(20),ScreenAdapter.height(100)),
    199. child: GestureDetector(
    200. onTap: () async{
    201. _saveVideo();
    202. },
    203. child: Icon(Icons.download_for_offline,color: Colors.white,size: ScreenAdapter.size(60),),
    204. ),),),
    205. ],
    206. ),)
    207. );
    208. }
    209. }

    视频演示

    语音聊天优化&照片及视频发送和保存到相册配套视频

  • 相关阅读:
    Python基础095:Python读取PDF中的字符
    十八章总结
    SpringCloud与SpringBoot的版本对应
    接雨水-热题 100?-Lua 中文代码解题第4题
    Apache DolphinScheduler新一代分布式工作流任务调度平台实战-上
    NIO学习
    【组成原理-总线】总线的概念和计算
    hystrix断路器
    7_JS关于数据代理_Object.defineProperty_Vue数据代理_双向绑定
    Gemmini测试test文件chisel源码详解(四)
  • 原文地址:https://blog.csdn.net/u013600907/article/details/126605160