10단계 디지털 주사위

송기영·2023년 12월 17일
0

플러터

목록 보기
12/25

첫번째 화면 핸드폰을 흔들면 난수가 생성되어 새로운 즈사위 눈금을 보여주는 기능,

두번째 화면 흔듦을 감지하는 민감도를 사용자가 지정할 수 있는 기능

10.1 사전지식

10.1.1. 가속도계

특정 물체가 특정 방향으로 이동하는 가속도가 어느 정도인지를 숫자로 측정하는 기기이다.

  • X축 : 좌우로 움직이는 방향
  • Y축: 위아래로 움직이는 방향
  • Z축: 앞뒤로 움직이는 방향

10.1.2. 자이로스코프

가속도계는 직선의 x,y,z축의 움직임만 측정이 가능하지만 자이로스코프는 이 단점을 보완해 회전을 측정할 수 있다.

10.1.3. Sensor_Plus 패키지

전반적인 핸드폰의 움직임을 측정하려면 정규화가 필요하다. 한마디로 x,y,z 각 값을 통합해 전반적인 움직임 수치를 계산해서 핸드폰ㅇ르 흔든 정도를 수치화해야 하는데 이를 도와주는 패키지이다.

10.2. 프로젝트 초기화

10.2.1. 상수 추가하기

// lib/const/colors.dart

import "package:flutter/material.dart";

const backgroundColor = Color(0xFF0E0E0E);
const primaryColor = Colors.white;
final secondaryColor = Colors.grey[600];

10.2.2. 초기 설정

// lib/main.dart

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

void main() {
  runApp(MaterialApp(
    theme: ThemeData(
        scaffoldBackgroundColor: backgroundColor,
        sliderTheme: SliderThemeData(
          thumbColor: primaryColor,
          activeTrackColor: primaryColor,
          inactiveTrackColor: primaryColor.withOpacity(0.3),
        ),
        bottomNavigationBarTheme: BottomNavigationBarThemeData(
            selectedItemColor: primaryColor,
            unselectedItemColor: secondaryColor,
            backgroundColor: backgroundColor)),
    home: const HomeScreen(),
  ));
}

// lib/screen/home_screen.dart

import "package:flutter/material.dart";

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Text('Home Screen'),
    );
  }
}

10.3. 구현하기

10.3.1. RootScreen 위젯 구현

TabBarView 위젯을 이용하여 Tab위젯과 쉽게 연동할 수 있는 UI를 구현할 수 있다.

TabBarView는 TabController가 필수이며 이를 초기화하려면 vsync 기능이 필요한데 이는 State 위젯의 TickerProviderMixin을 mixin으로 제공해줘야 한다.

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({super.key});

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

class _RootScreenState extends State<RootScreen> with TickerProviderStateMixin {
  TabController? controller;
  double threshold = 2.7;
  int number = 1;
  ShakeDetector? shakeDetector;

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

    // 컨트롤러 속성이 변경될 때마다 실행함수 등록
    controller?.addListener(tabListener);
    shakeDetector = ShakeDetector.autoStart(
        shakeSlopTimeMS: 100, // 감지 주기
        shakeThresholdGravity: threshold, // 감지 민감도
        onPhoneShake: onPhoneShake); // 감지 후 실행할 함수
  }

  void onPhoneShake() {
    print('흔들나!');
    final rand = Random();
    setState(() {
      number = rand.nextInt(5) + 1;
    });
  }

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

  
  void dispose() {
    controller!.removeListener(tabListener);
    shakeDetector!.stopListening();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      // 탭 화면을 보여줄 위젯
      body: TabBarView(
        // 컨트롤러 등록
        controller: controller,
        children: renderChildren(),
      ),
      // 아래 탭 내비게이션 구현하는 매개변수
      bottomNavigationBar: renderBootomNavigation(),
    );
  }

  List<Widget> renderChildren() {
    return [
      HomeScreen(number: number),
      SettingScreen(threshold: threshold, onThresholdChange: onThresholdChange)
    ];
  }

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

  BottomNavigationBar renderBootomNavigation() {
    // 탭 내비게이션을 구현하는 위젯
    return BottomNavigationBar(
        currentIndex: controller!.index,
        onTap: (int index) {
          setState(() {
            controller!.animateTo(index);
          });
        },
        items: const [
          BottomNavigationBarItem(
              icon: Icon(
                Icons.edgesensor_high_outlined,
              ),
              label: "주사위"),
          BottomNavigationBarItem(
              icon: Icon(
                Icons.settings,
              ),
              label: "설정"),
        ]);
  }
}

TickerProviderMixin은 애니메이션 효율을 담당한다. 플러터는 기기가 지원하는 대로 60~120FPS를 지원하는데 TickerProviderMixin는 정확히 한 틱(1FPS)마다 애니메이션을 실행한다. 간혹 애니메이션 주기가 더 자주 렌더링 되는것을 막아준다.

vsync는 수직동기화로 그래픽 처리 장치와 디스플레이간의 동기화를 제어하는데 사용된다.

10.3.2. HomeScreen 위젯 구현

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

class HomeScreen extends StatelessWidget {
  final int number;
  const HomeScreen({
    required this.number,
    super.key,
  });

  
  Widget build(BuildContext context) {
    return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
      Image.asset('asset/img/$number.png'),
      const SizedBox(
        height: 32.0,
      ),
      Text(
        "행운의 숫자",
        style: TextStyle(
            color: secondaryColor, fontSize: 20.0, fontWeight: FontWeight.w700),
      ),
      const SizedBox(
        height: 12.0,
      ),
      Text(
        number.toString(),
        style: const TextStyle(
            color: primaryColor, fontSize: 60.0, fontWeight: FontWeight.w200),
      )
    ]);
  }
}

