시계 어플에 있는 기능글을 구현해보고 싶어서 시작한 프로젝트이다.
특히 알람 시계를 구현해보고 싶었는데 참고할 수 있는 것이 별로 없고 너무 복잡해 일단 제외하고 완성했다.
기능은 총 두 가지로 타이머와 스톱워치로 기능은 간단하다.
각 기능 선택은 BottomNavigationBar에 클릭 시 화면이 전환된다.
Timer 기능
초기 화면으로 가장 위에 있는 시간 부분은 시간을 입력하는 부분으로 TextField로 만들었다.
이 부분에 원하는 시간을 입력하면 바로 아래 있는 회색 시계 부분에 시간이 지정된다.
-> 원래 TextField에 시간을 입력하고 TextField를 변화시켜 남은 시간을 보여주고 싶었으나 해당 기능에 대한 정보를 얻을 수 없어 시간 입력 부분과 출력 부분을 따로 나누게 되었다. 나중에 이 기능을 알게 된다면 파일을 수정하도록 하겠다.
버튼은 두 가지로 Start 버튼과 Restart 버튼이 있다.
맨 위 TextField에 시간을 입력하고 Start버튼을 누르면 회색 시계의 시간이 1초씩 감소된다.
Restart 버튼을 누르면 회색 시계의 시간이 00시 00분 00초로 리셋된다.
-> 이 부분도 마찬가지로 TextField의 내용을 변경할 수가 없어 회색 시계의 시간만 초기화가 된다.
남은 시간이 0초가 되면 타이머 실행이 종료된다.
Start 버튼을 누르면 Restart 버튼이 Stop 버튼으로 바뀌며 기존의 Start 버튼은 비활성화가 된다.
누를 수 있는 버튼이 Stop 버튼 하나만 남게 된다.
시간이 남았을 때 Stop 버튼을 누르면 시간이 멈추고 Start 버튼이 활성화되며 Stop 버튼이 Restart 버튼으로 다시 변경된다.
이 때 Start 버튼을 누르면 회색 시계의 시간이 다시 감소되며, Restart 버튼을 누르면 회색 시계의 시간이 0시 0분 0초로 초기화가 된다.
아쉬운 부분은 역시 텍스트 필드 안의 내용을 수정하는 방법을 몰라 여러 기능을 추가하지 못한 부분이다.
방법을 알게 된다면 꼭 수정해야겠다.
StopWatch 기능
가장 위에 있는 시계 부분은 시간이 표시될 부분으로 시 : 분 : 초 . 밀리세컨드 이다.
버튼은 총 세 개로 Start 버튼, Restart 버튼, Lap 버튼이다.
Start 버튼과 Restart 버튼은 Timer 기능의 버튼과 똑같고 추가된 기능은 Lap 버튼이다.
Lap 버튼은 현재 시간을 기록하는 버튼이다. 이후 화면에서 보일 것이다.
Start 버튼을 누르면 맨 위에 있는 시계의 시간이 흐르며 Restart 버튼이 Stop버튼으로 바뀌면서 기존의 Start 버튼은 비활성화가 된다.
Lap 버튼을 누르면 화면에 보이는 것처럼 Lap 버튼을 눌렀을 때의 시간이 Stack 처럼 쌓인다. 가장 오래된 것은 아래로 가고 가장 최신의 것은 맨 위에 올라간다.
Restart 버튼을 누르면 시간과 Lap 목록이 초기화되어 초기 화면으로 돌아가게 된다.
main.dart 코드
import 'package:flutter/material.dart';
import 'timer_clock.dart';
import 'stop_watch.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Clock',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blueAccent),
useMaterial3: true,
),
home: ClockPage(),
);
}
}
class ClockPage extends StatefulWidget {
const ClockPage({super.key});
@override
State<ClockPage> createState() => _ClockPageState();
}
class _ClockPageState extends State<ClockPage> {
var _index = 0;
final List<Widget> _widgetOptions = <Widget> [
TimerClock(title: 'Timer',),
StopWatch(title: 'StopWatch',),
];
void _onItemTapped(int index) {
setState(() {
_index = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _widgetOptions.elementAt(_index),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _index,
onTap: (int value) {
setState(() {
_index = value;
});
},
items: <BottomNavigationBarItem> [
BottomNavigationBarItem(icon: Icon(Icons.timer), label: 'Timer'),
BottomNavigationBarItem(icon: Icon(Icons.alarm_on), label: 'StopWatch'),
],),
);
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
}
timer_clock.dart 코드
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class TimerClock extends StatefulWidget {
const TimerClock({super.key, required this.title});
final String title;
@override
State<TimerClock> createState() => _TimerClockState();
}
class _TimerClockState extends State<TimerClock> {
int hours = 0;
int minutes = 0;
int seconds = 0;
Timer? _timer;
bool isEnabled = false;
final TextEditingController tecH = TextEditingController();
final TextEditingController tecM = TextEditingController();
final TextEditingController tecS = TextEditingController();
void startTimer() {
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
if (seconds > 0) {
seconds--;
} else {
if (minutes > 0) {
minutes--;
seconds = 59;
} else {
if (hours > 0) {
hours--;
minutes = 59;
seconds = 59;
} else {
isEnabled = false;
_timer?.cancel();
hours = 0;
minutes = 0;
seconds = 0;
}
}
}
});
});
}
void timerStart() {
setState(() {
isEnabled = true;
});
startTimer();
}
void timerStop() {
setState(() {
isEnabled = false;
});
_timer?.cancel();
}
void timerSetting() {
setState(() {
hours = hours;
minutes = minutes;
seconds = seconds;
});
}
void timerRestart() {
setState(() {
hours = 0;
minutes = 0;
seconds = 0;
});
}
String stopOrRestart(isEnabled) {
if (isEnabled) {
return "Stop";
} else {
return "Restart";
}
}
String? timeOption(int num) {
if (num < 10) {
return '0$num';
}
return '$num';
}
@override
void dispose() {
tecH.dispose();
tecM.dispose();
tecS.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 70,
margin: EdgeInsets.only(right: 20),
child: TextField(
controller: tecH,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
],
style: TextStyle(fontSize: 60),
decoration: InputDecoration(
border: InputBorder.none, hintText: '00'),
onChanged: (text) {
setState(() {
hours = int.parse(text);
timerSetting();
});
}),
),
Text(
textAlign: TextAlign.center,
style: TextStyle(fontSize: 70, color: Colors.black87),
':'),
Container(
width: 70,
margin: EdgeInsets.fromLTRB(20, 0, 20, 0),
child: TextField(
controller: tecM,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
],
style: TextStyle(fontSize: 60),
decoration: InputDecoration(
border: InputBorder.none, hintText: '00'),
onChanged: (text) {
setState(() {
minutes = int.parse(text);
timerSetting();
});
},
),
),
Text(
textAlign: TextAlign.center,
style: TextStyle(fontSize: 70, color: Colors.black87),
':'),
Container(
width: 70,
margin: EdgeInsets.only(left: 20),
child: TextField(
controller: tecS,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
],
style: TextStyle(fontSize: 60),
decoration: InputDecoration(
border: InputBorder.none, hintText: '00'),
onChanged: (text) {
setState(() {
seconds = int.parse(text);
timerSetting();
});
},
),
),
],
),
),
Text(
textAlign: TextAlign.center,
style: TextStyle(fontSize: 70, color: Colors.grey),
'${timeOption(hours)} : ${timeOption(minutes)} : ${timeOption(seconds)}'),
Container(
margin: EdgeInsets.all(10),
child: ElevatedButton(
onPressed: isEnabled ? null : timerStart,
child: Text('Start'),
style: ElevatedButton.styleFrom(
padding:
EdgeInsets.symmetric(horizontal: 20.0, vertical: 15.0),
foregroundColor: Colors.lightGreenAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
textStyle: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 30.0,
color: Colors.white,
),
),
),
),
Container(
margin: EdgeInsets.all(10),
child: ElevatedButton(
onPressed: isEnabled ? timerStop : timerRestart,
child: Text('${stopOrRestart(isEnabled)}'),
style: ElevatedButton.styleFrom(
padding:
EdgeInsets.symmetric(horizontal: 20.0, vertical: 15.0),
foregroundColor: Colors.redAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
textStyle: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 30.0,
color: Colors.white,
),
),
),
),
],
),
),
); // This trailing comma makes auto-formatting nicer for build methods.
}
}
stop_watch.dart 코드
import 'dart:async';
import 'package:flutter/material.dart';
class StopWatch extends StatefulWidget {
const StopWatch({super.key, required this.title});
final String title;
@override
State<StopWatch> createState() => _StopWatchState();
}
class _StopWatchState extends State<StopWatch> {
int hours = 0;
int minutes = 0;
int seconds = 0;
int milliseconds = 0;
Timer? _stopWatch;
bool isEnabled = false;
List<String> laps = [];
void startStopWatch() {
_stopWatch = Timer.periodic(Duration(milliseconds: 1), (stopWatch) {
setState(() {
if (milliseconds < 99) {
milliseconds++;
} else {
if (seconds < 59) {
seconds++;
milliseconds = 0;
} else {
if (minutes < 59) {
minutes++;
seconds = 0;
milliseconds = 0;
} else {
hours++;
minutes = 0;
seconds = 0;
milliseconds = 0;
}
}
}
});
});
}
String? timeOption(int num) {
if (num < 10) {
return '0$num';
}
return '$num';
}
void stopWatchStart() {
setState(() {
isEnabled = true;
});
startStopWatch();
}
void stopWatchStop() {
setState(() {
isEnabled = false;
});
_stopWatch?.cancel();
}
void stopWatchRestart() {
setState(() {
isEnabled = false;
});
hours=0;
minutes=0;
seconds=0;
milliseconds=0;
laps.clear();
_stopWatch?.cancel();
}
String stopWatchLap() {
String h = "$hours";
String m = "$minutes";
String s = "$seconds";
String ms = "$milliseconds";
if (hours < 10) {
h = "0$hours";
}
if (minutes < 10) {
m = "0$minutes";
}
if (seconds < 10) {
s = "0$seconds";
}
if (milliseconds < 10) {
ms = "0$milliseconds";
}
return "$h : $m : $s . $ms";
}
String stopOrRestart(isEnabled) {
if (isEnabled) {
return "Stop";
} else {
return "Restart";
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 90,
margin: EdgeInsets.only(right: 10),
child: Text(
textAlign: TextAlign.center,
style: TextStyle(fontSize: 70, color: Colors.black87),
'${timeOption(hours)}'),
),
Text(
textAlign: TextAlign.center,
style: TextStyle(fontSize: 70, color: Colors.black87),
':'),
Container(
width: 90,
margin: EdgeInsets.fromLTRB(10, 0, 10, 0),
child: Text(
textAlign: TextAlign.center,
style: TextStyle(fontSize: 70, color: Colors.black87),
'${timeOption(minutes)}'),
),
Text(
textAlign: TextAlign.center,
style: TextStyle(fontSize: 70, color: Colors.black87),
':'),
Container(
width: 220,
margin: EdgeInsets.only(left: 10),
child: Text(
textAlign: TextAlign.center,
style: TextStyle(fontSize: 70, color: Colors.black87),
'${timeOption(seconds)} . ${timeOption(milliseconds)}'),
),
],
),
),
Container(
margin: EdgeInsets.all(10),
child: ElevatedButton(
onPressed: isEnabled ? null : stopWatchStart,
child: Text('Start'),
style: ElevatedButton.styleFrom(
padding:
EdgeInsets.symmetric(horizontal: 20.0, vertical: 15.0),
foregroundColor: Colors.lightGreenAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
textStyle: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 30.0,
color: Colors.white,
),
),
),
),
Container(
margin: EdgeInsets.all(10),
child: ElevatedButton(
onPressed: isEnabled ? stopWatchStop: stopWatchRestart,
child: Text('${stopOrRestart(isEnabled)}'),
style: ElevatedButton.styleFrom(
padding:
EdgeInsets.symmetric(horizontal: 20.0, vertical: 15.0),
foregroundColor: Colors.redAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
textStyle: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 30.0,
color: Colors.white,
),
),
),
),
Container(
margin: EdgeInsets.all(10),
child: ElevatedButton(
onPressed: () {
isEnabled ? stopWatchLap : null;
setState(() {
laps.add(stopWatchLap());
});
},
child: Text('Lap'),
style: ElevatedButton.styleFrom(
padding:
EdgeInsets.symmetric(horizontal: 20.0, vertical: 15.0),
foregroundColor: Colors.orange,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
textStyle: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 30.0,
color: Colors.white,
),
),
),
),
Container(
height: 250,
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: Colors.grey),
),
),
child: ListView(
children: [
for (int i = laps.length - 1; i >= 0; i--)
Column(children: [
Text(
laps[i],
style: TextStyle(fontSize: 30, color: Colors.black87),
),
Divider(thickness: 1, height: 1, color: Colors.grey),
]),
],
),
),
],
),
),
); // This trailing comma makes auto-formatting nicer for build methods.
}
}
후기: 정보가 많이 없는 것인지 내가 잘 찾지 못하는 것인지 생각보다 내가 원하는 정보를 찾기가 쉽지 않아 여러 과정을 거쳐 스스로 알아내야 되는 부분이 많은 것 같다.
그래도 완성한 후에 보면 시각적으로 보기 좋진 않으나 원하는 기능이 제법 잘 구현되어 만드는 재미가 있는 것 같다.
[참고 내용]
https://fonts.google.com/icons?
Material Symbols and Icons - Google Fonts
Material Symbols are our newest icons consolidating over 2,500 glyphs in a single font file with a wide range of design variants.
fonts.google.com
flutter icon 페이지
https://kante-kante.tistory.com/31
[Flutter] Flutter - Bottom Navigation Bar 만들기
이전에 만들었던 플러터 앱 중에서 bottom navigation bar를 만들었던 것에 대해 작성해보려 한다. bottom navigation bar를 만드는 방법에는 다양한 방법이 있으나 본인은 BottomNavigationBar 위젯을 사용하여
kante-kante.tistory.com
bottomNabigationBar 참고 블로그
'개인 프로젝트' 카테고리의 다른 글
기한 관리 어플 개인 프로젝트(Dart, Flutter) (0) | 2025.02.16 |
---|---|
색상 선택기 개인 프로젝트 (Dart, Flutter) (0) | 2025.01.30 |
계산기 개인 프로젝트 (Dart, Flutter) (1) | 2025.01.23 |