Flutter开发笔记 —— 仿Telegram图片机制组件开发的简单实现

前言

最近在找项目demo练手,刚好看到Tg上图片组件这个小组件挺好玩的,顺手写了一下,希望对你有帮助

需求分析

效果图

我们从上图可以分析下需求

  • 底图一张
  • 高斯模糊
  • 断片下载 & 中断处理
  • 图片下载状态 & 中断状态相关loadding

接下来我们开始实现相关功能

功能实现

基础部分

底图和高斯模糊的UI比较好做,而且这里的CustomLoadding组件也是自己写的。

Container(
              margin: EdgeInsets.only(left: MediaQuery.of(context).size.width / 6),
              width: 300,
              height: 300,
              child: Stack(
                fit: StackFit.expand,
                children: [
                  Container(
                    child: Stack(
                      fit: StackFit.expand,
                      children: [
                        //底图
                        Container(
                          child: Image.network("http://175.24.177.189/mcAuthor/portrait2.jpg"),
                        ),
                        // 使用BackdropFilter实现模糊效果
                        Center(
                          child: ClipRect(
                            child: BackdropFilter(
                              filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
                              child: Container(
                                width: 300,
                                height: 300,
                                decoration: BoxDecoration(
                                  color: Colors.white.withOpacity(0.5),
                                  borderRadius: BorderRadius.circular(10.0),
                                ),
                              ),
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                  Center(
                    child: Container(
                      width: 65,
                      height: 65,
                      alignment: Alignment.center,
                      child: Stack(
                        fit: StackFit.expand,
                        children: [
                          Container(
                            decoration: BoxDecoration(
                              color: Colors.black.withOpacity(.3),
                              borderRadius: BorderRadius.circular(1000)
                            ),
                          ),
                          Center(
                            child: Stack(
                              children: [
                                Positioned(
                                  left: 10,
                                  top: 8 ,
                                  right: 10,
                                  child: Container(
                                    child: InkWell(
                                      onTap: (){
                                        if(!startFlag){
                                          startDownload();
                                        }else{
                                          stopDownload();
                                        }
                                      },
                                      child: SizedBox(
                                        width: startFlag ? 30 : 40,
                                        height: startFlag ? 30 : 40,
                                        child: Image.asset(startFlag ? "assets/exit.png" : "assets/xiazai.png",color: Colors.white,fit: BoxFit.fill,),
                                      ),
                                    ),
                                  ),
                                ),
                                IgnorePointer(
                                  child: AnimatedBuilder(
                                    animation: controller,
                                    builder: (BuildContext context, Widget? child) {
                                      return Transform.rotate(
                                        angle: rotateValue.value,
                                        child: Container(
                                          decoration: BoxDecoration(
                                            borderRadius: BorderRadius.circular(1000)
                                          ),
                                          width: 45,
                                          height: 45,
                                          child: Stack(
                                            children: [
                                              Container(
                                                width: 7,
                                                height: 7,
                                                decoration: BoxDecoration(
                                                    color: Colors.white,
                                                    borderRadius: BorderRadius.all(Radius.circular(20))
                                                ),
                                              ),
                                            ],
                                          ),
                                        ),
                                      );
                                    },
                                  ),
                                ),
                              ],
                            ),
                          )
                        ],
                      ) ,
                    ),
                  ),
                ],
              ),
            ),

基础UI到此完毕,接下来着重讲下核心功能 断片下载的实现

断片下载

网络请求库选择:Dio(https://pub.dev/packages/dio)

定义接口

我们接下来定义几个相关方法,封装成接口

import 'package:flutter/material.dart';
import 'package:dio/dio.dart';

typedef ProgressCallBack = void Function(int count, int total);
typedef CancelTokenProvider = void Function(CancelToken cancelToken);

abstract class AbstractDownload  {

  //下载资源
  void downloadResource(
      String url,
      {
        ProgressCallBack? progressCallBack,
        CancelTokenProvider? cancelTokenProvider,
        Function(String)? done,
        Function(String)? error
      });

  //中断下载
  void cancelDownload(CancelToken cancelToken);

  //获取目标资源已下载的大小
  Future<double> getCacheSize(String url);

  //清理缓存
  Future<void> clearCache(String url);
}

以上我们定义好相关方法后,我们不着急定义实现类,我们先封装下请求类,方便后续实现

在封装请求类之前,我们先定义一个路径的处理相关类,用来定义我们后续的文件下载缓存路径

路径处理

这个没什么多讲的,插件引用(https://pub.dev/packages/path_provider)

import 'dart:io';

import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
class FilePathProvider{

  Future<Directory> _getBaseDirectory() async{
    return await getApplicationDocumentsDirectory();
  }


  /*
   * @author Marinda
   * @date 2024/7/18 16:20
   * @description 获取默认缓存地址
   */
  Future<String> getImageCacheDir({String? subDir}) async{
    var baseDir = await _getBaseDirectory();
    String path = p.join(baseDir.path,subDir ?? "image");
    Directory directory = Directory(path);
    if(!directory.existsSync()){
      directory.createSync();
    }
    return path;
  }

  /*
   * @author Marinda
   * @date 2024/7/19 14:35
   * @description 构建一个缓存地址
   */
  String buildCachePath(String dir,String name){
    return p.join(dir,name);
  }


}

接下来我们再封装个工具类,用来缓存网络请求的CanelToken 以及url的处理

工具类

import 'package:dio/dio.dart';

class HttpUtil {
static Map<String,CancelToken> cancelTokenMap = {};

/*
* @author Marinda
* @date 2024/7/19 14:28
* @description 根据url获取文件名
*/
static String getUrlFileName(String url){
String fileName = url.substring(url.lastIndexOf("/")+1,url.lastIndexOf("."));
String suffix = url.substring(url.lastIndexOf(".")+1);
return "$fileName.$suffix";
}
}

封装请求

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';

import 'package:dio/dio.dart';
import 'package:flutter_demo_test/common/path_provider.dart';
import 'package:flutter_demo_test/common/util/http_util.dart';
import 'package:uuid/uuid.dart';
import 'package:path/path.dart' as p;
class Request {

  /// 用于记录正在下载的url,避免重复下载
  static final baseOption = BaseOptions(
    connectTimeout: Duration(seconds: 5),
    receiveTimeout: Duration(seconds: 5)
  );
  static Dio _dio = Dio();
  /*
   * @author Marinda
   * @date 2024/7/18 16:06
   * @description 请求
   */
  static Future request(String url,{String? savePath,String? fileName,String? method,String? header,dynamic data,ProgressCallback? onReceiverProgress,void Function(String resultPath)? done,void Function(DioException)? failed}) async {
    _dio = Dio(baseOption);
    int downloadStart = 0;
    FilePathProvider pathProvider = FilePathProvider();
    String cacheDir = await pathProvider.getImageCacheDir();
    CancelToken cancelToken = CancelToken();
    HttpUtil.cancelTokenMap[url] = cancelToken;
    String targetDir = savePath ?? cacheDir;
    String targetName = fileName ?? pathProvider.buildCachePath(cacheDir, HttpUtil.getUrlFileName(url));
    String targetPath = p.join(targetDir, targetName);

    //断片下载实现
    int downloadStart = 0;
    print(targetPath);
    File file = File(targetPath);
    if (file.existsSync()) {
      downloadStart = file.lengthSync();
    }
    print("start Download: ${downloadStart}");
    try {
      var response = await _dio.get<ResponseBody>(url, options: Options(
          responseType: ResponseType.stream,
          followRedirects: false,
          headers: {
            "range": "bytes=$downloadStart-",
          }
      ));

      RandomAccessFile raf = file.openSync(mode: FileMode.append);
      int receiver = downloadStart;
      int total = await _getContentLength(response);
      Stream<Uint8List> stream = response.data!.stream;
      StreamSubscription<Uint8List>? subscription;
      subscription = stream.listen((data) {
        raf.writeFromSync(data);
        receiver+=data.length;
        print('当前receiver: ${receiver},总大小: ${total / (1024 * 1024)}M,进度:${(receiver / total) * 100}%');
        onReceiverProgress?.call(receiver,total);
      },
        onDone: () async{
        print("地址:$targetPath");
        HttpUtil.cancelTokenMap.remove(url);
          await raf.close();
          done?.call(targetPath);
        },
        onError: (e) async{
          if(CancelToken.isCancel(e)){
            print("下载异常!");
          }else{
            failed?.call(e);
          }
          HttpUtil.cancelTokenMap.remove(url);
        }
      );
    cancelToken.whenCancel.then((_) async{
      await subscription?.cancel();
      await raf.close();
    });
    } catch (e) {
      print(e);
    }
  }


  static Future<int> _getContentLength(Response<ResponseBody> response) async{
    try{
      var headerContent = response.headers.value(HttpHeaders.contentRangeHeader);
      print("下载文件$headerContent");
      if(headerContent != null){
        return int.parse(headerContent.split("/").last);
      }else{
        return 0;
      }
    }catch(e){
      return 0;
    }
  }
}

以上代码你可能会看的略微有点迷糊,我们逐步分析下断片下载的实现逻辑

首先定义一个用来缓存当前已下载的总长度变量

    int downloadStart = 0;

接下来我们校验下这个缓存地址中是否存在相关文件,如果有则缓存文件当前总大小

    File file = File(targetPath);
    if (file.existsSync()) {
      downloadStart = file.lengthSync();
    }

之后我们使用Dio中的get方法,并且添加相关配置

      var response = await _dio.get<ResponseBody>(url, options: Options(
          responseType: ResponseType.stream,
          followRedirects: false,
          headers: {
            "range": "bytes=$downloadStart-",
          }
      ));

responseType为响应类型,正常为json,但是我们这里要做短片,所以改为Stream模式

followRedirects为是否重定向,我们为false

重点在headers,我们需要制定一个range属性,并且参数填写bytes=$downloadStart-

响应处理好了。接下来就是对响应流的处理


      RandomAccessFile raf = file.openSync(mode: FileMode.append);
      int receiver = downloadStart;
      int total = await _getContentLength(response);
      Stream<Uint8List> stream = response.data!.stream;
      StreamSubscription<Uint8List>? subscription;
      subscription = stream.listen((data) {
        raf.writeFromSync(data);
        receiver+=data.length;
        print('当前receiver: ${receiver},总大小: ${total / (1024 * 1024)}M,进度:${(receiver / total) * 100}%');
        onReceiverProgress?.call(receiver,total);
      },
        onDone: () async{
        print("地址:$targetPath");
        HttpUtil.cancelTokenMap.remove(url);
          await raf.close();
          done?.call(targetPath);
        },
        onError: (e) async{
          if(CancelToken.isCancel(e)){
            print("下载异常!");
          }else{
            failed?.call(e);
          }
          HttpUtil.cancelTokenMap.remove(url);
        }
      );
    cancelToken.whenCancel.then((_) async{
      await subscription?.cancel();
      await raf.close();
    });
    } catch (e) {
      print(e);
    }

RandomAccessFile 定义用来对文件流的追加

receiver 和 total 变量用来配置当前range长度 以及总长度

total总量大小可以通过以下方法获取,实际也是通过响应头获取总量,这里不过多讲解,看代码

  static Future<int> _getContentLength(Response<ResponseBody> response) async{
    try{
      var headerContent = response.headers.value(HttpHeaders.contentRangeHeader);
      print("下载文件$headerContent");
      if(headerContent != null){
        return int.parse(headerContent.split("/").last);
      }else{
        return 0;
      }
    }catch(e){
      return 0;
    }
  }

接下来就是对于Stream的监听处理,也就是切片逻辑核心

Stream<Uint8List> stream = response.data!.stream;
      StreamSubscription<Uint8List>? subscription;
      subscription = stream.listen((data) {
        raf.writeFromSync(data);
        receiver+=data.length;
      RandomAccessFile raf = file.openSync(mode: FileMode.append);
      int receiver = downloadStart;
      int total = await _getContentLength(response);
      Stream<Uint8List> stream = response.data!.stream;
      StreamSubscription<Uint8List>? subscription;
      subscription = stream.listen((data) {
        raf.writeFromSync(data);
        receiver+=data.length;
        print('当前receiver: ${receiver},总大小: ${total / (1024 * 1024)}M,进度:${(receiver / total) * 100}%');
        onReceiverProgress?.call(receiver,total);
      },
        onDone: () async{
        print("地址:$targetPath");
        HttpUtil.cancelTokenMap.remove(url);
          await raf.close();
          done?.call(targetPath);
        },
        onError: (e) async{
          if(CancelToken.isCancel(e)){
            print("下载异常!");
          }else{
            failed?.call(e);
          }
          HttpUtil.cancelTokenMap.remove(url);
        }
      );
    cancelToken.whenCancel.then((_) async{
      await subscription?.cancel();
      await raf.close();
    });

这里简单讲讲,逻辑很清楚,就是监听流并追加写入文件中,之后相加每次切片长度,并添加相关回调,cancelToken.whenCancel这里是用来做中断处理。

至此短片下载逻辑完毕。

实现接口

我们定义接口DownloadImpl,相关代码如下

import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter_demo_test/common/path_provider.dart';
import 'package:flutter_demo_test/common/util/http_util.dart';
import 'package:flutter_demo_test/download/abstract_download.dart';
import 'package:flutter_demo_test/download/request.dart';

class DownloadImpl implements AbstractDownload{
  FilePathProvider filePathProvider = FilePathProvider();

  DownloadImpl();

  @override
  void downloadResource(String url, {ProgressCallBack? progressCallBack, CancelTokenProvider? cancelTokenProvider, Function(String p1)? done, Function(String p1)? error}) async{
    String dir = await filePathProvider.getImageCacheDir();
    await Request.request(url,savePath: dir,onReceiverProgress: progressCallBack,done: (str){
      done?.call(str);
    },failed: (e){
      print(e);
      error?.call(e.error.toString());
    });
    // return
  }

  @override
  void cancelDownload(CancelToken cancelToken) {
    //如果存在则中断
    if(HttpUtil.cancelTokenMap.containsValue(cancelToken)){
      cancelToken.cancel();
      String key = "";
      for(var element in HttpUtil.cancelTokenMap.keys){
        var token = HttpUtil.cancelTokenMap[element];
        if(token == cancelToken){
          key = element;
          break;
        }
      }
      HttpUtil.cancelTokenMap[key]!.cancel();
      print("中断下载${key}任务成功!");
    }else{
      print("未查询到该任务!");
    }
  }

  @override
  Future<void> clearCache(String url) async{
    String fileName = HttpUtil.getUrlFileName(url);
    String dir = await filePathProvider.getImageCacheDir();
    File file = File(filePathProvider.buildCachePath(dir, fileName));
    if(!file.existsSync()){
      print("该文件不存在!");
      return;
    }
    file.deleteSync();
    print("删除完毕");
  }

  @override
  Future<double> getCacheSize(String url) async{
    String fileName = HttpUtil.getUrlFileName(url);
    String dir = await filePathProvider.getImageCacheDir();
    File file = File(filePathProvider.buildCachePath(dir, fileName));
    return file.lengthSync() / (1024 * 1024);
  }

}

这里实现方法都很简单,稍微看下就理解了,所以不做过多介绍

目前我们已经处理了接口封装和实现,以及相关请求类的封装完毕,接下来就是对我们UI进行下调整即可

视图更新

我们接下来在视图定义以下几个变量

  //下载进度文本
  String downloadProgressText = "";
  //下载结束标识
  bool downloadFlag = false;
  //开始下载标识
  bool startFlag = false;
  //下载实现类
  DownloadImpl downloadImpl = DownloadImpl();

再定义接口回调 以及相关下载和中断方法

  downloadDone(str){
    setState(() {
      donePath = str;
      downloadFlag = true;
    });
  }

  onReceiverProgress(receiver,total){
    downloadProgressText = "${double.parse(((receiver / total) * 100).toString()).toStringAsFixed(2).toString()}%";
  }

  //开始下载
  startDownload(){
    downloadImpl.downloadResource(url,progressCallBack: onReceiverProgress,done: downloadDone);
    setState(() {
      startFlag = true;
    });
  }

  stopDownload(){
    downloadImpl.cancelDownload(HttpUtil.cancelTokenMap[url]!);
    setState(() {
      startFlag = false;
    });
  }

目前我们万事俱备只欠东风啦,我们调整下视图UI功能就完成啦

    return Scaffold(
      appBar: AppBar(
        title: Text("图像实例"),
      ),
      body: Container(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Container(
              margin: EdgeInsets.only(left: MediaQuery.of(context).size.width / 6),
              width: 300,
              height: 300,
              child: Stack(
                fit: StackFit.expand,
                children: [
                  Container(
                    child: Stack(
                      fit: StackFit.expand,
                      children: [
                        //底图
                        Container(
                          child: Image.network("http://175.24.177.189/mcAuthor/portrait2.jpg"),
                        ),
                        // 使用BackdropFilter实现模糊效果
                        if(!downloadFlag)
                        Center(
                          child: ClipRect(
                            child: BackdropFilter(
                              filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
                              child: Container(
                                width: 300,
                                height: 300,
                                decoration: BoxDecoration(
                                  color: Colors.white.withOpacity(0.5),
                                  borderRadius: BorderRadius.circular(10.0),
                                ),
                              ),
                            ),
                          ),
                        ),
                        //下载进度
                        Positioned(
                          right: 10,
                            top: 5,
                            child: Container(
                              child: !downloadFlag ? Text.rich(
                                TextSpan(
                                  children: [
                                    if(startFlag)
                                    TextSpan(
                                        text: "下载进度:",
                                      style: TextStyle(
                                        color: Colors.black,
                                        fontSize: 16
                                      ),
                                    ),
                                    TextSpan(
                                      text: "$downloadProgressText",
                                      style: TextStyle(
                                          color: Colors.red,
                                          fontSize: 18
                                      )
                                    )
                                  ]
                                )
                              ) : Text(""),
                        ))
                      ],
                    ),
                  ),
                  // if(!downloadFlag)
                  Center(
                    child: Container(
                      width: 65,
                      height: 65,
                      alignment: Alignment.center,
                      child: Stack(
                        fit: StackFit.expand,
                        children: [
                          Container(
                            // width: 50,
                            // height: 50,
                            decoration: BoxDecoration(
                              color: Colors.black.withOpacity(.3),
                              borderRadius: BorderRadius.circular(1000)
                            ),
                          ),
                          Center(
                            child: Stack(
                              children: [
                                Positioned(
                                  left: 10,
                                  top: 8 ,
                                  right: 10,
                                  child: Container(
                                    child: InkWell(
                                      onTap: (){
                                        if(!startFlag){
                                          startDownload();
                                        }else{
                                          stopDownload();
                                        }
                                      },
                                      child: SizedBox(
                                        width: startFlag ? 30 : 40,
                                        height: startFlag ? 30 : 40,
                                        child: Image.asset(startFlag ? "assets/exit.png" : "assets/xiazai.png",color: Colors.white,fit: BoxFit.fill,),
                                      ),
                                    ),
                                  ),
                                ),
                                // if(startFlag)
                                IgnorePointer(
                                  child: AnimatedBuilder(
                                    animation: controller,
                                    builder: (BuildContext context, Widget? child) {
                                      return Transform.rotate(
                                        angle: rotateValue.value,
                                        child: Container(
                                          decoration: BoxDecoration(
                                            borderRadius: BorderRadius.circular(1000)
                                          ),
                                          width: 45,
                                          height: 45,
                                          child: Stack(
                                            children: [
                                              Container(
                                                width: 7,
                                                height: 7,
                                                decoration: BoxDecoration(
                                                    color: Colors.white,
                                                    borderRadius: BorderRadius.all(Radius.circular(20))
                                                ),
                                              ),
                                            ],
                                          ),
                                        ),
                                      );
                                    },
                                  ),
                                ),
                              ],
                            ),
                          )
                        ],
                      ) ,
                    ),
                  ),
                ],
              ),
            ),

          ],
        ),
      ),
    );

我们一起来看看最终效果

结束语

感谢你的观看!

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