flutter轮子计划之进度条

前言

本文的记录如何用CustomPaint、GestureDetector实现一个进度条控件。首先需要说明的是 flutter Material 组件库中提供了两种进度指示器:LinearProgressIndicator和CircularProgressIndicator。如果这两种进度指示器可以满足开发需求,就不要尝试自己造轮子了。本文实现的进度条控件,功能如下:

  • 进度的范围为0到1的double类型数据
  • 支持拖动,通过回调函数获取进度值
  • 支持跳转,点击某个位置后进度跳转,回调进度值
  • 样式为Material风格的样式,可以根据需要修改

识别拖动手势

使用GestureDetector可以方便得对滑动,点击事件进行监听。如下是监听的四个事件,重点关注onHorizontalDragUpdate即可,其回调函数将水平拖动事件的坐标等信息传递给_seekToRelativePosition函数。_seekToRelativePosition函数的功能是计算滑动时进度条的值,并更新界面。代码如下:

GestureDetector(
        onHorizontalDragStart: (DragStartDetails details) {
          widget.onDragStart?.call();
        },
        onHorizontalDragUpdate: (DragUpdateDetails details) {
          widget.onDragUpdate?.call();
          _seekToRelativePosition(details.globalPosition);
        },
        onHorizontalDragEnd: (DragEndDetails details) {
          widget.onDragEnd?.call(progress);
        },
        onTapDown: (TapDownDetails details) {
          widget.onTapDown?.call(progress);
          _seekToRelativePosition(details.globalPosition);
        },

        // ....

)

_seekToRelativePosition 将全局坐标转换为进度条控件所在的举动坐标。将点击处的横坐标,即x与进度条控件的长度的比率作为进度条的值。然后调用setState()更新界面。上面

void _seekToRelativePosition(Offset globalPosition) {
    final box = context.findRenderObject()! as RenderBox;
    final Offset tapPos = box.globalToLocal(globalPosition);
    progress = tapPos.dx / box.size.width;
    if (progress < 0) progress = 0;
    if (progress > 1) progress = 1;

    setState(() {
        widget.controller.progressBarValue = progress;
    });
  } 

上面代码中有一个controller控件,其定义如下:

class VideoProgressBarController extends ChangeNotifier
{
  double progressBarValue = .0;

   updateProgressValue(double value){
     progressBarValue = value;
    notifyListeners();
  }
}

其继承自ChangeNotifier, 因为此进度条控件的状态由其他控件和控件本身混合管理状态。当其他控件想改变进度条的值时,可以通过VidoeProgressBarController通知进度条控件更新界面。当然,将进度条控件改用statelesswidget实现,然后直接调用setState()更新界面实现起来会更简单一点,读者有需要可以尝试。

使用CustomPaint绘制进度条

绘制部分比较简单。如下,先绘制灰色背景,然后绘制红色的进度,再回事圆点。

class _VideoProgressBarPainter extends CustomPainter {
  _VideoProgressBarPainter(
      {required this.barHeight,
      required this.handleHeight,
      required this.value,
      required this.colors});

  final double barHeight;
  final double handleHeight;
  final ProgressColors colors;
  final double value;

  @override
  bool shouldRepaint(CustomPainter painter) {
    return true;
  }

  @override
  void paint(Canvas canvas, Size size) {
    final baseOffset = size.height / 2 - barHeight / 2;
    final double radius = 4.0;

    canvas.drawRRect(
      RRect.fromRectAndRadius(
        Rect.fromPoints(
          Offset(0.0, baseOffset),
          Offset(size.width, baseOffset + barHeight),
        ),
        const Radius.circular(4.0),
      ),
      colors.backgroundPaint,
    );

    double playedPart =
        value > 1 ? size.width - radius : value * size.width - radius;
    if (playedPart < radius) {
      playedPart = radius;
    }

    canvas.drawRRect(
      RRect.fromRectAndRadius(
        Rect.fromPoints(
          Offset(0.0, baseOffset),
          Offset(playedPart, baseOffset + barHeight),
        ),
        Radius.circular(radius),
      ),
      colors.playedPaint,
    );

    canvas.drawCircle(
      Offset(playedPart, baseOffset + barHeight / 2),
      handleHeight,
      colors.playedPaint,
    );
  }
}

