flutter轮子计划之进度条
作者:卡路里我的天敌 时间:2023-06-21 07:59:39
前言
本文的记录如何用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;
}
来源:https://blog.csdn.net/weixin_44239910/article/details/120660336