Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active May 1, 2022 06:54
Show Gist options
  • Save PlugFox/d2274f2d4278473774b79b0020cbd618 to your computer and use it in GitHub Desktop.
Save PlugFox/d2274f2d4278473774b79b0020cbd618 to your computer and use it in GitHub Desktop.
Custom circular progress indicator
/*
* Custom progress indicator
* https://gist.github.com/PlugFox/d2274f2d4278473774b79b0020cbd618
* https://dartpad.dev/d2274f2d4278473774b79b0020cbd618
* Matiunin Mikhail <plugfox@gmail.com>, 1 May 2022
*/
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
void main() => runZonedGuarded<Future<void>>(() async {
runApp(const App());
}, (error, stackTrace) {
// ignore: avoid_print
print(error);
});
/// {@template app}
/// App widget
/// {@endtemplate}
class App extends StatefulWidget {
/// {@macro main.app}
const App({
Key? key,
}) : super(key: key);
/// The state from the closest instance of this class
/// that encloses the given context, if any.
static AppController? maybeOf(BuildContext context) =>
context.findAncestorStateOfType<_AppState>();
@override
State<App> createState() => _AppState();
} // App
/// State for widget App
class _AppState extends State<App> with AppController {
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Custom progress indicator',
theme: _themeData,
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Custom progress indicator'),
centerTitle: true,
),
body: const SafeArea(
child: Center(
child: Padding(
padding: EdgeInsets.all(24),
child: CustomProgressIndicator(
size: 256,
child: ThemeSwitcher(size: 128),
),
),
),
),
),
);
} // _AppState
mixin AppController on State<App> {
ThemeData _themeData = ThemeData.light();
ThemeData get themeData => _themeData;
void setLightTheme() => setState(() => _themeData = ThemeData.light());
void setDarkTheme() => setState(() => _themeData = ThemeData.dark());
} // AppController
/// {@template main.main}
/// ThemeSwitcher widget
/// {@endtemplate}
class ThemeSwitcher extends StatelessWidget {
/// {@macro main.main}
const ThemeSwitcher({
this.size = 64,
Key? key,
}) : super(key: key);
/// The size of the icon
final double size;
@override
Widget build(BuildContext context) =>
Theme.of(context).brightness == Brightness.dark
? IconButton(
icon: const Icon(Icons.nightlight),
padding: EdgeInsets.zero,
iconSize: size,
tooltip: 'Switch to light theme',
onPressed: () => App.maybeOf(context)?.setLightTheme(),
)
: IconButton(
icon: const Icon(Icons.sunny),
padding: EdgeInsets.zero,
iconSize: size,
tooltip: 'Switch to dark theme',
onPressed: () => App.maybeOf(context)?.setDarkTheme(),
);
} // ThemeSwitcher
/// {@template custom_progress_indicator}
/// CustomProgressIndicator widget
/// {@endtemplate}
class CustomProgressIndicator extends StatefulWidget {
/// {@macro custom_progress_indicator}
const CustomProgressIndicator({
this.size = 64,
this.child,
Key? key,
}) : super(key: key);
/// The size of the progress indicator
final double size;
/// The child widget
final Widget? child;
@override
State<CustomProgressIndicator> createState() =>
_CustomProgressIndicatorState();
} // CustomProgressIndicator
/// State for widget CustomProgressIndicator
class _CustomProgressIndicatorState extends State<CustomProgressIndicator>
with SingleTickerProviderStateMixin {
late final AnimationController _sweepController;
late final Animation<double> _curvedAnimation;
/* #region Lifecycle */
@override
void initState() {
super.initState();
_sweepController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat();
_curvedAnimation = CurvedAnimation(
parent: _sweepController,
curve: Curves.ease,
);
}
@override
void dispose() {
_sweepController.dispose();
super.dispose();
}
/* #endregion */
@override
Widget build(BuildContext context) => SizedBox.square(
dimension: widget.size,
child: RepaintBoundary(
child: CustomPaint(
painter: _ProgressIndicatorPainter(
animation: _curvedAnimation,
color: Theme.of(context).indicatorColor,
),
child: Center(
child: widget.child,
),
),
),
);
} // _CustomProgressIndicatorState
/// {@template progress_indicator_painter}
/// _ProgressIndicatorPainter
/// {@endtemplate}
class _ProgressIndicatorPainter extends CustomPainter {
/// {@macro progress_indicator_painter}
_ProgressIndicatorPainter({
required Animation<double> animation,
Color color = Colors.blue,
}) : _animation = animation,
_arcPaint = Paint()
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..color = color,
super(repaint: animation);
final Animation<double> _animation;
final Paint _arcPaint;
@override
void paint(Canvas canvas, Size size) {
_arcPaint.strokeWidth = size.shortestSide / 8;
final progress = _animation.value;
final rect = Rect.fromCircle(
center: size.center(Offset.zero),
radius: size.shortestSide / 2 - _arcPaint.strokeWidth / 2,
);
//final rotate = progress * lerpDouble(.0, math.pi * 2, progress)!;
final rotate = math.pow(progress, 2) * math.pi * 2;
final sweep = math.sin(progress * math.pi) * 3 + math.pi * .25;
canvas.drawArc(rect, rotate, sweep, false, _arcPaint);
}
@override
bool shouldRepaint(
covariant _ProgressIndicatorPainter oldDelegate,
) =>
_animation.value != oldDelegate._animation.value;
@override
bool shouldRebuildSemantics(
covariant _ProgressIndicatorPainter oldDelegate,
) =>
false;
} // _ProgressIndicatorPainter
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment