외로운 Nova의 작업실

Flutter 프로그래밍 - 7(디지털 주사위) 본문

Programming/Flutter

Flutter 프로그래밍 - 7(디지털 주사위)

Nova_ 2024. 1. 13. 18:38

이번 장에서는 디지털 주사위를 만들어보겠습니다.

 

- 사전 지식

가속도계란 말 그대로 물체가 특정 방향으로 이동하는 가속도가 어느 정도 인지를 숫자로 측정하는 기기입니다. 대부분의 핸드폰에 가속도계가 장착 되어있으며 좌우는 x축, 위아래는 y축, 앞뒤로는 z축을 의미합니다.

 

자이로스코프는 가속도계의 단점인 회전 미측정이라는 점을 극복하기위해서 탄생했습니다. x축은 좌우로 회전하는 방향이며 y축은 위아래로 회전하는 방향, z축은 앞뒤로 회전하는 방향을 가르키게되며 회전값을 얻어낼 수 있습니다.

 

<Sensor_Plus 패키지>

Sensor_Plus 패키지를 사용하면 핸드폰의 가속도계와 자이로스코프 센서를 간단하게 사용할 수 있습니다. 전반적인 핸드폰의 움직임을 측정하려면 정규화가 필요한데, 이부분은 shake 패키지를 이용합니다. 

 

- 사전 준비

프로젝트 이름 : random_dice

 

<상수 준비>

프로그래밍을 하다보면 반복적으로 사용하는 상수들을 접합니다. 따라서 이는 파일에 별도로 정리해두는 것이 좋습니다.

lib>const>colors.dart파일을 생성하고 아래와 같이 작성해주겠습니다.

import 'package:flutter/material.dart';

const backgroundColor = Color(0xFF0E0E0E);  // 배경색

const primaryColor = Colors.white;  // 주 색상

final secondaryColor = Colors.grey[600];  // 보조 색상

 

<이미지 추가하기>

이제 asset 폴더를 만들고 img 폴더를 만들어서 img들을 넣어두겠습니다.

각 이미지는 주사위의 사진입니다. 이제 pubspec.yaml을 설정해주겠습니다.

name: random_dice
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1

environment:
  sdk: '>=3.2.3 <4.0.0'

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter
  
      


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  shake: 2.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^2.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true
  
  assets:
    - asset/img/

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

 

- 프로젝트 초기화

이제 lib 폴더에 screen 폴더를 생성하고 home_screen.dart를 생성합니다.

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget{
  const HomeScreen({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context){
    return Scaffold(
      body: Text('Home Screen'),
    )
  }
}

 

이제 main.dart 파일에 홈 위젯으로 등록해줍니다.

import 'package:flutter/material.dart';
import 'package:random_dice/screen/home_screen.dart';

void main() {
  runApp(
    MaterialApp(
      home: HomeScreen(),
    )
  );
}

 

- Theme 설정

저번 장에서 만든 상수를 활용해서 테마를 만들도록 하겠습니다. 아래는 main.dart 파일입니다.

import 'package:flutter/material.dart';
import 'package:random_dice/screen/home_screen.dart';
import 'package:random_dice/const/colors.dart';

void main() {
  runApp(
    MaterialApp(debugShowCheckedModeBanner: false,
      theme: ThemeData(
        scaffoldBackgroundColor: backgroundColor,
        sliderTheme: SliderThemeData(  // Slider 위젯 관련
          thumbColor: primaryColor,    // 동그라미 색
          activeTrackColor: primaryColor,  // 이동한 트랙 색

          // 아직 이동하지 않은 트랙 색
          inactiveTrackColor: primaryColor.withOpacity(0.3),         ),
        // BottomNavigationBar 위젯 관련
        bottomNavigationBarTheme: BottomNavigationBarThemeData(
          selectedItemColor: primaryColor,     // 선택 상태 색
          unselectedItemColor: secondaryColor, // 비선택 상태 색
          backgroundColor: backgroundColor,    // 배경 색
        ),
      ),
      home: HomeScreen(),
    )
  );
}

 

- 레이아웃 구상

이번 프로젝트는 BottomNavigatorBar 위젯을 사용해서 화면 전환을 해야하기때문에 지금까지와는 다른 형태로 구조를 짜겠습니다. 첫번째화면인 HomeScreen 위젯과 두번째 화면인 SettingsScreen을 TabBarView를 이용해서 RootScreen 위젯에 위치시키겠습니다. RootScreen하나에 홈스크린과 설정 스크린이 담겨져있고 TabBarView(PageView와 비슷)를 이용해서 좌우로 스크롤시 전환할 수 있게 만들겠습니다.

 

- RootScreen 위젯 구현

screen 폴더밑에 RootScreen을 만들겠습니다.

import 'package:flutter/material.dart';

class RootScreen extends StatelessWidget{
  const RootScreen({Key? key}) : super(key : key);

  @override
  Widget build(BuildContext context){
    return Scaffold(
      body: TabBarView(
        children: renderChildren(),
      ),
      
      bottomNavigationBar: renderBottomNevigation(),
    );
  }

  List<Widget> renderChildren(){
    return [];
  }
  
  BottomNavigationBar renderBottomNevigation(){
    return BottomNavigationBar(items: []);
  }
}

 

이제 main.dart파일에 home_screen이 아닌 root_screen으로 home 설정을 변경해줍니다. 아래는 main.dart 파일입니다.

import 'package:flutter/material.dart';
import 'package:random_dice/screen/home_screen.dart';
import 'package:random_dice/const/colors.dart';
import 'package:random_dice/screen/root_screen.dart';

void main() {
  runApp(
    MaterialApp(debugShowCheckedModeBanner: false,
      theme: ThemeData(
        scaffoldBackgroundColor: backgroundColor,
        sliderTheme: SliderThemeData(  // Slider 위젯 관련
          thumbColor: primaryColor,    // 동그라미 색
          activeTrackColor: primaryColor,  // 이동한 트랙 색

          // 아직 이동하지 않은 트랙 색
          inactiveTrackColor: primaryColor.withOpacity(0.3),         ),
        // BottomNavigationBar 위젯 관련
        bottomNavigationBarTheme: BottomNavigationBarThemeData(
          selectedItemColor: primaryColor,     // 선택 상태 색
          unselectedItemColor: secondaryColor, // 비선택 상태 색
          backgroundColor: backgroundColor,    // 배경 색
        ),
      ),
      home: RootScreen(),
    )
  );
}

 

이제 TabBarView부터 작업하겠습니다. TabBarView는 TabController가 필수 입니다. 추가적으로 TabController를 초기화하려면 vsync 기능이 피룡한데 이는 State 위젯의 TickerProviderMixin을 mixin으로 제공해줘야 할 수 있습니다. TabController는 위젯이 생성될때 단 한번만 초기화 되어야하니 HomeScreen 위젯을 Stateful Widget으로 변경하고 initState()에서 초기화하겠습니다.

 

쉽게 TabBarView 컨트롤러를 사용하려면 Statefulwidget이여야되서 이렇게 합니다.

import 'package:flutter/material.dart';

class RootScreen extends StatefulWidget{
  const RootScreen({Key? key}) : super(key : key);

  @override
  State<RootScreen> createState() => _RootScreenState();
}

class _RootScreenState extends State<RootScreen> with
TickerProviderStateMixin{
  TabController? controller;
  
  @override
  void initState() {
    super.initState();
    
    controller = TabController(length: 2, vsync: this); //컨트롤러 초기화
  }

  @override
  Widget build(BuildContext context){
    return Scaffold(
      body: TabBarView(
        controller: controller, //컨트롤러 등록
        children: renderChildren(),
      ),

      bottomNavigationBar: renderBottomNevigation(),
    );
  }

  List<Widget> renderChildren(){
    return [];
  }

  BottomNavigationBar renderBottomNevigation(){
    return BottomNavigationBar(items: []);
  }
}

 

이제 BottomNavigationBar를 작업하겠습니다. items 매개변수에는 BottomNavigationBarItem이라는 클래스를 사용해서 각 탭의 정의를 제공해주고 icon 매개변수와 label 매개변수를 이용해서 구현할 두 탭과 예시로 TabBarView의 스크린을 작어하겠습니다.

import 'package:flutter/material.dart';

class RootScreen extends StatefulWidget{
  const RootScreen({Key? key}) : super(key : key);

  @override
  State<RootScreen> createState() => _RootScreenState();
}

class _RootScreenState extends State<RootScreen> with
TickerProviderStateMixin{
  TabController? controller;

  @override
  void initState() {
    super.initState();

    controller = TabController(length: 2, vsync: this); //컨트롤러 초기화
  }

  @override
  Widget build(BuildContext context){
    return Scaffold(
      body: TabBarView(
        controller: controller, //컨트롤러 등록
        children: renderChildren(),
      ),

      bottomNavigationBar: renderBottomNevigation(),
    );
  }

  List<Widget> renderChildren(){
    return [
      Container( //홈탭
        child: Center(
          child: Text(
            'Tab 1',
            style: TextStyle(
              color: Colors.white
            ),
          ),
        ),
      ),
      Container( // 설정 스크린 탭
        child: Center(
          child: Text(
            'Tab 2',
            style: TextStyle(
                color: Colors.white
            ),
          ),
        ),
      )
    ];
  }

  BottomNavigationBar renderBottomNevigation(){
    return BottomNavigationBar(items: [
      BottomNavigationBarItem(icon: Icon(
        Icons.edgesensor_high_outlined,
      ),
      label: '주사위'),
      BottomNavigationBarItem(icon: Icon(
        Icons.settings
      ),
      label: '설정'),
    ],);
  }
}

 

한번 실행시켜보겠습니다.

 

스크롤이 잘됩니다. 하지만 스크롤을 하거나 네비게이션 바를 설정을 눌러도 화면이 변하지 않습니다. 이 둘을 컨트롤러로 연동시켜보겠습니다.

import 'package:flutter/material.dart';

class RootScreen extends StatefulWidget{
  const RootScreen({Key? key}) : super(key : key);

  @override
  State<RootScreen> createState() => _RootScreenState();
}

class _RootScreenState extends State<RootScreen> with
TickerProviderStateMixin{
  TabController? controller;

  @override
  void initState() {
    super.initState();

    controller = TabController(length: 2, vsync: this); //컨트롤러 초기화

    controller!.addListener((tabListener)); //컨틀롤러 속성 변경시 실행할 함수 등록
  }

  tabListener(){ //리스너로 사용할 함수
    setState(() {
    });
  }

  @override
  void dispose() { //위젯 삭제시 실행되는 함수
    controller!.removeListener(tabListener); //리스너에 등록한 함수 등록 취소
    super.dispose();
  }

  @override
  Widget build(BuildContext context){
    return Scaffold(
      body: TabBarView(
        controller: controller, //컨트롤러 등록
        children: renderChildren(),
      ),

      bottomNavigationBar: renderBottomNevigation(),
    );
  }

  List<Widget> renderChildren(){
    return [
      Container( //홈탭
        child: Center(
          child: Text(
            'Tab 1',
            style: TextStyle(
              color: Colors.white
            ),
          ),
        ),
      ),
      Container( // 설정 스크린 탭
        child: Center(
          child: Text(
            'Tab 2',
            style: TextStyle(
                color: Colors.white
            ),
          ),
        ),
      )
    ];
  }

  BottomNavigationBar renderBottomNevigation(){
    return BottomNavigationBar(
      currentIndex: controller!.index, //현재 탭의 인덱스로 변환(스크롤시 변동)
      onTap: (int index){
        setState(() {
          controller!.animateTo(index); //탭이 눌리면 컨트롤러로 화면 전환
        });
      },
    items: [
      BottomNavigationBarItem(icon: Icon(
        Icons.edgesensor_high_outlined,
      ),
      label: '주사위'),
      BottomNavigationBarItem(icon: Icon(
        Icons.settings
      ),
      label: '설정'),
    ],);
  }
}

 

한번 실행시켜보겠습니다.

 

스크롤시 네비게이션과 연동됨을 알 수 있습니다.

 

- HomeScreen 위젯 구현

이제 tab1이 아닌 주사위와 주사위 값, 행운의 숫자라는 글씨가 들어가게 해보겠습니다. 아래는 HomeScreen.dart 파일입니다.

import 'package:flutter/material.dart';
import 'package:random_dice/const/colors.dart';

class HomeScreen extends StatelessWidget{

  final int number;
  const HomeScreen({required this.number, Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context){
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        //주사위 이미지
        Center(
        child: Image.asset('asset/img/$number.png'),
        ),
        SizedBox(height: 32.0,),
        Text(
          '행운의 숫자',
          style: TextStyle(
            color: secondaryColor,
            fontSize: 20.0,
            fontWeight: FontWeight.w700
          )
        ),
        SizedBox(height: 12.0),
        Text(
          number.toString(),
          style: TextStyle(
            color: primaryColor,
            fontSize: 60.0,
            fontWeight: FontWeight.w200
          ),
        )

      ],
    );
  }
}

 

