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

564 阅读5分钟

目录

用到的组件

ios和android端配置文件

ios

android

主页部分代码

显示及保存照片组件代码

显示及保存视频组件代码

视频演示


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

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

用到的组件

#相册插件
image_picker: ^0.8.5+3
#查看图片组件
photo_view: ^0.14.0
#缓存照片插件
cached_network_image: ^3.2.1
#视频播放
video_player: ^2.4.7
#视频缩略图
video_thumbnail: ^0.5.2
#文件目录获取
path_provider: ^2.0.11
#保存视频、照片到本地相册
image_gallery_saver: ^1.7.1

ios和android端配置文件

ios

在info.plist中添加

<key>NSPhotoLibraryAddUsageDescription</key>
<string>保存图片</string>
<key>NSAppTransportSecurity</key>
  <string>http</string>

android

在AndroidManifest.xml中添加

    <!-- 网络权限 -->
    <uses-permission android:name="android.permission.INTERNET"/>
    <application ...
        android:requestLegacyExternalStorage="true"
    ...
 </application>

主页部分代码

  //获取相册照片并上传
  _getPhotos() async {
    final XFile? pickImage =
        await _picker.pickImage(source: ImageSource.gallery);
    //上传
    var filePath = await _uploadFile(pickImage!, MessageType.photo);
    insertFile(MessageType.photo,filePath,"");
  }

  //拍照并上传
  _takePhotos() async {
    final XFile? pickImage =
        await _picker.pickImage(source: ImageSource.camera);
    //上传
    var filePath = await _uploadFile(pickImage!, MessageType.photo);
    insertFile(MessageType.photo,filePath,"");
  }

  //上传视频
  _getVideo() async {
    final XFile? pickImage = await _picker.pickVideo(source: ImageSource.gallery);
    //获取缩略图文件
    File videoThumbnailFile = await _getVideoThumbnail(pickImage!);
    XFile file = XFile(videoThumbnailFile.path);
    //上传缩略图
    var videoThumbnailFilePath = await _uploadFile(file, MessageType.photo);
    //上传视频
    var videoPath = await _uploadFile(pickImage, MessageType.video);
    insertFile(MessageType.video,videoPath,videoThumbnailFilePath);
  }


//获取视频缩略图
Future<File> _getVideoThumbnail(XFile videoFile) async {
  Uint8List? thumbnail = await VideoThumbnail.thumbnailData(
    video: videoFile.path,
    imageFormat: ImageFormat.JPEG,
    quality: 25,
  );
  var tempDir = await getTemporaryDirectory();
  //生成file文件格式
  String videoThumbnail = '${tempDir.path}/image_${DateTime.now().millisecond}.jpg';
  var file = await File(videoThumbnail).create();
  file.writeAsBytesSync(thumbnail!);
  return file;
}




//上传文件
Future<String> _uploadFile(XFile imageDir, String type) async {
  String filePath = imageDir.path;
  var list = filePath.split(".");
  var last = list.last;
  String fileName = "${const Uuid().v4()}.$last";
  FormData formData = FormData.fromMap({
    "file": await MultipartFile.fromFile(imageDir.path, filename: fileName),
  });
  Dio dio = Dio();
  var response = await dio.post("http://192.168.9.253:8091/sc/file/upload", data: formData);
  var path = response.data["data"]["detail"]["filePath"];
  return path;
}
//写入文件
void insertFile(String type,String path,String thumbnail) {
  Map data = {};
  data['messageId'] = const Uuid().v4();
  data['message'] = "图片";
  data['messageType'] = type;
  data['messageTime'] =
      TimeUtils.getFormatDataString(DateTime.now(), "yyyy-MM-dd HH:mm:ss");
  data['isMe'] = Random.secure().nextBool();
  data['fileUrl'] = path;
  if(thumbnail.isNotEmpty){
    data['thumbnail'] = thumbnail;
  }
  setState(() {
    _messageData.insert(0, data);
  });
}
//照片显示组件:
GestureDetector(
  onTap: () {
    Navigator.push(
        context,
        PanPageRouteBuilder(
            builder: (context) =>
                FullImageWidget(imageUrl: data['fileUrl']),
            popDirection: AxisDirection.up));
  },
  child: Container(
      clipBehavior: Clip.hardEdge,
      width: ScreenAdapter.width(300),
      height: ScreenAdapter.height(400),
      decoration: const BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.all(Radius.circular(10))),
      child: CachedNetworkImage(
        imageUrl: data['fileUrl'],
        fit: BoxFit.cover,
      )),
);
//视频显示组件
GestureDetector(
  onTap: () {
    Navigator.push(
        context,
        PanPageRouteBuilder(
            builder: (context) =>
                FullVideoWidget(videoUrl: data['fileUrl']),
            popDirection: AxisDirection.up));
  },
  child: Stack(
    alignment: Alignment.center,
    children: [
      Container(
          color: Colors.white,
          width: ScreenAdapter.width(300),
          height: ScreenAdapter.height(400),
          child: Image.network(data['thumbnail'],fit: BoxFit.cover,),),
      Icon(Icons.play_circle,color: Colors.white,size: ScreenAdapter.size(70),)
    ],
  )
);