完整代码:

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  double _progressValue = .5;
  late VideoProgressBarController controller;

  @override
  void initState() {
    controller = VideoProgressBarController();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    print("build:$_progressValue");
    return SafeArea(
      child: Scaffold(
        appBar: AppBar(title: Text("test")),
        body: Column(
            //aspectRatio: 16 / 9,
            children: [
              Container(
                width: 200,
                height: 26,
                //color: Colors.blue,
                child: VideoProgressBar(
                  controller: controller,
                  barHeight: 2,
                  onDragEnd: (double progress) {
                    print("$progress");
                  },
                ),
              ),
              Text("value:$_progressValue"),
              ElevatedButton(
                  onPressed: (){
                      _progressValue = 1;
                      controller.updateProgressValue(_progressValue);
                  },
                  child: Text("increase")
              )
            ]
        ),
      ),
    );
  }
}

/// progress bar
class VideoProgressBar extends StatefulWidget {
  VideoProgressBar({
    ProgressColors? colors,
    Key? key,
    required this.controller,
    required this.barHeight,
    this.handleHeight = 6,
    this.onDragStart,
    this.onDragEnd,
    this.onDragUpdate,
    this.onTapDown,
  })  : colors = colors ?? ProgressColors(),
        super(key: key);

  final ProgressColors colors;
  final Function()? onDragStart;
  final Function(double progress)? onDragEnd;
  final Function()? onDragUpdate;
  final Function(double progress)? onTapDown;

  final double barHeight;
  final double handleHeight;

  final TVideoProgressBarController controller;

  //final bool drawShadow;

  @override
  _VideoProgressBarState createState() => _VideoProgressBarState();
}

class _VideoProgressBarState extends State<VideoProgressBar> {
  double progress = .0;

  @override
  void initState() {
    super.initState();
    progress = widget.controller.progressBarValue;
    widget.controller.addListener(_updateProgressValue);
  }

  @override
  void dispose() {
    widget.controller.removeListener(_updateProgressValue);
    super.dispose();
  }

  _updateProgressValue()
  {
    setState(() {
      progress = widget.controller.progressBarValue;
    });
  }

  void _seekToRelativePosition(Offset globalPosition) {
    final box = context.findRenderObject()! as RenderBox;
    final Offset tapPos = box.globalToLocal(globalPosition);
    progress = tapPos.dx / box.size.width;
    if (progress < 0) progress = 0;
    if (progress > 1) progress = 1;

    setState(() {
        widget.controller.progressBarValue = progress;
    });
  }

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;

    return GestureDetector(
        onHorizontalDragStart: (DragStartDetails details) {
          widget.onDragStart?.call();
        },
        onHorizontalDragUpdate: (DragUpdateDetails details) {
          widget.onDragUpdate?.call();
          _seekToRelativePosition(details.globalPosition);
        },
        onHorizontalDragEnd: (DragEndDetails details) {
          widget.onDragEnd?.call(progress);
        },
        onTapDown: (TapDownDetails details) {
          widget.onTapDown?.call(progress);
          _seekToRelativePosition(details.globalPosition);
        },
        child: Center(
          child: Container(
            height: MediaQuery.of(context).size.height,
            width: MediaQuery.of(context).size.width,
            child: CustomPaint(
                painter: _VideoProgressBarPainter(
                    barHeight: widget.barHeight,
                    handleHeight: widget.handleHeight,
                    colors: widget.colors,
                    value: progress)),
          ),
        ));
  }
}

class _VideoProgressBarPainter extends CustomPainter {
  _VideoProgressBarPainter(
      {required this.barHeight,
      required this.handleHeight,
      required this.value,
      required this.colors});

  final double barHeight;
  final double handleHeight;
  final ProgressColors colors;
  final double value;

