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

【flutter轮子计划之进度条】_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 = https://www.it610.com/article/progress; }); }

上面代码中有一个controller控件,其定义如下:
class VideoProgressBarController extends ChangeNotifier{double progressBarValue = https://www.it610.com/article/.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; @overridebool shouldRepaint(CustomPainter painter) {return true; } @overridevoid 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.@overrideWidget 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 = https://www.it610.com/article/.5; late VideoProgressBarController controller; @overridevoid initState() {controller = VideoProgressBarController(); super.initState(); } @overrideWidget 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 = https://www.it610.com/article/1; controller.updateProgressValue(_progressValue); },child: Text("increase"))]),),); }} /// progress barclass 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; @overridevoid initState() {super.initState(); progress = widget.controller.progressBarValue; widget.controller.addListener(_updateProgressValue); } @overridevoid 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 = https://www.it610.com/article/progress; }); } @overrideWidget 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; @overridebool shouldRepaint(CustomPainter painter) {return true; } @overridevoid 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 = https://www.it610.com/article/.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; }

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

    推荐阅读