preface
This article documents how to implement a progress bar control using CustomPaint and GestureDetector. First of all, to be sure the flutter Material in the component library provides two progress indicator: LinearProgressIndicator and CircularProgressIndicator. If these two progress indicators meet your development needs, don’t try to build your own wheels. The progress bar control implemented in this paper has the following functions:
- Progress data of type double in the range 0 to 1
- Drag is supported to retrieve progress values through callback functions
- Support jump, click a location after the progress jump, callback progress value
- The style is Material style and can be modified as needed
Implementation steps:
Recognize drag gestures
Using GestureDetector makes it easy to listen for sliding and clicking events. Here are the four events to listen for, focusing on onHorizontalDragUpdate, whose callback passes information such as the coordinates of the horizontal drag event to the _seekToRelativePosition function. The _seekToRelativePosition function computs the value of the slider’s progress bar and updates the interface. The code is as follows:
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); },/ /...
)
Copy the code
_seekToRelativePosition Converts the global coordinates to the action coordinates of the progress bar control. Take the value of the progress bar as the ratio of x to the length of the progress bar control at the point of click. Then call setState() to update the interface.
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;
});
}
Copy the code
The code above has a controller control defined as follows:
class ProgressBarController extends ChangeNotifier
{
double progressBarValue = . 0;
updateProgressValue(doublevalue){ progressBarValue = value; notifyListeners(); }}Copy the code
It inherits from ChangeNotifier because the state of this progress bar control is managed by a mixture of other controls and the control itself. When other controls want to change the value of the progress bar, they can use the ProgressBarController to inform the progress bar control to update the screen. Of course, it is easier to implement the progress bar control with the StatelessWidget, and then directly call setState() to update the interface. Readers can try it if necessary.
Draw a progress bar using CustomPaint
The drawing part is easy. Below, first draw the gray background, then draw the red progress, and then return to the dot.
class _ProgressBarPainter extends CustomPainter {
_ProgressBarPainter(
{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, ); }}Copy the code
Complete code:
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 ProgressBarController controller;
@override
void initState() {
controller = ProgressBarController();
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: ProgressBar(
controller: controller,
barHeight: 2,
onDragEnd: (double progress) {
print("$progress");
},
),
),
Text("value:$_progressValue"),
ElevatedButton(
onPressed: (){
_progressValue = 1;
controller.updateProgressValue(_progressValue);
},
child: Text("increase"() [(), (); }}/// progress bar
classProgressBar extends StatefulWidget {
ProgressBar({
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 ProgressBarController controller;
//final bool drawShadow;
@override
_ProgressBarState createState() => _ProgressBarState();
}
class _ProgressBarState extends State<ProgressBar> {
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;
returnGestureDetector( 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: _ProgressBarPainter( barHeight: widget.barHeight, handleHeight: widget.handleHeight, colors: widget.colors, value: progress)), ), )); }}class _ProgressBarPainter extends CustomPainter {
_ProgressBarPainter(
{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 ProgressBarController extends ChangeNotifier
{
double progressBarValue = . 0;
updateProgressValue(doublevalue){ 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;
}
Copy the code