  @override
  bool shouldRepaint(CustomPainter painter) {
    return true;
  }

  @override
  void paint(Canvas canvas, Size size) {
    final baseOffset = size.height / 2 - barHeight / 2;
    final double radius = 4.0;

    canvas.drawRRect(
      RRect.fromRectAndRadius(
        Rect.fromPoints(
          Offset(0.0, baseOffset),
          Offset(size.width, baseOffset + barHeight),
        ),
        const Radius.circular(4.0),
      ),
      colors.backgroundPaint,
    );

    double playedPart =
        value > 1 ? size.width - radius : value * size.width - radius;
    if (playedPart < radius) {
      playedPart = radius;
    }

    canvas.drawRRect(
      RRect.fromRectAndRadius(
        Rect.fromPoints(
          Offset(0.0, baseOffset),
          Offset(playedPart, baseOffset + barHeight),
        ),
        Radius.circular(radius),
      ),
      colors.playedPaint,
    );

    canvas.drawCircle(
      Offset(playedPart, baseOffset + barHeight / 2),
      handleHeight,
      colors.playedPaint,
    );
  }
}

class VideoProgressBarController extends ChangeNotifier
{
  double progressBarValue = .0;

   updateProgressValue(double value){
     progressBarValue = value;
    notifyListeners();
  }
}