이제 이걸 Root_Screen에 적용합니다.

List<Widget> renderChildren(){
    return [
      HomeScreen(number: 1),
      Container( // 설정 스크린 탭
        child: Center(
          child: Text(
            'Tab 2',
            style: TextStyle(
                color: Colors.white
            ),
          ),
        ),
      )
    ];
  }

한번 봐보겠습니다.

잘 나오는것을 확인할 수 있습니다.

 

- SettingsScreen 위젯 구현

이제 설정 위젯을 만들 차례입니다. /lib/screen/settings_screen.dart 파일을 생성하겠습니다. 해당 위젯은 Text 위젯과 Slider 위젯으로 이루어져있습니다. 가운데에 세로로 배치하겠습니다.

import 'package:flutter/material.dart';
import 'package:random_dice/const/colors.dart';

class SettingsScreen extends StatelessWidget{
  final double threshold; //slider의 현재값

  //slider가 변경될때마다 실행 되는 함수
  final ValueChanged<double> onThresholdChange;

  const SettingsScreen({Key? key,
    required this.threshold,
    required this.onThresholdChange,
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context){
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Padding(padding: const EdgeInsets.only(left: 20.0),
        child: Row(
          children: [
            Text('민감도',
            style: TextStyle(
              color: secondaryColor,
              fontSize: 20.0,
              fontWeight: FontWeight.w700
            ),)
          ],
        ),),
        Slider(
          min: 0.1,//최솟값
          max: 10.0,//최댓값
          divisions: 101,//최소와 최대 사이의 구간 개수
          value: threshold,//슬라이더 선택값
          onChanged: onThresholdChange,//값변경시 실행될 함수
          label: threshold.toStringAsFixed(1),//표싯값
        )
      ],
    );
  }
}

