Picture & video upload, display and save to photo album of Flutter chat layout

Table of contents

components used

ios and android configuration files

ios

android

home page section code

Display and save photo component code

Display and save video component code

video demo


Continued from the Flutter simple chat interface layout and voice recording and playback - Programmer Sought

This article mainly develops the simple development of uploading, displaying and saving pictures and videos in the chat layout to the photo album.

components used

#相册插件
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 and android configuration files

ios

Add in info.plist

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

android

Add in AndroidManifest.xml

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

home page section code

  //获取相册照片并上传
  _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),)
    ],
  )
);

Display and save photo component code

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);
        },
      ),
    );
  }
}

Display and save video component code

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),),
              ),),),
        ],
      ),)
    );
  }
}

video demo

Voice chat optimization & photo and video sending and saving to album supporting video

Guess you like

Origin blog.csdn.net/u013600907/article/details/126605160