Skip to content

Commit f60baec

Browse files
committed
initial commit
0 parents  commit f60baec

File tree

11 files changed

+353
-0
lines changed

11 files changed

+353
-0
lines changed

.gitignore

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Miscellaneous
2+
*.class
3+
*.log
4+
*.pyc
5+
*.swp
6+
.DS_Store
7+
.atom/
8+
.buildlog/
9+
.history
10+
.svn/
11+
migrate_working_dir/
12+
13+
# IntelliJ related
14+
*.iml
15+
*.ipr
16+
*.iws
17+
.idea/
18+
19+
# The .vscode folder contains launch configuration and tasks you configure in
20+
# VS Code which you may wish to be included in version control, so this line
21+
# is commented out by default.
22+
#.vscode/
23+
24+
# Flutter/Dart/Pub related
25+
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
26+
/pubspec.lock
27+
**/doc/api/
28+
.dart_tool/
29+
.flutter-plugins
30+
.flutter-plugins-dependencies
31+
build/

.metadata

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# This file tracks properties of this Flutter project.
2+
# Used by Flutter tool to assess capabilities and perform upgrades etc.
3+
#
4+
# This file should be version controlled and should not be manually edited.
5+
6+
version:
7+
revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
8+
channel: "stable"
9+
10+
project_type: package

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## 0.0.1
2+
3+
* TODO: Describe initial release.