이제 이것을 rootscreen에 적용시켜주겠습니다.

import 'package:flutter/material.dart';
import 'package:random_dice/screen/home_screen.dart';
import 'package:random_dice/screen/settings_screen.dart';

class RootScreen extends StatefulWidget{
  const RootScreen({Key? key}) : super(key : key);

  @override
  State<RootScreen> createState() => _RootScreenState();
}

class _RootScreenState extends State<RootScreen> with
TickerProviderStateMixin{
  TabController? controller;
  double threshold = 2.7; //민감도 기본 설정

  @override
  void initState() {
    super.initState();

    controller = TabController(length: 2, vsync: this); //컨트롤러 초기화

    controller!.addListener((tabListener)); //컨틀롤러 속성 변경시 실행할 함수 등록
  }

  tabListener(){ //리스너로 사용할 함수
    setState(() {
    });
  }

  @override
  void dispose() { //위젯 삭제시 실행되는 함수
    controller!.removeListener(tabListener); //리스너에 등록한 함수 등록 취소
    super.dispose();
  }

  @override
  Widget build(BuildContext context){
    return Scaffold(
      body: TabBarView(
        controller: controller, //컨트롤러 등록
        children: renderChildren(),
      ),

      bottomNavigationBar: renderBottomNevigation(),
    );
  }

  List<Widget> renderChildren(){
    return [
      HomeScreen(number: 1),
      SettingsScreen(threshold: threshold, onThresholdChange: onThresholdChange)
    ];
  }
  
  void onThresholdChange(double val){
    setState(() {
      threshold = val;
    });
  }

  BottomNavigationBar renderBottomNevigation(){
    return BottomNavigationBar(
      currentIndex: controller!.index, //현재 탭의 인덱스로 변환(스크롤시 변동)
      onTap: (int index){
        setState(() {
          controller!.animateTo(index); //탭이 눌리면 컨트롤러로 화면 전환
        });
      },
    items: [
      BottomNavigationBarItem(icon: Icon(
        Icons.edgesensor_high_outlined,
      ),
      label: '주사위'),
      BottomNavigationBarItem(icon: Icon(
        Icons.settings
      ),
      label: '설정'),
    ],);
  }
}