10.3.3. SettingScreen 위젯 구현

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

class SettingScreen extends StatelessWidget {
  final double threshold; // slider의 현재값
  final ValueChanged<double> onThresholdChange;

  const SettingScreen({
    required this.threshold,
    required this.onThresholdChange,
    super.key,
  });

  
  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), // 소숫점 표시값
        )
      ],
    );
  }
}

shake 패키지까지 적용해 실제기기에서 적용을 해보았는데 엄청 강하게 흔들어야 인식이 됨을 확인했다.. 민감도가 최대임에도 불구하고 엄청 강해야했음.

💡Tips : Visual Studio Code의 린트에서 위젯에 const를 사용을 권장 했는데 위젯 내에 const이외의 final 변수들을 사용하게 되면 const를 위젯을 사용하면 안된다.

번외

자이로스코프를 이용한 구현

아래와 같이 구현을 했으나.. 자이로스코프가 엄청나게 민감한 센서이기 때문에 주사위의 값이 계속 바뀌는 현상이 발생했다. 그래서 야매로 핸드폰을 가만히 놔두면 주사위 값이 안바뀌고 들면 바뀌는식으로 구현했는데 좀 이상하긴하다 …ㅎ 실력 부족으로 민감도는 따로 적용하지 않았다.

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:sensors_plus/sensors_plus.dart";

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

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

class _RootScreenState extends State<RootScreen> with TickerProviderStateMixin {
  TabController? controller;
  double threshold = 2.7;
  int number = 1;
  GyroscopeEvent? gyroscopeEvent;

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

    // 컨트롤러 속성이 변경될 때마다 실행함수 등록
    controller?.addListener(tabListener);
    gyroscopeEventStream(samplingPeriod: Duration(milliseconds: 100)).listen(
      (GyroscopeEvent event) {
        setState(() {
          gyroscopeEvent = event;
        });

        double x = double.parse(event.x.toStringAsFixed(1));
        double y = double.parse(event.y.toStringAsFixed(1));
        double z = double.parse(event.z.toStringAsFixed(1));

        if (x != 0.0 || y != 0.0 || z != 0.0) {
          onPhoneShake();
        }
      },
    );
  }

  void onPhoneShake() {
    final rand = Random();
    setState(() {
      number = rand.nextInt(5) + 1;
    });
  }

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

  
  void dispose() {
    controller!.removeListener(tabListener);
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      // 탭 화면을 보여줄 위젯
      body: TabBarView(
        // 컨트롤러 등록
        controller: controller,
        children: renderChildren(),
      ),
      // 아래 탭 내비게이션 구현하는 매개변수
      bottomNavigationBar: renderBootomNavigation(),
    );
  }

  List<Widget> renderChildren() {
    return [
      HomeScreen(number: number, gyroscopeEvent: gyroscopeEvent),
      SettingScreen(threshold: threshold, onThresholdChange: onThresholdChange)
    ];
  }

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

  BottomNavigationBar renderBootomNavigation() {
    // 탭 내비게이션을 구현하는 위젯
    return BottomNavigationBar(
        currentIndex: controller!.index,
        onTap: (int index) {
          setState(() {
            controller!.animateTo(index);
          });
        },
        items: const [
          BottomNavigationBarItem(
              icon: Icon(
                Icons.edgesensor_high_outlined,
              ),
              label: "주사위"),
          BottomNavigationBarItem(
              icon: Icon(
                Icons.settings,
              ),
              label: "설정"),
        ]);
  }
}
// lib/screen/home_screen.dart

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

class HomeScreen extends StatelessWidget {
  final int number;
  final GyroscopeEvent? gyroscopeEvent;
  const HomeScreen({
    required this.number,
    required this.gyroscopeEvent,
    super.key,
  });

  
  Widget build(BuildContext context) {
    return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
      Image.asset('asset/img/$number.png'),
      const SizedBox(
        height: 32.0,
      ),
      Text(
        "행운의 숫자",
        style: TextStyle(
            color: secondaryColor, fontSize: 20.0, fontWeight: FontWeight.w700),
      ),
      const SizedBox(
        height: 12.0,
      ),
      Text(
        number.toString(),
        style: const TextStyle(
            color: primaryColor, fontSize: 60.0, fontWeight: FontWeight.w200),
      ),
      Text(
        "x:(${gyroscopeEvent?.x.toString()})" ?? '?',
        style: TextStyle(
            color: secondaryColor, fontSize: 20.0, fontWeight: FontWeight.w700),
      ),
      Text(
        "y:(${gyroscopeEvent?.y.toString()})" ?? '?',
        style: TextStyle(
            color: secondaryColor, fontSize: 20.0, fontWeight: FontWeight.w700),
      ),
      Text(
        "z:(${gyroscopeEvent?.y.toString()})" ?? '?',
        style: TextStyle(
            color: secondaryColor, fontSize: 20.0, fontWeight: FontWeight.w700),
      )
    ]);
  }
}
profile
업무하면서 쌓인 노하우를 정리하는 블로그🚀 풀스택 개발자를 지향하고 있습니다👻

0개의 댓글