class ProgressColors {
  ProgressColors({
    Color playedColor = const Color.fromRGBO(255, 0, 0, 0.7),
    Color bufferedColor = const Color.fromRGBO(30, 30, 200, 0.2),
    Color handleColor = const Color.fromRGBO(200, 200, 200, 1.0),
    Color backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5),
  })  : playedPaint = Paint()..color = playedColor,
        bufferedPaint = Paint()..color = bufferedColor,
        handlePaint = Paint()..color = handleColor,
        backgroundPaint = Paint()..color = backgroundColor;

  final Paint playedPaint;
  final Paint bufferedPaint;
  final Paint handlePaint;
  final Paint backgroundPaint;
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Flutter 实现进度条效果

    用flutter开发的项目,最大的好处除了跨平台之外,还有一点就是插件多,便于很多功能的实现. 画风(话锋)一转,老子说,事物都有两面性,物极必反.插件多是多,插件带来的问题也是不容小觑,总结下,插件带来的几大问题. (1)插件更新不及时 插件更新不及时,会导致我们连编译都过不去,甚至要去改插件的原生代码,这个时候,我们可能会自己创建一个github插件,然后直接引用自己的github插件地址,算了,真不靠谱的插件作者!千言万语尽在不言中... (2)插件冲突 插件冲突,有的时候是配置冲突,比如

  • Flutter质感设计之进度条

    LinearProgressIndicator控件是质感设计中的线性进度指示器,具体内容如下 import 'package:flutter/material.dart'; class ActionViewEcology extends StatelessWidget { /* * 构建函数,传递参数 * 最大能量值 * 最大饥饿值 * 最大情绪值 * 当前能量值 * 当前饥饿值 * 当前情绪值 */ ActionViewEcology({ this.maximumEmergy, this.ma

  • flutter轮子计划之进度条

    前言 本文的记录如何用CustomPaint.GestureDetector实现一个进度条控件.首先需要说明的是 flutter Material 组件库中提供了两种进度指示器:LinearProgressIndicator和CircularProgressIndicator.如果这两种进度指示器可以满足开发需求,就不要尝试自己造轮子了.本文实现的进度条控件,功能如下: 进度的范围为0到1的double类型数据 支持拖动,通过回调函数获取进度值 支持跳转,点击某个位置后进度跳转,回调进度值 样式

  • Struts2 文件上传进度条的实现实例代码

    最近在写我们大三项目的一个视频文件上传的页面,实现后台对上传的进度进行监听,然后将监听的信息返回给前台页面. 前台的页面效果图: 前台进度条控件选择使用easyui 的progressbar控件. 详细的使用说明参考官网文档:http://www.jeasyui.com/documentation/index.php 所有需要引入jquery-1.11.1.min.js 以及jquery.easyui.min.js 一.前台的代码: <%@ page language="java"

  • iOS中使用NSProgress类来创建UI进度条的方法详解

    一.引言 在iOS7之前,系统一直没有提供一个完整的框架来描述任务进度相关的功能.这使得在开发中进行耗时任务进度的监听将什么麻烦,在iOS7之后,系统提供了NSProgress类来专门报告任务进度. 二.创建单任务进度监听器 单任务进度的监听是NSProgress最简单的一种运用场景,我们来用定时器模拟一个耗时任务,示例代码如下: @interface ViewController () { NSProgress * progress; } @end @implementation ViewCo

  • Python显示进度条的方法

    本文实例讲述了Python显示进度条的方法,是Python程序设计中非常实用的技巧.分享给大家供大家参考.具体方法如下: 首先,进度条和一般的print区别在哪里呢? 答案就是print会输出一个\n,也就是换行符,这样光标移动到了下一行行首,接着输出,之前已经通过stdout输出的东西依旧保留,而且保证我们在下面看到最新的输出结果. 进度条不然,我们必须再原地输出才能保证他是一个进度条,否则换行了怎么还叫进度条? 最简单的办法就是,再输出完毕后,把光标移动到行首,继续在那里输出更长的进度条即可

  • Android应用中炫酷的横向和环形进度条的实例分享

    一.概述 最近需要用进度条,秉着不重复造轮子的原则,上github上搜索了一番,看了几个觉得比较好看的ProgressBar,比如:daimajia的等.简单看了下代码,基本都是继承自View,彻彻底底的自定义了一个进度条.盯着那绚丽滚动条,忽然觉得,为什么要通过View去写一个滚动条,系统已经提供了ProgressBar以及属于它的特性,我们没必要重新去构建一个,但是系统的又比较丑,不同版本变现还不一定一样.那么得出我们的目标:改变系统ProgressBar的样子. 对没错,我们没有必要去从0

  • Core Animation一些Demo总结 (动态切换图片、大转盘、图片折叠、进度条等动画效果)

    前一篇总结了Core Animation的一些基础知识,这一篇主要是Core Animation 的一些应用,涉及到CAShapeLayer.CAReplicatorLayer等图层的知识. 先看效果图: 1.切换图片: 2.彩票转盘 3.图片折叠 4.进度条旋转 5.粒子效果 一.切换图片 看起来很复杂的动画,通过少量的计算和编码就可以简单的实现.要做到这一步,必须是需要研究iOS开发中的Core Animation和Core Graphics框架的.日常工作中,对于很多东西不求甚解,只是拿过

  • 使用Timer实现网页匀速加载的进度条样式

    在使用WebView加载网页时有时候网速等原因加载比较慢时,为了避免在加载网页的时候出现一片空白的区域,给用户很不好的体验感,我们往往在加载的时候添加一个进度条,使用户直观的感受到网页加载的进度,通常我们可以通过WebChromeClient里面的onProgressChanged()方法获取到当前的网页加载进度,但是当我们使用时会发现他的网页加载进度不是一点一点加载的,也许一下就加载到50%下一秒直接加载到80%,如果我们将其设置给progressBar看起来就很快而且很不顺畅,体验感较差,如

  • Python实现控制台进度条功能

    本文实例讲述了Python显示进度条的方法,是Python程序设计中非常实用的技巧.分享给大家供大家参考.具体方法如下: 首先,进度条和一般的print区别在哪里呢? 答案就是print会输出一个\n,也就是换行符,这样光标移动到了下一行行首,接着输出,之前已经通过stdout输出的东西依旧保留,而且保证我们在下面看到最新的输出结果. 进度条不然,我们必须再原地输出才能保证他是一个进度条,否则换行了怎么还叫进度条? 最简单的办法就是,再输出完毕后,把光标移动到行首,继续在那里输出更长的进度条即可

随机推荐