显示及保存照片组件代码

import 'dart:typed_data';

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:new_chat/service/screen_adapter.dart';
import 'package:new_chat/widget/toast_widget.dart';
import 'package:photo_view/photo_view.dart';

class FullImageWidget extends StatelessWidget {
  final String imageUrl;

  const FullImageWidget({Key? key, required this.imageUrl}) : super(key: key);

  //保存照片
  _saveImage() async {
    var response = await Dio().get(
        imageUrl,
        options: Options(responseType: ResponseType.bytes));
    final result = await ImageGallerySaver.saveImage(
        Uint8List.fromList(response.data),
        name: "hello");
    if(result['isSuccess']){
      ToastWidget.showToast("照片保存成功", ToastGravity.CENTER);
    }
  }

  //长摁保存照片组件
  _showSaveVideoWidget(BuildContext context) async {
    showModalBottomSheet(
        context: context,
        isDismissible: true,
        isScrollControlled: false,
        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15))),
        builder: (BuildContext context) {
          return Container(
            decoration: const BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.only(topLeft: Radius.circular(15),topRight: Radius.circular(15))
            ),
            height: ScreenAdapter.height(400),
            child: Column(
              children: [
                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)),),),
                Divider(height: ScreenAdapter.height(0.5)),
                InkWell(
                  child:Padding(padding: EdgeInsets.all(ScreenAdapter.height(18)),child: Center(child: Text("保存照片",style: TextStyle(color: Colors.red,fontSize: ScreenAdapter.size(30)),),),),
                  onTap: () async{
                    _saveImage();
                    Navigator.pop(context);
                  },
                ),
                Container(color: const Color.fromRGBO(245, 245, 245,1),height: ScreenAdapter.height(15)),
                InkWell(
                  onTap: (){
                    Navigator.pop(context);
                  },
                  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)),)),),
              ],
            ),
          );
        });

  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black87,
      body: GestureDetector(
        child: Center(
            child: PhotoView(
          imageProvider: NetworkImage(imageUrl),
        )),
        onTap: () {
          Navigator.pop(context);
        },
        //长摁弹出保存照片界面
        onLongPress: (){
          _showSaveVideoWidget(context);
        },
      ),
    );
  }
}

显示及保存视频组件代码

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:new_chat/service/screen_adapter.dart';
import 'package:new_chat/widget/toast_widget.dart';
import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';
import 'package:video_player/video_player.dart';

class FullVideoWidget extends StatefulWidget {
  final String videoUrl;

  const FullVideoWidget({Key? key, required this.videoUrl}) : super(key: key);

  @override
  State<FullVideoWidget> createState() => _FullVideoWidgetState();
}

class _FullVideoWidgetState extends State<FullVideoWidget> {
  late VideoPlayerController _controller;

  //视频总时长
  String videoPlayerEndTime = "";

  //视频正在播放的时长
  String videoPlayerTime = "";

