Created at : 2025-01-16 18:02
Auther: Soo.Y
πλ¬Έμ
-
μ±μ μλμ κ°μ κΈ°λ₯μ κ°κ³ μμ΄μΌ ν©λλ€.
- μ μ κ° νμ΄λ¨Έμ μκ°(15, 20, 25, 30, 35)μ μ νν μ μμ΄μΌ ν©λλ€.
- μ μ κ° νμ΄λ¨Έλ₯Ό μ¬μ€μ (리μ )ν μ μμ΄μΌ ν©λλ€.
- μ μ κ° ν μ¬μ΄ν΄μ μλ£ν νμλ₯Ό μΉ΄μ΄νΈν΄μΌ ν©λλ€.
- μ μ κ° 4κ°μ μ¬μ΄ν΄(1λΌμ΄λ)λ₯Ό μλ£ν νμλ₯Ό μΉ΄μ΄νΈν΄μΌ ν©λλ€.
- κ° λΌμ΄λκ° λλλ©΄ μ¬μ©μκ° 5λΆκ° ν΄μμ μ·¨ν μ μμ΄μΌ ν©λλ€.
-
μ΄λ² κ³Όμ μμ μ€μν κ²μ λ½λͺ¨λλ‘ νμ΄λ¨Έλ₯Ό μ§μ ꡬννλ λΆλΆμ λλ€. λλ¬Έμ μ¬μ΄ν΄μ νμλ μμλ‘ μ€μ νμ λ λ©λλ€.
μ½λ
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Pomotimer',
theme: ThemeData(primarySwatch: Colors.red),
home: const PomodoroTimer(),
);
}
}
class PomodoroTimer extends StatefulWidget {
const PomodoroTimer({Key? key}) : super(key: key);
@override
_PomodoroTimerState createState() => _PomodoroTimerState();
}
class _PomodoroTimerState extends State<PomodoroTimer> {
Timer? _timer;
int _remainingSeconds = 25 * 60;
final List<int> _timeOptions = [15, 20, 25, 30, 35];
int _selectedTime = 25;
bool _isRunning = false;
bool _isBreak = false;
int _cycleCount = 0;
int _roundCount = 0;
final int _breakTime = 5;
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
// νμ΄λ¨Έ μμ
void _startTimer() {
_isRunning = true;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
if (_remainingSeconds > 0) {
_remainingSeconds--;
} else {
// νμ΄λ¨Έ λλ κ²½μ°
_timerEndHandler();
}
});
});
}
// νμ΄λ¨Έ μΌμμ μ§
void _pauseTimer() {
_isRunning = false;
_timer?.cancel();
setState(() {});
}
// νμ΄λ¨Έ 리μ
void _resetTimer() {
_timer?.cancel();
_isRunning = false;
_isBreak = false;
// μ΄κΈ°κ°μ _selectedTimeμ λ°λΌ λΆ μ€μ
_remainingSeconds = _selectedTime * 60;
setState(() {});
}
// νμ΄λ¨Έ μ’
λ£ μ μ²λ¦¬
void _timerEndHandler() {
_timer?.cancel();
_isRunning = false;
if (_isBreak) {
// ν΄μ νμ΄λ¨Έκ° λλ κ²½μ°
_isBreak = false;
_resetTimer();
} else {
// μμ
νμ΄λ¨Έκ° λλ κ²½μ°
_cycleCount++;
// μ¬μ΄ν΄μ΄ 4κ°κ° λλ©΄ λΌμ΄λ 1 μ¦κ°
if (_cycleCount % 4 == 0) {
_roundCount++;
}
// ν΄μ μμ
_startBreak();
}
setState(() {});
}
// ν΄μ νμ΄λ¨Έ μμ
void _startBreak() {
_isBreak = true;
_remainingSeconds = _breakTime * 60;
_startTimer();
}
// λΆ:μ΄ ννλ‘ λ³ν
String _formatTime(int totalSeconds) {
final int minutes = totalSeconds ~/ 60;
final int seconds = totalSeconds % 60;
final String minutesStr = minutes.toString().padLeft(2, '0');
final String secondsStr = seconds.toString().padLeft(2, '0');
return '$minutesStr:$secondsStr';
}
// μ¬μ©μ μ ν μκ° λ³κ²½
void _changeTimeOption(int newTime) {
// νμ΄λ¨Έ λμ μ€μ΄λ©΄ λ³κ²½ λΆκ°νλλ‘ ν μλ μμΌλ,
// μμ μμλ κ°λ¨ν 리μ
ν λ³κ²½ μ²λ¦¬
_pauseTimer();
_selectedTime = newTime;
_resetTimer();
}
@override
Widget build(BuildContext context) {
// μ 체 ν
λ§ μ»¬λ¬
const Color mainColor = Color(0xFFFF5A5F);
return Scaffold(
backgroundColor: mainColor,
body: SafeArea(
child: SingleChildScrollView(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0),
child: Column(
// νλ©΄μ΄ μμ λ λμΉμ§ μλλ‘ μ€ν¬λ‘€ κ°λ₯νκ²
mainAxisAlignment: MainAxisAlignment.center,
children: [
// μ± νμ΄ν
const Padding(
padding: EdgeInsets.only(bottom: 20),
child: Text(
'POMOTIMER',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
),
// λ¨μ μκ° νμ (λΆ:μ΄)
Text(
_formatTime(_remainingSeconds),
style: const TextStyle(
color: Colors.white,
fontSize: 60,
fontWeight: FontWeight.bold,
),
),
// νμ΄λ¨Έ κΈΈμ΄ μ ν
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _timeOptions.map((time) {
return GestureDetector(
onTap: () => _changeTimeOption(time),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 5),
padding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 8),
decoration: BoxDecoration(
color: _selectedTime == time
? Colors.white.withAlpha((0.8 * 255).toInt())
: Colors.white.withAlpha((0.3 * 255).toInt()),
borderRadius: BorderRadius.circular(5),
),
child: Text(
'$time',
style: TextStyle(
color: Colors.red.shade700,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
);
}).toList(),
),
),
// μμ/μΌμμ μ§ λ²νΌ
IconButton(
iconSize: 70,
color: Colors.white,
icon: Icon(_isRunning
? Icons.pause_circle_filled
: Icons.play_circle_fill),
onPressed: () {
if (_isRunning) {
_pauseTimer();
} else {
_startTimer();
}
},
),
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white.withAlpha((0.9 * 255).toInt()),
),
onPressed: _resetTimer,
child: Text(
'RESET',
style: TextStyle(
color: Colors.red.shade700,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 40),
// νλ¨μ μ¬μ΄ν΄ / λΌμ΄λ μ§ν νν© νμ
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
Text(
'${_cycleCount % 4}/4',
style: const TextStyle(
color: Colors.white,
fontSize: 22,
),
),
const Text(
'ROUND',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
const SizedBox(width: 40),
Column(
children: [
Text(
'$_roundCount/12',
style: const TextStyle(
color: Colors.white,
fontSize: 22,
),
),
const Text(
'GOAL',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
],
),
],
),
),
),
),
),
);
}
}
TAμ μ λ΅ ν΄μ€
- κ° MinButtonμ SingleChildScrollView μμμ μνμΌλ‘ μ€ν¬λ‘€ λ©λλ€.
- λ°λ³΅λλ MinButton μ List.generate λ₯Ό νμ©νμ¬ 5λΆ κ°κ²©μΌλ‘ μμ±λκ² νμμ΅λλ€.
- TimeTileμ κ²½μ° Stack, Positioned, Opacity μ μ‘°ν©μΌλ‘ μΉ΄λκ° μμΈ ν¨κ³Όλ₯Ό λ§λ€μμ΅λλ€.
- μ¬μ λ²νΌμ λλ₯΄λ μκ° countDown ν¨μκ° μ€νλκ³ 0.001 μ΄ μ ν λ²μ© μλ λ‘μ§μ λ°λ³΅ν©λλ€.
- 1μ΄ κ°μ
- 0μ΄κ° λ κ²½μ° μ§μ λ μκ°μΌλ‘ reset
- break λͺ¨λμ round λͺ¨λ λ³κ²½
- round λͺ¨λμλ κ²½μ° round μ goal μ κ°±μ
- μ¬μμ¬λΆ false μ€μ
- μ¬μμ€μ΄κ³ μκ°μ΄ λ¨μλμ§ μ¬λΆμ λ°λΌ 루ν μ’ λ£
- StatefullWidget μ λ§λ€κ³ κ° state μ λ³νμ λ°λΌ 루νλ₯Ό λ°λ³΅νκ³ μμ ―μ λ€μ λ λλ§ νλ μ°μ΅μ ν μ μλ κ³Όμ μμ΅λλ€.
main.dart
import 'package:flutter/material.dart';
import 'package:pomodoro/views/pomodoro.dart';
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Pomodoro',
theme: ThemeData(
primaryColor: Colors.green[200],
),
home: const Pomodoro(),
);
}
}
views/pomodoro.dart
import 'package:flutter/material.dart';
import 'package:pomodoro/widgets/min_button.dart';
import '../widgets/time_tile.dart';
class Pomodoro extends StatefulWidget {
const Pomodoro({Key? key}) : super(key: key);
@override
State<Pomodoro> createState() => _PomodoroState();
}
class _PomodoroState extends State<Pomodoro> {
static final List<int> mins = List.generate(7, (i) => 5 + i * 5).toList();
static const _initMin = 15;
int targetMin = _initMin;
int sec = _initMin * 60;
bool playing = false;
bool isBreak = false;
int round = 1;
int goal = 0;
void countDown() {
Future.doWhile(() async {
await Future.delayed(const Duration(milliseconds: 1));
setState(() {
sec = sec - 1;
});
if (sec == 0) {
sec = isBreak ? targetMin * 60 : 5 * 60;
isBreak = !isBreak;
setRoundGoal();
playing = false;
setState(() {});
return false;
}
return playing && sec > 0;
});
}
void setRoundGoal() {
if (isBreak) {
return;
}
if (round >= 4) {
round = 1;
goal++;
} else {
round++;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: isBreak ? Colors.purple : Theme.of(context).primaryColor,
appBar: AppBar(
elevation: 0,
backgroundColor:
isBreak ? Colors.purple : Theme.of(context).primaryColor,
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: const [
Text(
'Minchodoro',
textAlign: TextAlign.start,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
isBreak
? Text(
'Break till bored',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
)
: const Text(
'Work till die',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.purple,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TimeTile(
time: (sec / 60).floor(),
),
const Text(
':',
style: TextStyle(
color: Colors.white,
fontSize: 32,
),
),
TimeTile(time: sec % 60)
],
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var min in mins)
MinButton(
min: min,
selected: min == targetMin,
onPressed: () => setState(() {
targetMin = min;
sec = min * 60;
}),
)
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: () => setState(() {
playing = !playing;
if (playing) {
countDown();
}
}),
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey.shade700,
),
child: Icon(
playing ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 48,
),
),
),
const SizedBox(width: 20),
InkWell(
onTap: () => setState(() {
playing = false;
sec = targetMin * 60;
}),
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey.shade700,
),
child: const Icon(
Icons.replay,
color: Colors.white,
size: 48,
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Round: $round/4'),
const SizedBox(width: 20),
Text('Goal: $goal/12'),
],
),
],
),
);
}
}
widgets/min_buttion.dart
import 'package:flutter/material.dart';
class MinButton extends StatelessWidget {
final int min;
final bool selected;
final VoidCallback onPressed;
const MinButton({
Key? key,
required this.min,
required this.selected,
required this.onPressed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
child: TextButton(
onPressed: onPressed,
style: TextButton.styleFrom(
fixedSize: const Size(84, 48),
backgroundColor: selected ? Colors.white : Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
width: 1,
color: Colors.grey.shade700,
),
),
),
child: Text(
min < 10 ? "0${min.toString()}" : min.toString(),
style: TextStyle(
color: selected ? Colors.black : Colors.white,
),
),
),
);
}
}
widgets/time_title.dart
import 'package:flutter/material.dart';
class TimeTile extends StatelessWidget {
final int time;
const TimeTile({
Key? key,
required this.time,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
top: -20,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
),
),
Positioned.fill(
top: -15,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
),
),
Positioned.fill(
top: -10,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.4),
borderRadius: BorderRadius.circular(8),
),
),
),
Positioned.fill(
top: -5,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
borderRadius: BorderRadius.circular(8),
),
),
),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(48),
child: Text(
time < 10 ? "0${time.toString()}" : time.toString(),
style: const TextStyle(fontSize: 48, color: Colors.purple),
),
)
],
);
}
}