前言
本文的记录如何用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{ 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 { 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; }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。