  @override
  void initState() {
    _controller = VideoPlayerController.network(widget.videoUrl)
      ..initialize().then((_) {
        setState(() {
          _controller.play();
        });
      });
    _controller.addListener(() {
      setState(() {
        //拼接视频总时长
        int endMinutes = _controller.value.duration.inMinutes;
        //不足2位补0
        var endMinutesPadLeft = endMinutes.toString().padLeft(2,"0");
        int endSeconds = _controller.value.duration.inSeconds;
        var endSecondsPadLeft = endSeconds.toString().padLeft(2,"0");
        videoPlayerEndTime = "$endMinutesPadLeft:$endSecondsPadLeft";
        int videoPlayerMinutes = _controller.value.position.inMinutes;
        var videoPlayerMinutesPadLeft = videoPlayerMinutes.toString().padLeft(2,"0");
        int videoPlayerSeconds = _controller.value.position.inSeconds;
        var videoPlayerSecondsPadLeft = videoPlayerSeconds.toString().padLeft(2,"0");
        videoPlayerTime = "$videoPlayerMinutesPadLeft:$videoPlayerSecondsPadLeft";
      });
    });
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  //保存视频
  _saveVideo() async {
    var appDocDir = await getTemporaryDirectory();
    String savePath = "${appDocDir.path}+${const Uuid().v4()}/temp.mp4";
    await Dio().download(widget.videoUrl, savePath);
    final result = await ImageGallerySaver.saveFile(savePath);
    if(result['isSuccess']){
      ToastWidget.showToast("视频保存成功", ToastGravity.CENTER);
    }
  }

  //长摁保存视频组件
  _showSaveVideoWidget(BuildContext context) async {
    showModalBottomSheet(
        context: context,
        isDismissible: true,
        isScrollControlled: false,
        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15))),
        builder: (BuildContext context) {
          return Container(
            decoration: const BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.only(topLeft: Radius.circular(15),topRight: Radius.circular(15))
            ),
            height: ScreenAdapter.height(400),
            child: Column(
              children: [
                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)),),),
                Divider(height: ScreenAdapter.height(0.5)),
                InkWell(
                  child:Padding(padding: EdgeInsets.all(ScreenAdapter.height(18)),child: Center(child: Text("保存视频",style: TextStyle(color: Colors.red,fontSize: ScreenAdapter.size(30)),),),),
                  onTap: () async{
                    _saveVideo();
                    Navigator.pop(context);
                  },
                ),
                Container(color: const Color.fromRGBO(245, 245, 245,1),height: ScreenAdapter.height(15)),
                InkWell(
                  onTap: (){
                    Navigator.pop(context);
                  },
                  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)),)),),
              ],
            ),
          );
        });

  }

  @override
  Widget build(BuildContext context) {
    ScreenAdapter.init(context);
    return Scaffold(
      backgroundColor: Colors.black87,
      body: GestureDetector(
        onLongPress: ()async {
          //长摁弹出保存视频界面
          await _showSaveVideoWidget(context);
        },
        child:  Stack(
        children: [
          //视频内容
          Align(
            child: Container(
              child: _controller.value.isInitialized
                  ? AspectRatio(
                aspectRatio: _controller.value.aspectRatio,
                child: VideoPlayer(_controller),
              )
                  : Container(),
            ),
          ),
          //播放暂定和播放进度条和视频时间
          Container(
            margin: EdgeInsets.only(bottom: ScreenAdapter.height(200)),
            child: Align(
                alignment: Alignment.bottomCenter,
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.start,
                  children: [
                    Container(
                      padding:
                      EdgeInsets.only(left: ScreenAdapter.width(20)),
                      child: GestureDetector(
                        onTap: () {
                          _controller.value.isPlaying
                              ? _controller.pause()
                              : _controller.play();
                        },
                        child: Icon(
                          _controller.value.isPlaying
                              ? Icons.pause_outlined
                              : Icons.play_arrow,
                          color: Colors.white,
                          size: ScreenAdapter.size(60),
                        ),
                      ),
                    ),
                    Container(
                      padding:
                      EdgeInsets.only(left: ScreenAdapter.width(40)),
                      child: Text(
                        videoPlayerTime,
                        style: const TextStyle(color: Colors.white),
                      ),
                    ),
                    Expanded(
                      flex: 1,
                      child: VideoProgressIndicator(
                        _controller,
                        allowScrubbing: true,
                        colors: const VideoProgressColors(
                            playedColor: Colors.white,
                            bufferedColor: Colors.white10,
                            backgroundColor: Colors.black26),
                        padding: EdgeInsets.fromLTRB(
                            ScreenAdapter.width(20),
                            0,
                            ScreenAdapter.width(20),
                            0),
                      ),
                    ),
                    Container(
                      padding:
                      EdgeInsets.only(right: ScreenAdapter.width(50)),
                      child: Text(
                        videoPlayerEndTime,
                        style: const TextStyle(color: Colors.white),
                      ),
                    ),
                  ],
                )),
          ),
          //关闭视频按钮
          Align(
            alignment: Alignment.bottomLeft,
            child:Container(
              padding: EdgeInsets.fromLTRB(ScreenAdapter.width(20),0,0,ScreenAdapter.height(100)),
              child:  GestureDetector(
                onTap: (){
                  Navigator.pop(context);
                },
                child: Icon(Icons.cancel,color: Colors.white,size: ScreenAdapter.size(60),),
              ),),),
          //视频保存按钮
          Align(
            alignment: Alignment.bottomRight,
            child:Container(
              padding: EdgeInsets.fromLTRB(0,0,ScreenAdapter.width(20),ScreenAdapter.height(100)),
              child:  GestureDetector(
                onTap: () async{
                  _saveVideo();
                },
                child: Icon(Icons.download_for_offline,color: Colors.white,size: ScreenAdapter.size(60),),
              ),),),
        ],
      ),)
    );
  }
}

视频演示

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

本文转自 blog.csdn.net/u013600907/…,如有侵权,请联系删除。