Skip to content

Instantly share code, notes, and snippets.

@rxlabz
Created April 1, 2023 19:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rxlabz/084fcffd2bfb48519e723a7b45117673 to your computer and use it in GitHub Desktop.
Save rxlabz/084fcffd2bfb48519e723a7b45117673 to your computer and use it in GitHub Desktop.
obsidian-diamond-9881

obsidian-diamond-9881

Created with <3 with dartpad.dev.

import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
const thumbSize = 52.0;
const minTemperature = 0.0;
const maxTemperature = 50.0;
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(home: MainScreen());
}
}
class MainScreen extends StatelessWidget {
const MainScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade100,
body: TemperatureView(),
);
}
}
class TemperatureView extends StatelessWidget {
final ValueNotifier<double> temperature = ValueNotifier(20.0);
TemperatureView({super.key});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Center(
child: SizedBox(
width: 400,
child: ValueListenableBuilder(
valueListenable: temperature,
builder: (context, value, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
' ${temperature.value.toInt()}°',
style: textTheme.displayLarge?.copyWith(
fontWeight: FontWeight.w500, color: Colors.grey[350]),
textAlign: TextAlign.center,
),
),
Expanded(
child: Stack(
children: [
Positioned.fill(
child: Thermo(temperature: temperature.value)),
Positioned.fill(
left: 300,
child: CustomSlider(
value: temperature.value,
onValueChanged: (value) => temperature.value = value,
),
),
],
),
),
TemperatureIconBar(
value: value / maxTemperature,
onTemperatureChanged: (t) => temperature.value = t,
)
],
);
},
),
),
);
}
}
class TemperatureIconBar extends StatelessWidget {
final double value;
final ValueChanged<double> onTemperatureChanged;
const TemperatureIconBar({
super.key,
required this.value,
required this.onTemperatureChanged,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 64),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ThermoIcon(
icon: Icons.ac_unit,
color: Colors.cyan,
selected: value < 1 / 3,
onTap: () => onTemperatureChanged(10),
),
ThermoIcon(
icon: Icons.water_drop,
color: Colors.amber,
selected: value >= 1 / 3 && value < 2 / 3,
onTap: () => onTemperatureChanged(20),
),
ThermoIcon(
icon: Icons.local_fire_department,
color: Colors.deepOrange,
selected: value >= 2 / 3,
onTap: () => onTemperatureChanged(35),
),
],
),
);
}
}
class ThermoIcon extends StatelessWidget {
final IconData icon;
final Color color;
final bool selected;
final VoidCallback onTap;
const ThermoIcon({
required this.icon,
required this.color,
required this.onTap,
required this.selected,
super.key,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: SizedBox(
width: 64,
height: 64,
child: Center(
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: selected ? 64 : 48,
height: selected ? 64 : 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: selected ? color.withOpacity(.3) : Colors.grey.shade200,
),
child: Center(
child: Icon(
icon,
size: selected ? 52 : 32,
color: selected ? color : Colors.grey.shade300,
),
),
),
),
),
);
}
}
class Thermo extends StatefulWidget {
const Thermo({
super.key,
required this.temperature,
});
final double temperature;
@override
State<Thermo> createState() => _ThermoState();
}
class _ThermoState extends State<Thermo> with SingleTickerProviderStateMixin {
late final AnimationController anim;
late final AnimationController impulseController;
final spring = SpringSimulation(
const SpringDescription(mass: 2, stiffness: 120, damping: 1),
0,
0.5,
1,
);
@override
void initState() {
super.initState();
anim = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
anim.animateWith(spring);
}
@override
void didUpdateWidget(covariant Thermo oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.temperature != widget.temperature) {
anim.animateWith(spring);
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: anim,
builder: (context, _) {
return CustomPaint(
painter: ThermoPainter(widget.temperature, animation: anim.value),
);
},
);
}
}
const thermoWidth = 96.0;
const vPadding = 20.0;
const thickness = 8.0;
const radius = thermoWidth / 2;
class ThermoPainter extends CustomPainter {
final double temperature;
final double animation;
ThermoPainter(this.temperature, {required this.animation});
@override
void paint(Canvas canvas, Size size) {
//container
final topLeft = Offset(
(size.width - thermoWidth) / 2 + thickness,
vPadding + thickness,
);
final bottomRight = Offset(
(size.width + thermoWidth) / 2 - thickness,
size.height - vPadding - thickness,
);
_drawLiquid(
canvas,
size,
bottomRight,
topLeft,
offset: const Offset(25, 25),
blur: true,
);
drawContainer(canvas, topLeft, bottomRight);
_drawRules(size, canvas, temperature / maxTemperature);
// mask
_drawMask(canvas, size);
_drawLiquid(canvas, size, bottomRight, topLeft);
// white border
_drawContainerBorder(canvas, topLeft, bottomRight);
// reflets
_drawReflections(size, canvas);
}
void _drawContainerBorder(
ui.Canvas canvas, ui.Offset topLeft, ui.Offset bottomRight) {
return canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromPoints(topLeft, bottomRight),
const Radius.circular(thermoWidth),
),
Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 12,
);
}
void _drawMask(ui.Canvas canvas, ui.Size size) {
canvas.clipRRect(
RRect.fromRectAndRadius(
Rect.fromPoints(
Offset(
(size.width - thermoWidth) / 2 + thickness, vPadding + thickness),
Offset((size.width + thermoWidth) / 2 - thickness,
size.height - vPadding - thickness),
),
const Radius.circular(48),
),
);
}
void _drawReflections(ui.Size size, ui.Canvas canvas) {
var refletTopLeft1 = Offset(
size.width / 2,
vPadding + thickness + 30,
);
var refletBottomRight1 = Offset(
size.width / 2 + 28,
size.height - vPadding - thickness - 30,
);
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromPoints(refletTopLeft1, refletBottomRight1),
const Radius.circular(18),
),
Paint()
..shader = ui.Gradient.linear(
refletTopLeft1,
refletTopLeft1 + const Offset(30, 0),
[Colors.white10, Colors.white],
[0, 1],
)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10),
);
var refletTopLeft2 = Offset(
size.width / 2 - 30,
vPadding + thickness + 30,
);
var refletBottomRight2 = Offset(
size.width / 2 - 12,
size.height - vPadding - thickness - 30,
);
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromPoints(refletTopLeft2, refletBottomRight2),
const Radius.circular(8),
),
Paint()
..shader = ui.Gradient.linear(
refletTopLeft2,
refletTopLeft2 + const Offset(20, 0),
[Colors.white10, Colors.white70],
[0, 1],
)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8),
);
}
void _drawLiquid(
ui.Canvas canvas,
ui.Size size,
ui.Offset bottomRight,
ui.Offset topLeft, {
ui.Offset offset = Offset.zero,
bool blur = false,
}) {
const gradientColors = [
Colors.red,
Colors.orange,
Colors.amber,
Colors.lightGreen,
Colors.cyan,
];
final lightColors = gradientColors.map((e) => e.withOpacity(.5)).toList();
// liquid back
final liquidTopLeft = Offset(
(size.width - thermoWidth) / 2 + thickness,
(size.height - radius - vPadding) -
((temperature / 50) *
(size.height - (2 * vPadding + radius * 2))),
) -
offset;
final r = Rect.fromPoints(liquidTopLeft, bottomRight - offset);
final colorStops = List.generate(
gradientColors.length,
(index) => index / gradientColors.length,
);
final pathLight = Path()
..moveTo(r.topLeft.dx, r.topLeft.dy - (30 * (1 - animation)))
..quadraticBezierTo(
r.topRight.dx - radius,
r.topRight.dy - (50 * (animation)),
r.topRight.dx,
r.topRight.dy - (30 * (animation)),
)
..lineTo(r.bottomRight.dx, r.bottomRight.dy)
..lineTo(r.bottomLeft.dx, r.bottomLeft.dy);
canvas.drawPath(
pathLight,
Paint()
..shader = ui.Gradient.linear(
topLeft,
bottomRight,
lightColors,
colorStops,
)
..maskFilter = MaskFilter.blur(BlurStyle.normal, blur ? 20 : 0),
);
// liquid
if (!blur) {
final path = Path()
..moveTo(r.topLeft.dx, r.topLeft.dy - (30 * animation))
..quadraticBezierTo(
r.topRight.dx - radius,
r.topRight.dy,
r.topRight.dx,
r.topRight.dy - (30 * (1 - animation)),
)
..lineTo(r.bottomRight.dx, r.bottomRight.dy)
..lineTo(r.bottomLeft.dx, r.bottomLeft.dy);
canvas.drawPath(
path,
Paint()
..shader = ui.Gradient.linear(
topLeft,
bottomRight,
gradientColors,
colorStops,
),
);
}
}
void _drawRules(ui.Size size, ui.Canvas canvas, double ratio) {
final steps = List.generate(20, (index) => index);
for (final step in steps.skip(2)) {
final y = step * (size.height - 40) / 20;
canvas.drawLine(
Offset(340, y),
Offset(348, y),
Paint()
..color = y < (1 - ratio) * size.height
? Colors.grey.shade300
: Colors.grey.shade400
..style = PaintingStyle.stroke
..strokeWidth = 3,
);
}
}
void drawContainer(
ui.Canvas canvas, ui.Offset topLeft, ui.Offset bottomRight) {
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromPoints(topLeft, bottomRight),
const Radius.circular(48),
),
Paint()..color = Colors.grey.shade300,
);
}
@override
bool shouldRepaint(covariant ThermoPainter oldDelegate) {
return oldDelegate.temperature != temperature ||
oldDelegate.animation != animation;
}
}
class CustomSlider extends StatelessWidget {
final double value;
final ValueChanged<double> onValueChanged;
const CustomSlider({
required this.value,
required this.onValueChanged,
super.key,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final refHeight = constraints.maxHeight - thumbSize * 1.8;
var ratio = 1 - value / maxTemperature;
final trackTop = refHeight * ratio;
final color = tween.transform(ratio)!;
return Stack(
children: [
Positioned.fill(
top: 25,
child: CustomPaint(
painter: CustomSliderTrackShapePainter(
value: ratio,
color: color,
),
),
),
Positioned(
left: 16,
top: ui.clampDouble(trackTop, 30, refHeight /*- thumbSize*/),
child: CustomSliderThumb(
refHeight: refHeight,
trackTop: trackTop,
onValueChanged: onValueChanged,
color: color,
),
),
],
);
},
);
}
}
class CustomSliderThumb extends StatefulWidget {
const CustomSliderThumb({
super.key,
required this.refHeight,
required this.trackTop,
required this.onValueChanged,
required this.color,
});
final double refHeight;
final double trackTop;
final ValueChanged<double> onValueChanged;
final ui.Color color;
@override
State<CustomSliderThumb> createState() => _CustomSliderThumbState();
}
class _CustomSliderThumbState extends State<CustomSliderThumb> {
bool active = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onVerticalDragStart: (d) => setState(() => active = true),
onVerticalDragEnd: (d) => setState(() => active = false),
onVerticalDragUpdate: (d) {
final newValue = (widget.refHeight - widget.trackTop - d.delta.dy) /
widget.refHeight *
maxTemperature;
widget.onValueChanged(ui.clampDouble(newValue, 0, 50));
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: thumbSize,
height: thumbSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
Colors.white70,
active ? widget.color : Colors.grey.shade300,
],
),
boxShadow: [
BoxShadow(
offset: const Offset(3, 0),
blurRadius: 2,
spreadRadius: 1,
color: Colors.grey.shade400,
blurStyle: BlurStyle.inner,
),
],
),
),
);
}
}
final tween = TweenSequence<Color?>(
[
TweenSequenceItem(
tween: ColorTween(begin: Colors.red, end: Colors.orange).chain(
CurveTween(curve: const Interval(0.0, 0.15, curve: Curves.linear))),
weight: 0.25,
),
TweenSequenceItem(
tween: ColorTween(begin: Colors.orange, end: Colors.amber).chain(
CurveTween(curve: const Interval(0.15, 0.3, curve: Curves.linear))),
weight: 0.25,
),
TweenSequenceItem(
tween: ColorTween(begin: Colors.amber, end: Colors.lightGreen).chain(
CurveTween(curve: const Interval(0.3, 0.5, curve: Curves.linear))),
weight: 0.25,
),
TweenSequenceItem(
tween: ColorTween(begin: Colors.lightGreen, end: Colors.cyan).chain(
CurveTween(curve: const Interval(0.5, .7, curve: Curves.linear))),
weight: 0.25,
),
],
);
class CustomSliderTrackShapePainter extends CustomPainter {
final double value;
final Color color;
CustomSliderTrackShapePainter({required this.value, required this.color});
final thickness = 4.0;
@override
void paint(ui.Canvas canvas, ui.Size size) {
const marginRight = 24.0;
var refHeight =
(size.height - thumbSize + ((value - .35) * 14) - (value + .1) * 30);
final path = Path()
..moveTo(marginRight, 0)
..lineTo(marginRight, refHeight * (value) - 60)
..cubicTo(
marginRight,
refHeight * (value) - 20,
marginRight / 3,
refHeight * (value) - 40,
0,
max(
thumbSize / 2 + 10,
refHeight * (value),
),
)
..cubicTo(
0,
max(80, refHeight * (value) + 40),
marginRight,
max(60, refHeight * (value) + 30),
marginRight,
max(100, refHeight * (value) + 60),
)
..lineTo(marginRight, size.height) /*..lineTo(marginRight, 0)*/;
canvas.drawPath(
path,
Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 4
..shader = ui.Gradient.linear(
const Offset(0, 0),
Offset(0, size.height),
[Colors.grey.shade300.withOpacity(0), color, color.withOpacity(0)],
[0, ui.clampDouble(value, .1, .9), 1],
),
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment