디지털 주사위 (가속도계, 자이로스코프, Sensor_Plus 패키지 활용)

잠만보·2024년 8월 24일

사전지식

가속도계

특정 물체가 특정 방향으로 이동하는 가속도가 어느 정도인지를 숫자로 측정하는 기기.
3개의 축으로 가속도를 측정할 수 있다.

사람은 기계가 아니기 때문에 하나의 축으로만 핸드폰을 움직이는 것은 불가능하다.
가속도계를 사용해서 움직임 이벤트를 받으면 x, y, z 축의 측정 결과가 모두 double 값으로 반환된다.

자이로스코프

가속도계는 직선 움직임만 측정할 수 있다.
자이로스코프는 회전 움직임을 측정한다.

가속도계와 마찬가지로 자이로스코프도 회전에 대한 이벤트를 받게 되면 x, y, z 축 모두에서의 회전값이 동시에 반환된다.

Sensor_Plus 패키지

Sensor_Plus 패키지를 사용하면 핸드폰의 가속계와 자이로스코프 센서를 간단하게 사용할 수 있다.
다만, x, y, z 축의 각 값을 통합해 전반적인 움직임 수치를 계산해서 핸드폰을 흔든 정도를 수치화 해야 하는 정규화 작업이 필요하다.
우리는 미리 정규화 작업을 해둔 Shake 패키지를 이용하겠다.

터미널에 해당 명령어를 입력해서 패키지를 설치하자

flutter pub add sensors_plus

이후 pubspec.yaml 파일의 dependencies 부분에 sensors_plus: ^6.0.1 버젼이 있는지 확인한다.

dependencies:
  flutter:
    sdk: flutter
  sensors_plus: ^6.0.1

이후 패키지를 사용할 때 import 해주자

import 'package:sensors_plus/sensors_plus.dart';

사전 준비

상수 추가하기

프로그래밍을 하다 보면 반복적으로 사용하는 상수들이 있다.(글자 크기, 색깔 등등...)
이런 값들을 나중에 바꾸려고 하면 하나하나 바꿔야 하는 귀찮은 일이 발생할 수 있다.
따라서 반복적으로 이용하는 상수들을 별도의 파일에 정리해두는것이 좋다.

lib 폴더 하위에 const 라는 폴더를 만들고 모든 색상 상수값들을 저장할 colors.dart 파일을 생성한다.

import 'package:flutter/material.dart';

const backgroundColor = Color(0xFF0E0E0E); // 배경색
const primaryColor = Colors.white; // 주 색상
final secondaryColor = Colors.grey[600]; // 보조 색상, 600 이라는 키 값을 입력하면 런타임에 색상이 계산되기 때문에 const가 아니라 final로 상수를 선언한다.

이미지 추가하기

asset 폴더를 만들고 하위에 img 폴더를 만든다. 그리고 주사위 이미지 6개를 추가해준다.

pubspec.yaml 설정하기

이미지 경로를 추가해준다.

  assets:
    - asset/img/

프로젝트 초기화

앱의 기본 홈 화면으로 사용할 HomeScreen 위젯을 생성할 home_screen.dart 파일을 생성한다.

Theme 설정

상수를 사용해서 테마를 적용하겠다.

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

void main() {
  runApp(
    MaterialApp(
      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(),
    ),
  );
}

레이아웃 구상

기본(Root) 스크린 위젯

  1. TabBarView를 통해 선택된 화면을 보여준다.

  2. BottomNavigatorBar 에서 각 탭(주사위, 설정)을 누르거나 TabBarView에서 좌우로 스크롤을 해서 화면을 전환할 수 있다.

홈 스크린 위젯

  1. 주사위 이미지를 보여줄 image 위젯이 있다.

  2. 글자와 주사위 눈 숫자를 보여줄 Text 위젯이 두개가 있다.

설정 스크린 위젯

세팅 버튼을 누르면 설정 스크린 화면으로 이동한다.

Slider 위젯을 사용해서 흔들기 기능의 민감도를 정하도록 한다.

구현하기

RootScreen 위젯 구현하기

BottomNavigationBar 을 아래에 위치시키고 남는 공간에 TabBarView를 위치시켜서 스크린 전환이 가능한 구조를 만들겠다.

1. lib/screen 폴더에 root_screen.dart 파일 만들기

2. renderChildren() renderBottomNavigation() 함수 작업

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: TabBarView( // 1. 탭 화면을 보여줄 위젯
        children: renderChildren(),
      ),
      bottomNavigationBar: renderBottomNavigation(), // 2. 아래 탭 네비게이션을 구현하는 매개변수
    );
  }
    List<Widget> renderChildren() {
    return [];
  }

  BottomNavigationBar renderBottomNavigation() {
    return BottomNavigationBar(items: []); // 3. 탭 네비게이션을 구현하는 위젯
  }
}
  1. TabBarView 위젯PageView와 매우 비슷한 기본 애니메이션이 제공되며 children 매개변수에 각 탭의 화면으로 활용하고 싶은 위젯을 List 로 넣어주면 된다.

  2. Scaffold 위젯BottomNavigationBar 을 위치시키는 매개변수(파라미터)를 따로 보유한다.
    bottomNavigation 매개변수BottomNavigationBar 를 넣어주면 쉽게 Tab을 조정할 수 있는 UI를 핸드폰의 아래에 배치할 수 있다.

  3. BottomNavigationBar 에 제공될 각 탭(주사위, 세팅 버튼)은 BottomNavigationBar 위젯의 items 매개변수 에 제공하면 된다.

TabBarView 작업하기

  • TabBarViewTabController 가 필수이다.

  • TabController 를 초기화 하려면 vsync 기능이 필요한데, State 위젯TickerProviderMixinmixin 으로 제공해줘야 사용할 수 있다.

  • TabController 는 위젯이 생성될 때 단 한번만 초기화되어야 하니 HomeScreen 위젯StatefulWidget 으로 변경하고 initState() 에서 초기화 하겠다.

import 'package:flutter/material.dart';

class RootScreen extends StatefulWidget {
  const RootScreen({super.key});

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

class _RootScreenState extends State<RootScreen> with TickerProviderStateMixin { // 1. TickerProviderStateMixin 사용
  TabController? controller; // 사용할 TabController 선언

  @override
  void initState() {
    super.initState();
    controller = TabController(length: 2, vsync: this); // 2. TabController 초기화하기
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: TabBarView(
        controller: controller, // 3. TabController 등록하기
        children: renderChildren(),
      ),
      bottomNavigationBar: renderBottomNavigation(),
    );
  }
    List<Widget> renderChildren() {
    return [];
  }

  BottomNavigationBar renderBottomNavigation() {
    return BottomNavigationBar(items: []);
  }
}
  1. TabController 에서 vsync 기능을 사용하려면 TickerProviderStateMixin 을 필수로 사용해야 한다.
    TickerProviderMixinSingleTickerProviderMixin 은 애니메이션의 효율을 높여주는 역할을 한다.

  2. TabControllerlength 매개변수에는 사용할 탭의 개수를 int 값으로 제공해주고, vsync 에는 TickerProviderMixin을 사용하는 State 클래스를 this 형태로 넣어주면 된다.

  3. 생성된 TabControllerTabBarViewcontroller 매개변수 에 입력해주면 된다.
    이제 입력된 TabController 를 이용해서 TabBarView 를 조작할 수 있다.

TickerProviderMixin과 vsync
플러터는 기기가 지원하는 대로 60 ~ 120 FPS(초당 프레임) 을 지원하는데 TickerProviderMixin을 사용하면 정확히 한 틱(1FPS) 마다 애니메이션을 실행한다.
간혹 애니메이션 코드를 실행하면 실제로 화면에 렌더링할 수 있는 주기보다 더 자주 렌더링을 하는 때가 있는데 이 때 TickerProviderMixin 을 사용하면 비효율적으로 렌더링을 더 하는 상황을 막아준다.
TabController 도 마찬가지로 vsync 에 TickerProviderMixin 을 제공함으로써 렌더링 효율을 극대화 할 수 있다.

BottomNavigationBar 작업하기

BottomNavigationBar renderBottomNavigation() {
  return BottomNavigationBar(
    items: [
      BottomNavigationBarItem( // 1. 하단 탭바의 각 버튼을 구현
        icon: Icon(
          Icons.edgesensor_high_outlined,
        ),
        label: '주사위', // 주사위 버튼
      ),
      BottomNavigationBarItem(
        icon: Icon(
          Icons.settings,
        ),
        label: '설정', // 설정 버튼
      ),
    ],
  );
}

BottomNavigationBarItem 클래스에는 아이템아이콘을 지정할 수 있는 icon 매개변수와, 아이템의이름을 지정할 수 있는 label 매개변수를 제공할 수 있다.

즉, (아이콘, 라벨) 묶음으로 버튼을 만들 수 있다.

각 탭의 위젯 구현하기

각 탭을 표현해줄 위젯들을 TabBarViewchildren에 제공해줘야 한다.
일단 Tab1,2 로 만든 뒤 세부사항은 뒤에서 구현하겠다.

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와 TabBarView 연동하기

import 'package:flutter/material.dart';

class RootScreen extends StatefulWidget {
  const RootScreen({super.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); // 1. Controller 속서이 변경될 때 마다 실행할 함수 등록
  }

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

  @override
  void dispose() {
    controller!.removeListener(tabListener); // 3. 리스너에 등록한 함수 등록 취소
    super.dispose();
  }

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

  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 renderBottomNavigation() {
    return BottomNavigationBar(
      currentIndex: controller!.index, // 4. 현재 화면에 렌더링 되는 탭의 인덱스
      onTap: (int index) { // 5. 탭이 선택될 때 마다 실행되는 함수
        setState(() {
          controller!.animateTo(index);
        });
      },
      items: [
        BottomNavigationBarItem(
          icon: Icon(
            Icons.edgesensor_high_outlined,
          ),
          label: '주사위',
        ),
        BottomNavigationBarItem(
          icon: Icon(
            Icons.settings,
          ),
          label: '설정',
        ),
      ],
    );
  }
}
  1. addListener() 함수controller속성이 변할 때 마다 특정 함수를 실행할 수 있도록 콜백 함수를 등록할 수 있다.
    콜백setState()를 실행하여 controller 의 속성이 변경될 때 마다 build()를 재실행하도록 한다.

  2. TabController의 속성이 변경될 때 마다 실행할 함수이다.

  3. addListener를 사용해서 listener를 등록하면 위젯이 삭제될 때 항상 등록된 listener도 같이 삭제해 줘야 한다.

  4. BottomNavigationBar에서 현재 선택된 상태로 표시해야 하는 BottomNavigationBarItemindex 이다. TabBarView와 같은 탭의 인덱스를 바라보게 해줘야 한다.

  5. BottomNavigationBarItem(아이콘)이 눌릴 때 마다 실행되는 함수이다. 매개변수로 눌린 탭의 인덱스를 전달해 준다.
    탭을 눌렀을 때 TabBarView와 화면을 동기화해줘야 하니 animateTo() 함수를 사용해서 자연스러운 애니메이션으로 지정한 탭으로 TabBarView가 전환되게 한다.

HomeScreen 위젯 구현하기

레이아웃 처럼 Image 위젯 하나랑 Text위젯 2개를 Column으로 가운데 정렬해서 배치하면 된다.

추가적으로 어떤 숫자(주사위 눈) 를 보여줄건지는 RootScreen 위젯에서 정하도록 생성자를 통해서 number 매개변수값을 입력받겠다.

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

class HomeScreen extends StatelessWidget {
  final int number; // 보여줄 주사위 눈 멤버변수

  const HomeScreen({super.key, required this.number}); // RootScreen 에서 정한 number를 생성자를 통해서 number 매개변수 값으로 전달받음.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
      // 1. 주사위 이미지 위젯
        Center(
          child: Image.asset('asset/img/$number.png'),
        ),
        SizedBox(
          height: 32.0,
        ),
          // 2. 행운의 숫자 택스트 위젯
        Text(
          '행운의 숫자',
          style: TextStyle(
            color: secondaryColor,
            fontSize: 20.0,
            fontWeight: FontWeight.w700,
          ),
        ),
        SizedBox(height: 12.0),
          // 3. 주사위 눈 텍스트 위젯
        Text(
          number.toString(), // 생성자로 입력받은 number(주사위 눈 값) 에 해당하는 숫자
          style: TextStyle(
            color: primaryColor,
            fontSize: 60.0,
            fontWeight: FontWeight.w200,
          ),
        ),
      ],
    ));
  }
}

RootScreen.dart 에서 HomeScreen 위젯 연결

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


숫자 1 주사위 눈 이미지와 숫자가 잘 나온다

SettingsScreen 위젯 구현하기

lib/screen에 settings_screen.dart 파일 생성

세팅스크린 레이아웃

Slider 위젯과 민감도 text 위젯으로 구성되어 있다.

세팅스크린 구현하기

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

class SettingsScreen extends StatelessWidget {
  final double threshold; // Slider 위젯의 현재 값
  final ValueChanged<double> onThresholdChange; // Slider가 변경될 때 마다 실행되는 함수

  const SettingsScreen(
      {super.key, required this.threshold, required this.onThresholdChange}); // threshold 와 onThresholdChange는 SettingsScreen 에서 입력

  @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, // 1. 최솟값
          max: 10.0, // 2. 최댓값
          divisions: 101, // 3. 최솟값 ~ 최댓값 사이 구간의 개수
          value: threshold, // 4. 슬라이더 선택값
          onChanged: onThresholdChange, // 5. 값 변경 시 실행되는 함수
          label: threshold.toStringAsFixed(1), // 6. 표시하는 값
        ),
      ],
    );
  }
}

세팅스크린 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({super.key});

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

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

  .
  .
  .

  List<Widget> renderChildren() {
    return [
      HomeScreen(number: 1),
      SettingsScreen(
          threshold: threshold, onThresholdChange: onThresholdChange), // SettingsScreen으로 교체
    ];
  }

  void onThresholdChange(double val) { // 1. Slider 값 변경 시 실행할 함수
    setState(() {
      threshold = val; // 변경된 val 값으로 threshold값 초기화
    });
  }

  .
  .
  .
}
  1. Slider 위젯 의 현재값이 변경될 때 마다 threshold 변수 에 변경된 값val을 저장하고, setState() 함수를 실행해서 build() 함수를 재실행해준다.
    Slider 위젯은 변경된 threshold 변수의 값을 기반으로 화면에 다시 그려진다.

shake 플러그인 적용하기

shake 플러그인 설치

터미널에 해당 명령어를 입력한다.

flutter pub add shake

shake 플러그인 적용 준비

핸드폰을 흔들 때 마다 새로운 숫자가 생성되어야 하니 HomeScreen 위젯number 매개변수에 들어갈 값을 number 변수상태 관리하도록 코드를 변경하겠다.

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

. 
.
.

class _RootScreenState extends State<RootScreen> with TickerProviderStateMixin {
  TabController? controller;
  double threshold = 2.7; // 민감도의 기본값 설정
  int number = 1; // 1. 주사위 숫자 상태

  .
  .
  .

  List<Widget> renderChildren() {
    return [
      HomeScreen(number: number), // number 상태 변수로 대체
      SettingsScreen(
          threshold: threshold, onThresholdChange: onThresholdChange),
    ];
  }
  
  .
  .
  .
  
}

shake 플러그인 사용해보기

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 _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( // 1. 흔들기 감지 즉시 시작
      shakeSlopTimeMS: 100, // 2. 감지 주기
      shakeThresholdGravity: threshold, // 3. 감지 민감도
      onPhoneShake: onPhoneShake, // 4. 감지 후 실행할 함수 등록
    );
  }

  void onPhoneShake() { // 5. 감지 후 실행할 함수
    final rand = new Random();
    setState(() {
      number = rand.nextInt(5) + 1;
    });
  }

  tabListener() {
    setState(() {});
  }

  @override
  void dispose() {
    controller!.removeListener(tabListener);
    shakeDetector!.stopListening(); // 6. 흔들기 감지 중지
    super.dispose();
  }

.
.
.

}
  1. ShakeDetector의 autoStart 생성자를 이용하면 코드가 실행되는 순간부터 흔들기를 감지한다.
    waitForStart 생성자를 이용하면 코드만 등록을 해두고 추후에 흔들기 감지를 시작하는 코드를 따로 실행해 줄 수 있다.

  2. shakeSlopTimeMS: 로 감지 주기를 설정할 수 있다.

  3. shakeThresholdGravity 로 감지 민감도를 설정할 수 있다. 이 값은 SettingsScreen 위젯의 Slider 위젯에서 받아온다.

  4. onPhoneShake 로 흔들기를 감지했을때 실행되는 함수를 등록한다.

  5. dart 에서 기본으로 제공하는 dart:math 패키지의 Random 클래스를 이용해서 난수를 생성한다.

Random클래스
nextInt() 함수를 제공하는데 이 nextInt() 함수의 첫번째 매개변수에 생성될 최대 int 값을 넣어주면 된다.
즉, rand.nextInt(5) 이렇게 하면 0~5까지의 난수가 생성된다.
여기다 1을 더하면 1~6까지 난수를 생성할 수 있다.

테스트하기

이번 프로젝트는 가속도계, 자이로스코프를 활용하므로 일반 컴퓨터IDE 에서는 실험해볼 수가 없다. 따라서 스마트폰과 무선 디버깅 연결을 해서 테스트를 해야 한다.

방법은 여기 블로그를 참고했다.
https://joo-selfdev.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%8A%A4%ED%8A%9C%EB%94%94%EC%98%A4-%EB%AC%B4%EC%84%A0-%EB%94%94%EB%B2%84%EA%B9%85-ADB-WiFi-%ED%8E%98%EC%96%B4%EB%A7%81

민감도를 높여서 하니까 더 세게 흔들어야 주사위가 바뀐다!!

profile
아프지 말자 - (잘못된 정보, 수정 사항 있으면 언제든지 알려주시면 감사하겠습니다!)

0개의 댓글