LICENSE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TODO: Add your license here.

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# 📦 dial_timer
2+
3+
A beautiful circular dial timer widget for Flutter.
4+
5+
Easily select a time (in minutes) by dragging around a circular dial UI.
6+
7+
---
8+
9+
## 🚀 Installation
10+
11+
Add the following line to your `pubspec.yaml`:
12+
13+
```yaml
14+
dependencies:
15+
dial_timer: ^0.0.1

analysis_options.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include: package:flutter_lints/flutter.yaml
2+
3+
# Additional information about this file can be found at
4+
# https://dart.dev/guides/language/analysis-options

example/lib/main.dart

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:dial_timer/dial_timer.dart';
3+
4+
void main() {
5+
runApp(const MyApp());
6+
}
7+
8+
class MyApp extends StatelessWidget {
9+
const MyApp({super.key});
10+
11+
@override
12+
Widget build(BuildContext context) {
13+
return MaterialApp(
14+
title: 'Dial Timer Example',
15+
theme: ThemeData(
16+
primarySwatch: Colors.deepPurple,
17+
),
18+
home: const HomeScreen(),
19+
debugShowCheckedModeBanner: false,
20+
);
21+
}
22+
}
23+
24+
class HomeScreen extends StatefulWidget {
25+
const HomeScreen({super.key});
26+
27+
@override
28+
State<HomeScreen> createState() => _HomeScreenState();
29+
}
30+
31+
class _HomeScreenState extends State<HomeScreen> {
32+
int minutes = 0;
33+
34+
@override
35+
Widget build(BuildContext context) {
36+
return Scaffold(
37+
appBar: AppBar(title: const Text('Dial Timer Example')),
38+
body: Center(
39+
child: Column(
40+
mainAxisAlignment: MainAxisAlignment.center,
41+
children: [
42+
CircularTimer(
43+
onMinutesChanged: (m) {
44+
setState(() {
45+
minutes = m;
46+
});
47+
},
48+
),
49+
const SizedBox(height: 32),
50+
Text(
51+
'선택된 시간: $minutes 분',
52+
style: const TextStyle(fontSize: 20),
53+
),
54+
],
55+
),
56+
),
57+
);
58+
}
59+
}

lib/dial_timer.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// A Calculator.
2+
class Calculator {
3+
/// Returns [value] plus 1.
4+
int addOne(int value) => value + 1;
5+
}

lib/src/circular_timer.dart

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import 'dart:math';
2+
import 'package:flutter/material.dart';
3+
4+
class CircularTimer extends StatefulWidget {
5+
final void Function(int minutes)? onMinutesChanged;
6+
final bool isDraggable;
7+
final bool isBreakTime;
8+
9+
const CircularTimer({
10+
super.key,
11+
this.isDraggable = true,
12+
this.onMinutesChanged,
13+
this.isBreakTime = false,
14+
});
15+
16+
@override
17+
State<CircularTimer> createState() => _CircularTimerState();
18+
}
19+
20+
class _CircularTimerState extends State<CircularTimer> {
21+
double angle = 0;
22+
Offset center = Offset.zero;
23+
double radius = 0;
24+
25+
@override
26+
Widget build(BuildContext context) {
27+
final Color timerColor =
28+
widget.isBreakTime ? Colors.blueAccent : Colors.deepPurple;
29+
30+
return Opacity(
31+
opacity: widget.isDraggable ? 1.0 : 0.5,
32+
child: GestureDetector(
33+
onPanUpdate: _onPanUpdate,
34+
child: LayoutBuilder(
35+
builder: (context, constraints) {
36+
final size = constraints.maxWidth;
37+
center = Offset(size / 2, size / 2);
38+
radius = size / 2;
39+
40+
int minutes = (angle / 360 * 60).round();
41+
if (minutes == 0) minutes = 60;
42+
43+
return Stack(
44+
alignment: Alignment.center,
45+
children: [
46+
CustomPaint(
47+
painter: _CircularTimerPainter(
48+
angle: angle,
49+
color: timerColor,
50+
),
51+
size: Size(size, size),
52+
),
53+
Column(
54+
mainAxisSize: MainAxisSize.min,
55+
children: [
56+
Text(
57+
widget.isBreakTime
58+
? '쉬는 시간!'
59+
: '${minutes.toString().padLeft(2, '0')}:00',
60+
style: const TextStyle(
61+
fontSize: 32,
62+
fontWeight: FontWeight.bold,
63+
color: Colors.black,
64+
),
65+
),
66+
],
67+
),
68+
],
69+
);
70+
},
71+
),
72+
),
73+
);
74+
}
75+
76+
void _onPanUpdate(DragUpdateDetails details) {
77+
if (!widget.isDraggable) return;
78+
79+
final touchPosition = details.localPosition;
80+
final dx = touchPosition.dx - center.dx;
81+
final dy = touchPosition.dy - center.dy;
82+
double radians = atan2(dy, dx);
83+
84+
double newAngle = (radians * 180 / pi + 90) % 360;
85+
if (newAngle < 0) newAngle += 360;
86+
87+
double snappedAngle = (newAngle / 6).round() * 6;
88+
int minutes = (snappedAngle / 360 * 60).round();
89+
if (minutes == 0) minutes = 60;
90+
91+
widget.onMinutesChanged?.call(minutes);
92+
93+
setState(() {
94+
angle = snappedAngle;
95+
});
96+
}
97+
}
98+
99+
class _CircularTimerPainter extends CustomPainter {
100+
final double angle;
101+
final Color color;
102+
103+
_CircularTimerPainter({required this.angle, required this.color});
104+
105+
@override
106+
void paint(Canvas canvas, Size size) {
107+
final center = Offset(size.width / 2, size.height / 2);
108+
final radius = size.width / 2.5;
109+
110+
final backgroundPaint = Paint()
111+
..color = Colors.grey[200]!
112+
..strokeWidth = 12
113+
..style = PaintingStyle.stroke;
114+
115+
final foregroundPaint = Paint()
116+
..color = color
117+
..strokeWidth = 12
118+
..style = PaintingStyle.stroke
119+
..strokeCap = StrokeCap.round;
120+
121+
canvas.drawCircle(center, radius - 20, backgroundPaint);
122+
123+
double sweepAngle = 2 * pi * (angle / 360);
124+
canvas.drawArc(
125+
Rect.fromCircle(center: center, radius: radius - 20),
126+
-pi / 2,
127+
sweepAngle,
128+
false,
129+
foregroundPaint,
130+
);
131+
132+
final double radians = (angle - 90) * pi / 180;
133+
final handleRadius = 12.0;
134+
final handleX = center.dx + (radius - 20) * cos(radians);
135+
final handleY = center.dy + (radius - 20) * sin(radians);
136+
137+
final handlePaint = Paint()
138+
..color = Colors.white
139+
..style = PaintingStyle.fill;
140+
141+
final handleBorderPaint = Paint()
142+
..color = color
143+
..style = PaintingStyle.stroke
144+
..strokeWidth = 3;
145+
146+
canvas.drawCircle(Offset(handleX, handleY), handleRadius, handlePaint);
147+
canvas.drawCircle(
148+
Offset(handleX, handleY), handleRadius, handleBorderPaint);
149+
150+
for (int i = 0; i < 60; i++) {
151+
double tickRadians = (i * 6 - 90) * pi / 180;
152+
final double tickLength = (i % 5 == 0) ? 10 : 5;
153+
154+
final Offset start = Offset(
155+
center.dx + (radius + 5) * cos(tickRadians),
156+
center.dy + (radius + 5) * sin(tickRadians),
157+
);
158+
final Offset end = Offset(
159+
center.dx + (radius + 5 - tickLength) * cos(tickRadians),
160+
center.dy + (radius + 5 - tickLength) * sin(tickRadians),
161+
);
162+
163+
final Paint tickPaint = Paint()
164+
..color = Colors.black54
165+
..strokeWidth = 2;
166+
167+
canvas.drawLine(start, end, tickPaint);
168+
169+
if (i % 5 == 0) {
170+
final textPainter = TextPainter(
171+
text: TextSpan(
172+
text: '${i == 0 ? '0' : i}',
173+
style: const TextStyle(color: Colors.black54, fontSize: 12),
174+
),
175+
textAlign: TextAlign.center,
176+
textDirection: TextDirection.ltr,
177+
)..layout();
178+
179+
final labelX = center.dx + (radius + 15) * cos(tickRadians);
180+
final labelY = center.dy + (radius + 15) * sin(tickRadians);
181+
182+
textPainter.paint(
183+
canvas,
184+
Offset(
185+
labelX - textPainter.width / 2,
186+
labelY - textPainter.height / 2,
187+
),
188+
);
189+
}
190+
}
191+
}
192+
193+
@override
194+
bool shouldRepaint(covariant _CircularTimerPainter oldDelegate) {
195+
return oldDelegate.angle != angle || oldDelegate.color != color;
196+
}
197+
}

pubspec.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: dial_timer
2+
description: "A beautiful circular dial timer widget for Flutter."
3+
version: 0.0.1
4+
5+
environment:
6+
sdk: ">=3.0.0 <4.0.0"
7+
8+
dependencies:
9+
flutter:
10+
sdk: flutter
11+
12+
dev_dependencies:
13+
flutter_test:
14+
sdk: flutter
15+
16+
flutter:

0 commit comments

Comments
 (0)