Skip to content

Instantly share code, notes, and snippets.

@subramanian42
Created October 23, 2023 07:24
Show Gist options
  • Save subramanian42/378154595371f286c94036dbb9da9f65 to your computer and use it in GitHub Desktop.
Save subramanian42/378154595371f286c94036dbb9da9f65 to your computer and use it in GitHub Desktop.
avian-dawn-7491
library nuts_activity_indicator;
import 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
/// A highly customizable activity indicator (spinner)
/// based on the iOS-style activity indicator from the
/// `cupertino` package.
///
/// Key takeaways from [Apple Human Interface Guidelines on Activity Indicators](https://developer.apple.com/design/human-interface-guidelines/ios/controls/progress-indicators/#activity-indicators)
/// that are relevant to the [NutsActivityIndicator] class:
///
/// * use activity indicator only when activity cannot be quantified
/// (for example remaining time, task count, data size).
/// * keep it moving: only disable [animating] if the process stalls.
///
/// For more information, see
/// * [Flutter `cupertino` library `CupertinoActivityIndicator` class](https://api.flutter.dev/flutter/cupertino/CupertinoActivityIndicator-class.html)
/// * [Apple Human Interface Guidelines on Activity Indicators](https://developer.apple.com/design/human-interface-guidelines/ios/controls/progress-indicators/#activity-indicators)
class NutsActivityIndicator extends StatefulWidget {
/// Whether the activity indicator is running its animation.
///
/// Defaults to true.
final bool animating;
/// Radius of the activity indicator.
///
/// Defaults to 10px. Must be positive and cannot be null.
final double radius;
/// The count of rectangles the activity indicator has.
///
/// The activity indicator (spinner) is made up of multiple small
/// rectangles, "ticks", and this number specifies how many of
/// these small ticks should be painted in the widget.
///
/// Defaults to 12. Must be positive and cannot be null.
final int tickCount;
/// The active color of the small rectangles within the activity indicator.
///
/// The activity indicator (spinner) contains [tickCount] stationary rectangles
/// and these ticks' colors are animated between the [activeColor] and
/// [inactiveColor] colors, thus creating a perceived rotation of the object.
///
/// Defaults to a grey color, #9D9D9D
final Color activeColor;
/// The deactive color of the small rectangles within the activity indicator.
///
/// The activity indicator (spinner) contains [tickCount] stationary rectangles
/// and these ticks' colors are animated between the [activeColor] and
/// [inactiveColor] colors, thus creating a perceived rotation of the object.
///
/// Defaults to a grey color, #E5E5EA
final Color inactiveColor;
/// The time in which the activity indicator's animation finishes.
///
/// The animation takes a circle by fading between the active and
/// inactive colors for each small tick.
///
/// Defaults to 1 second.
final Duration animationDuration;
/// TODO: what is this width value really?
final double relativeWidth;
/// Radius ratio tells where the rectangles start.
///
/// Defaults to 0.5, meaning that the "ticks" will go from
/// (0.5 * radius, radius).
///
/// I'm sorry I can't explain it any better. Check the example app...
///
/// If you know how to explain it better, open a PR!
final double startRatio;
/// Radius ratio tells where the rectangles end.
///
/// Defaults to 1, meaning that the "ticks" will go from
/// (0.5 * radius, 1 * radius).
///
/// I'm sorry I can't explain it any better. Check the example app...
///
/// If you know how to explain it better, open a PR!
final double endRatio;
/// Creates a highly customizable activity indicator.
const NutsActivityIndicator({
Key? key,
this.animating = true,
this.radius = 10,
this.startRatio = 0.5,
this.endRatio = 1.0,
this.tickCount = 12,
this.activeColor = const Color(0xFF9D9D9D),
this.inactiveColor = const Color(0xFFE5E5EA),
this.animationDuration = const Duration(seconds: 1),
this.relativeWidth = 1,
}) : super(key: key);
@override
State<NutsActivityIndicator> createState() => _NutsActivityIndicatorState();
}
class _NutsActivityIndicatorState extends State<NutsActivityIndicator>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: widget.animationDuration,
vsync: this,
);
if (widget.animating) {
_animationController.repeat();
}
}
@override
void didUpdateWidget(NutsActivityIndicator oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.animating != oldWidget.animating) {
if (widget.animating) {
_animationController.repeat();
} else {
_animationController.stop();
}
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: widget.radius * 2,
width: widget.radius * 2,
child: CustomPaint(
painter: _NutsActivityIndicatorPainter(
animationController: _animationController,
radius: widget.radius,
tickCount: widget.tickCount,
activeColor: widget.activeColor,
inactiveColor: widget.inactiveColor,
relativeWidth: widget.relativeWidth,
startRatio: widget.startRatio,
endRatio: widget.endRatio,
),
),
);
}
}
class _NutsActivityIndicatorPainter extends CustomPainter {
final int _halfTickCount;
final Animation<double> animationController;
final Color activeColor;
final Color inactiveColor;
final double relativeWidth;
final int tickCount;
final double radius;
final RRect _tickRRect;
final double startRatio;
final double endRatio;
_NutsActivityIndicatorPainter({
required this.radius,
required this.tickCount,
required this.animationController,
required this.activeColor,
required this.inactiveColor,
required this.relativeWidth,
required this.startRatio,
required this.endRatio,
}) : _halfTickCount = tickCount ~/ 2,
_tickRRect = RRect.fromLTRBXY(
-radius * endRatio,
relativeWidth * radius / 10,
-radius * startRatio,
-relativeWidth * radius / 10,
1,
1,
),
super(repaint: animationController);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint();
canvas
..save()
..translate(size.width / 2, size.height / 2);
final activeTick = (tickCount * animationController.value).floor();
for (int i = 0; i < tickCount; ++i) {
paint.color = Color.lerp(
activeColor,
inactiveColor,
((i + activeTick) % tickCount) / _halfTickCount,
)!;
canvas
..drawRRect(_tickRRect, paint)
..rotate(-math.pi * 2 / tickCount);
}
canvas.restore();
}
@override
bool shouldRepaint(_NutsActivityIndicatorPainter oldPainter) {
return oldPainter.animationController != animationController;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoApp(
debugShowCheckedModeBanner: false,
title: 'NutsActivityIndicator Demo',
home: ActivityIndicatorDemo(),
);
}
}
class ActivityIndicatorDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
backgroundColor: Colors.white,
navigationBar: const CupertinoNavigationBar(
middle: Text('nuts_activity_indicator'),
backgroundColor: Colors.red,
),
child: SafeArea(
child: ListView(
children: const [
Padding(
padding: EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Text(
'For all the supported features, check out the API reference.',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
ActivityIndicatorDemoRow(
'Without any customization, the activity indicator will look like the "standard" activity indicator from the Flutter cupertino library.',
NutsActivityIndicator(),
),
ActivityIndicatorDemoRow(
'We can use the "radius" named parameter make the indicators a bit bigger. So far nothing extraordinary, the cupertino activity indicator can do that too.',
NutsActivityIndicator(
radius: 20,
),
),
ActivityIndicatorDemoRow(
'We also support the animating option. Set to false to let the user know that your app has crapped its pants, I guess?',
NutsActivityIndicator(
radius: 15,
animating: false,
),
),
ActivityIndicatorDemoRow(
'Now, let\'s explore the fun parts of this package. For starters, we can change the colors. Let\'s use red and... Orange?',
NutsActivityIndicator(
radius: 25,
activeColor: Colors.amber,
inactiveColor: Colors.red,
),
),
ActivityIndicatorDemoRow(
'BORING. Speed things up by specifying the animation duration. The animation below takes 300 milliseconds to compete (go full circle). By default, this duration is 1 second.',
NutsActivityIndicator(
radius: 20,
activeColor: Colors.yellow,
inactiveColor: Colors.black,
animationDuration: Duration(milliseconds: 300),
),
),
ActivityIndicatorDemoRow(
'Want to make the rectangles shorter relative to the size of the widget? Use the startRatio and endRatio parameters.',
NutsActivityIndicator(
activeColor: Colors.indigo,
inactiveColor: Colors.blueGrey,
tickCount: 24,
relativeWidth: 0.4,
radius: 60,
startRatio: 0.6,
),
),
ActivityIndicatorDemoRow(
'Two many ticks (rectangles) in the spinner? If you do not like it, change it by passing the tickCount parameter! This example uses 3 rectangles to draw the spinner. The default value is 12.',
NutsActivityIndicator(
activeColor: Colors.red,
tickCount: 3,
radius: 30,
),
),
ActivityIndicatorDemoRow(
'Last thing I show you is how to make the spinner ticks thinner. Use the relativeWidth parameter.',
NutsActivityIndicator(
radius: 50,
tickCount: 16,
activeColor: Colors.red,
inactiveColor: Colors.black,
animationDuration: Duration(milliseconds: 750),
relativeWidth: 0.3,
),
),
],
),
),
);
}
}
class ActivityIndicatorDemoRow extends StatelessWidget {
const ActivityIndicatorDemoRow(
this.description,
this.indicator, {
Key? key,
}) : super(key: key);
final String description;
final NutsActivityIndicator indicator;
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Text(description),
),
indicator,
const SizedBox(height: 16),
const Divider(),
],
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment