Skip to content

Instantly share code, notes, and snippets.

@alwerr
Forked from pskink/color_wheel.dart
Last active March 27, 2024 15:53
Show Gist options
  • Save alwerr/6d21e3508cf5d90f2576bfe09eaabc74 to your computer and use it in GitHub Desktop.
Save alwerr/6d21e3508cf5d90f2576bfe09eaabc74 to your computer and use it in GitHub Desktop.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
main() {
runApp(MaterialApp(home: Scaffold(body: ColorWheel())));
}
typedef RangeRecord = ({int index, double begin, double end, double turn});
class ColorWheel extends StatefulWidget {
@override
State<ColorWheel> createState() => _ColorWheelState();
}
final spring = SpringDescription.withDampingRatio(
mass: 0.4,
stiffness: 500,
ratio: 0.95,
);
class _ColorWheelState extends State<ColorWheel> with TickerProviderStateMixin {
late final rotation = AnimationController.unbounded(vsync: this)..addListener(_listener);
double currentAngle = 0;
double oldAngle = 0;
var data = [
(0, 3.5, Colors.orange, 'orange', 'Reprehenderit eu laboris aliquip aliqua officia.'),
(1, 2.5, Colors.pink, 'pink', 'In culpa pariatur.'),
(2, 2.5, Colors.teal, 'teal', 'In culpa pariatur.'),
(3, 3, Colors.deepPurple, 'deep purple', 'Aliqua quis et id dolore labore.'),
];
final current = ValueNotifier(0);
late final ranges = _initRanges();
late double delta;
List<RangeRecord> _initRanges() {
final totalFlex = data.fold(0.0, (acc, d) => acc += d.$2);
double sum = 0;
delta = 0.5 * data.first.$2 / totalFlex;
return List.generate(data.length, (index) {
final v0 = sum / totalFlex;
sum += data[index].$2;
final v1 = sum / totalFlex;
return (index: index, begin: 1 - v1 + delta, end: 1 - v0 + delta, turn: (v0 + v1) / 2 - delta);
});
}
(double, RangeRecord) get normalizedRotationWithRange {
double nr = rotation.value % 1;
if (nr < delta) nr++;
assert(nr >= delta && nr <= 1 + delta);
return (nr, ranges.firstWhere((r) => r.begin <= nr && nr <= r.end));
}
@override
Widget build(BuildContext context) {
ranges.forEach(print);
return AspectRatio(
aspectRatio: 1,
child: LayoutBuilder(
builder: (context, constraints) {
return ListenableBuilder(
listenable: current,
builder: (context, _) {
print('current: ${current.value} (${data[current.value].$4})');
return GestureDetector(
onTap: () {
setState(() {
data = [
(0, 3.5, Colors.orange, 'orange', 'Reprehenderit eu laboris aliquip aliqua officia.'),
(1, 2.5, Colors.teal, 'teal', 'In culpa pariatur.'),
];
});
},
onPanDown: (d) => _updateAngle(constraints.biggest, d.localPosition, true),
onPanUpdate: (d) {
_updateAngle(constraints.biggest, d.localPosition, false);
},
onPanEnd: (d) async {
final (v, r) = normalizedRotationWithRange;
print('starting animation, target: ${data[r.index].$4}');
final simulation = SpringSimulation(spring, v, (r.begin + r.end) / 2, 0);
await rotation.animateWith(simulation);
// print('animation finished');
},
child: RotationTransition(
turns: rotation,
child: Stack(
children: [
for (final (i, _, color, colorText, label) in data)
Transform.rotate(
angle: 2 * pi * ranges[i].turn,
child: AnimatedContainer(
duration: Durations.extralong4,
decoration: ShapeDecoration(
shape: PieShape(sweepAngle: 2 * pi * (ranges[i].end - ranges[i].begin)),
color: current.value == i ? color : Colors.grey,
),
child: Align(
alignment: const Alignment(0, -0.80),
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: constraints.maxWidth * 0.4),
child: AnimatedContainer(
duration: Durations.extralong4,
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(8)),
color: current.value == i ? Color.alphaBlend(Colors.white12, color) : Colors.grey,
boxShadow: current.value == i ? kElevationToShadow[2] : null,
),
child: Padding(
padding: const EdgeInsets.all(4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
_buildMoveArrow(Icons.arrow_left, i - 1, current.value == i),
Expanded(
child: Center(
child: Text(colorText,
style: const TextStyle(fontWeight: FontWeight.bold)),
),
),
_buildMoveArrow(Icons.arrow_right, i + 1, current.value == i),
],
),
ClipRect(
child: AnimatedAlign(
alignment: Alignment.bottomCenter,
duration: Durations.long4,
heightFactor: current.value == i ? 1 : 0,
child: Text(label),
),
),
],
),
),
),
),
),
),
),
],
),
),
);
});
},
),
);
}
_updateAngle(Size size, Offset position, bool down) {
final center = size.center(Offset.zero);
currentAngle = (position - center).direction;
final delta = down ? 0 : currentAngle - oldAngle;
oldAngle = currentAngle;
rotation.value += delta / (2 * pi);
}
void _listener() {
final (_, r) = normalizedRotationWithRange;
current.value = r.index;
}
Widget _buildMoveArrow(IconData icon, int i, bool top) {
return SizedOverflowBox(
size: const Size.square(18),
child: AnimatedScale(
duration: Durations.long1,
scale: top ? 1 : 0,
child: IconButton(
onPressed: () => _move(i),
icon: Icon(icon),
),
),
);
}
_move(int i) async {
final r = ranges[i % ranges.length];
double to = (r.begin + r.end) / 2;
final diff = to - rotation.value;
if (diff.abs() > 0.5) {
final delta = (to - rotation.value).round();
// print('before, to: $to, rotation.value: ${rotation.value}, delta: $delta');
to -= delta;
// print('after, to: $to');
}
final simulation = SpringSimulation(spring, rotation.value, to, 0);
await rotation.animateWith(simulation);
}
}
class PieShape extends ShapeBorder {
PieShape({
required this.sweepAngle,
});
final double sweepAngle;
@override
EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
@override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) => getOuterPath(rect);
@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
return Path()
..moveTo(rect.center.dx, rect.center.dy)
..arcTo(rect, -(sweepAngle + pi) / 2, sweepAngle, false);
}
@override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {}
@override
ShapeBorder scale(double t) => this;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment