본문 바로가기
PROJECT

[Flutter] 내기에서 지지 않는 APP - 메인화면 및 설정 창 만들기(1)

by Nuridal_class 2024. 11. 15.
728x90
728x90

프로젝트 사이에 시간이 비어서 뭘 해볼까 하는 생각에 내기할때 내가 무조껀 이긴다면??

하는 막연한 생각에 만들어볼까? 해서 시작한 자그마한 어플개발입니다.
(※ 전체 코드는 마지막에 있습니다)

🏷️ 먼저 개발에 사용되었던 버전 정보들입니다.
FVM version: 3.2.1
Flutter version: 3.13.9
Dart version: 3.1.5
Git version: 2.47.0.windows.2
Java version: OpenJDK 11 2018-09-25

1. 기본 구조

🧑‍💻main() 함수에서 MyApp 위젯을 실행
- MyApp은 앱의 테마와 홈 화면을 설정하는 기본 구조를 제공
void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  // MaterialApp을 반환하는 기본 앱 구조
}

 

2. 홈 화면 구현

 2.1 애니매이션 컨트롤러

🧑‍💻터치 시 나타나는 리플 효과를 위한 애니메이션 설정(300ms 동안 실행되는 애니메이션)
late AnimationController _animationController;
late Animation<double> _animation;

@override
void initState() {
  _animationController = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 300),
  );
  _animation = Tween<double>(begin: 0, end: 1).animate(
    CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
  );
}

 

 2.2 터치 이벤트 처리

🧑‍💻터치 위치를 저장하고 애니메이션을 시작
void _handleTapDown(TapDownDetails details) async {
  setState(() {
    _showRipple = true;
    _tapPosition = details.localPosition;
  });
  _animationController.forward();
}

 

3. UI 구성

 3.1 메인화면 구성

🧑‍💻민트색 배경의 메인 화면
- 중앙에 "Betting Start!" 텍스트 표시
- 터치 시 리플 효과 표시
Scaffold(
  backgroundColor: const Color(0xFF98E4D8),  // 민트색 배경
  body: GestureDetector(
    // 터치 이벤트 처리
    child: Stack(
      children: [
        // 중앙 텍스트
        // 리플 효과
      ],
    ),
  ),
)

 

 3.2 설정버튼( FloatingActionButton)

🧑‍💻 작은 크기의 흰색 플로팅 버튼
- 터치 시 설정 모달을 표시
floatingActionButton: SizedBox(
  width: 25,
  height: 25,
  child: FloatingActionButton(
    onPressed: () async {
      // 설정 모달 표시
    },
  ),
)

 

 3.3 설정모달

🧑‍💻바텀 시트 형태의 설정 모달
- 다크 테마 적용 (배경색: #1C1C1E)
- 앱 버전 정보 표시 (package_info_plus 사용)
showModalBottomSheet(
  // 설정 화면 UI
  child: Column(
    children: [
      // 헤더 ('설정' 텍스트와 '완료' 버튼)
      // 앱 버전 정보
    ],
  ),
)

 

2. 실행화면

앱 첫번째 커밋 실행화면

더보기
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Bet',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF98E4D8),
        ),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Bet'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  // final int _betAmount = 0;
  bool _showRipple = false;
  Offset _tapPosition = Offset.zero;
  late AnimationController _animationController;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
    _animation = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _handleTapDown(TapDownDetails details) async {
    setState(() {
      _showRipple = true;
      _tapPosition = details.localPosition;
    });
    // await Vibration.vibrate(duration: 50);
    _animationController.forward();
  }

  void _handleTapUp(TapUpDetails details) {
    _animationController.reverse().then((_) {
      setState(() {
        _showRipple = false;
      });
    });
  }

  void _handleTapCancel() {
    _animationController.reverse().then((_) {
      setState(() {
        _showRipple = false;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF98E4D8),
      body: GestureDetector(
        onTapDown: _handleTapDown,
        onTapUp: _handleTapUp,
        onTapCancel: _handleTapCancel,
        child: Stack(
          children: [
            const Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Text(
                    'Betting Start!',
                    style: TextStyle(
                      fontSize: 32,
                      fontWeight: FontWeight.bold,
                      color: Colors.white,
                    ),
                  ),
                ],
              ),
            ),
            if (_showRipple)
              Positioned(
                left: _tapPosition.dx - 25,
                top: _tapPosition.dy - 25,
                child: AnimatedBuilder(
                  animation: _animation,
                  builder: (context, child) {
                    return Container(
                      width: 50 * _animation.value,
                      height: 50 * _animation.value,
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: Colors.white.withOpacity(0.3),
                        border: Border.all(
                          color: Colors.white,
                          width: 2,
                        ),
                      ),
                    );
                  },
                ),
              ),
          ],
        ),
      ),
      floatingActionButton: SizedBox(
        width: 25,
        height: 25,
        child: FloatingActionButton(
          onPressed: () async {
            final packageInfo = await PackageInfo.fromPlatform();

            showModalBottomSheet(
              context: context,
              isScrollControlled: true,
              builder: (BuildContext context) {
                return FractionallySizedBox(
                  heightFactor: 0.9,
                  widthFactor: 1.0,
                  child: Container(
                    width: double.infinity,
                    padding: const EdgeInsets.all(20),
                    decoration: const BoxDecoration(
                      color: Color(0xFF1C1C1E),
                      borderRadius: BorderRadius.only(
                        topLeft: Radius.circular(20),
                        topRight: Radius.circular(20),
                      ),
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Center(
                          child: Container(
                            width: 40,
                            height: 4,
                            margin: const EdgeInsets.only(bottom: 20),
                            decoration: BoxDecoration(
                              color: Colors.grey[600],
                              borderRadius: BorderRadius.circular(2),
                            ),
                          ),
                        ),
                        Stack(
                          alignment: Alignment.center,
                          children: [
                            Container(
                              width: double.infinity,
                              padding: const EdgeInsets.symmetric(
                                vertical: 12,
                              ),
                              decoration: const BoxDecoration(
                                border: Border(
                                  bottom: BorderSide(
                                    color: Color(0xFF3C3C3E),
                                    width: 1,
                                  ),
                                ),
                              ),
                              child: const Center(
                                child: Text(
                                  '설정',
                                  style: TextStyle(
                                    fontSize: 20,
                                    fontWeight: FontWeight.bold,
                                    color: Colors.white,
                                  ),
                                ),
                              ),
                            ),
                            Positioned(
                              right: 0,
                              child: TextButton(
                                onPressed: () {
                                  Navigator.pop(context);
                                },
                                child: const Text(
                                  '완료',
                                  style: TextStyle(
                                    fontSize: 16,
                                    fontWeight: FontWeight.w500,
                                    color: Color(0xFF0A84FF),
                                  ),
                                ),
                              ),
                            ),
                          ],
                        ),
                        const SizedBox(height: 30),
                        const Text(
                          '앱 정보',
                          style: TextStyle(
                            fontSize: 16,
                            fontWeight: FontWeight.bold,
                            color: Colors.white,
                          ),
                        ),
                        const SizedBox(height: 15),
                        Container(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 16,
                            vertical: 12,
                          ),
                          decoration: BoxDecoration(
                            color: const Color(0xFF2C2C2E),
                            borderRadius: BorderRadius.circular(10),
                          ),
                          child: Row(
                            children: [
                              const Icon(
                                Icons.info_outline,
                                color: Colors.white,
                                size: 20,
                              ),
                              const SizedBox(width: 12),
                              Text(
                                '버전 ${packageInfo.version}',
                                style: const TextStyle(
                                  color: Colors.white,
                                  fontSize: 16,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                );
              },
              shape: const RoundedRectangleBorder(
                borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
              ),
            );
          },
          backgroundColor: Colors.white,
          elevation: 2,
          shape: const CircleBorder(),
          child: Container(),
        ),
      ),
    );
  }
}

재미로 만들어 보는 앱 개발 일지 첫번쨰 장입니다.

완료되는 그날까지!!

728x90
300x250