한번 실행시켜보겠습니다.

잘 된것을 확인할 수 잇습니다. 원위에 가끔 값이 표시되는데 이는 label 매개변수입니다.

 

- Shake 플러그인 적용

이제 핸드폰을 흔들면 Shake 플러그인으로 흔들기를 감지하고 이후 랜덤으로 1~6까지 값을 만들어서 number에 넣어주겠습니다. 이는 rootscreen.dart 파일에서 감지하고 변수를 변경하고 setState()하는 방법으로 가겠습니다.

import 'package:flutter/material.dart';
import 'package:random_dice/screen/home_screen.dart';
import 'package:random_dice/screen/settings_screen.dart';
import 'dart:math';
import 'package:shake/shake.dart';


class RootScreen extends StatefulWidget{
  const RootScreen({Key? key}) : super(key : key);

  @override
  State<RootScreen> createState() => _RootScreenState();
}

class _RootScreenState extends State<RootScreen> with
TickerProviderStateMixin{
  TabController? controller;
  double threshold = 2.7; //민감도 기본 설정
  int number = 1; //주사위 초기 숫자
  ShakeDetector? shakeDetector; //흔들기 감지 함수

  @override
  void initState() {
    super.initState();

    controller = TabController(length: 2, vsync: this); //컨트롤러 초기화

    controller!.addListener((tabListener)); //컨틀롤러 속성 변경시 실행할 함수 등록

    shakeDetector = ShakeDetector.autoStart(
      shakeSlopTimeMS: 100,//감지 주기
      shakeThresholdGravity: threshold, //감지 민감도
      onPhoneShake: onPhoneShake, //감지 후 실행할 함수
    );
  }

  void onPhoneShake(){ //number을 고치고 setState함수 실행
    final rand = new Random();

    setState(() {
      number = rand.nextInt(5)+ 1;
    });
  }

  tabListener(){ //리스너로 사용할 함수
    setState(() {
    });
  }

  @override
  void dispose() { //위젯 삭제시 실행되는 함수
    controller!.removeListener(tabListener); //리스너에 등록한 함수 등록 취소
    shakeDetector!.stopListening(); //흔들기 감지 중지
    super.dispose();
  }

  @override
  Widget build(BuildContext context){
    return Scaffold(
      body: TabBarView(
        controller: controller, //컨트롤러 등록
        children: renderChildren(),
      ),

      bottomNavigationBar: renderBottomNevigation(),
    );
  }

  List<Widget> renderChildren(){
    return [
      HomeScreen(number: number)// 넘버로 변경,
      SettingsScreen(threshold: threshold, onThresholdChange: onThresholdChange)
    ];
  }

  void onThresholdChange(double val){
    setState(() {
      threshold = val;
    });
  }

  BottomNavigationBar renderBottomNevigation(){
    return BottomNavigationBar(
      currentIndex: controller!.index, //현재 탭의 인덱스로 변환(스크롤시 변동)
      onTap: (int index){
        setState(() {
          controller!.animateTo(index); //탭이 눌리면 컨트롤러로 화면 전환
        });
      },
    items: [
      BottomNavigationBarItem(icon: Icon(
        Icons.edgesensor_high_outlined,
      ),
      label: '주사위'),
      BottomNavigationBarItem(icon: Icon(
        Icons.settings
      ),
      label: '설정'),
    ],);
  }
}

한번 실행해보겠습니다.

흔들었더니 4로 변경되었습니다.

 

Comments