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

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

가속도계와 마찬가지로 자이로스코프도 회전에 대한 이벤트를 받게 되면 x, y, z 축 모두에서의 회전값이 동시에 반환된다.
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개를 추가해준다.
이미지 경로를 추가해준다.
assets:
- asset/img/
앱의 기본 홈 화면으로 사용할 HomeScreen 위젯을 생성할 home_screen.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(
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(),
),
);
}

TabBarView를 통해 선택된 화면을 보여준다.
BottomNavigatorBar 에서 각 탭(주사위, 설정)을 누르거나 TabBarView에서 좌우로 스크롤을 해서 화면을 전환할 수 있다.

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

세팅 버튼을 누르면 설정 스크린 화면으로 이동한다.
Slider 위젯을 사용해서 흔들기 기능의 민감도를 정하도록 한다.

BottomNavigationBar 을 아래에 위치시키고 남는 공간에 TabBarView를 위치시켜서 스크린 전환이 가능한 구조를 만들겠다.
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. 탭 네비게이션을 구현하는 위젯
}
}
TabBarView 위젯은 PageView와 매우 비슷한 기본 애니메이션이 제공되며 children 매개변수에 각 탭의 화면으로 활용하고 싶은 위젯을 List 로 넣어주면 된다.
Scaffold 위젯은 BottomNavigationBar 을 위치시키는 매개변수(파라미터)를 따로 보유한다.
bottomNavigation 매개변수에 BottomNavigationBar 를 넣어주면 쉽게 Tab을 조정할 수 있는 UI를 핸드폰의 아래에 배치할 수 있다.
BottomNavigationBar 에 제공될 각 탭(주사위, 세팅 버튼)은 BottomNavigationBar 위젯의 items 매개변수 에 제공하면 된다.
TabBarView 는 TabController 가 필수이다.
TabController 를 초기화 하려면 vsync 기능이 필요한데, State 위젯에 TickerProviderMixin 을 mixin 으로 제공해줘야 사용할 수 있다.
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: []);
}
}
TabController 에서 vsync 기능을 사용하려면 TickerProviderStateMixin 을 필수로 사용해야 한다.
TickerProviderMixin 과 SingleTickerProviderMixin 은 애니메이션의 효율을 높여주는 역할을 한다.
TabController 의 length 매개변수에는 사용할 탭의 개수를 int 값으로 제공해주고, vsync 에는 TickerProviderMixin을 사용하는 State 클래스를 this 형태로 넣어주면 된다.
생성된 TabController는 TabBarView 의 controller 매개변수 에 입력해주면 된다.
이제 입력된 TabController 를 이용해서 TabBarView 를 조작할 수 있다.
TickerProviderMixin과 vsync
플러터는 기기가 지원하는 대로 60 ~ 120 FPS(초당 프레임) 을 지원하는데 TickerProviderMixin을 사용하면 정확히 한 틱(1FPS) 마다 애니메이션을 실행한다.
간혹 애니메이션 코드를 실행하면 실제로 화면에 렌더링할 수 있는 주기보다 더 자주 렌더링을 하는 때가 있는데 이 때 TickerProviderMixin 을 사용하면 비효율적으로 렌더링을 더 하는 상황을 막아준다.
TabController 도 마찬가지로 vsync 에 TickerProviderMixin 을 제공함으로써 렌더링 효율을 극대화 할 수 있다.
BottomNavigationBar renderBottomNavigation() {
return BottomNavigationBar(
items: [
BottomNavigationBarItem( // 1. 하단 탭바의 각 버튼을 구현
icon: Icon(
Icons.edgesensor_high_outlined,
),
label: '주사위', // 주사위 버튼
),
BottomNavigationBarItem(
icon: Icon(
Icons.settings,
),
label: '설정', // 설정 버튼
),
],
);
}
BottomNavigationBarItem 클래스에는 아이템아이콘을 지정할 수 있는 icon 매개변수와, 아이템의이름을 지정할 수 있는 label 매개변수를 제공할 수 있다.
즉, (아이콘, 라벨) 묶음으로 버튼을 만들 수 있다.
각 탭을 표현해줄 위젯들을 TabBarView의 children에 제공해줘야 한다.
일단 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,
),
),
),
),
];
}
실행해보면 화면을 스크롤 하면 탭이 잘 넘어간다.

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: '설정',
),
],
);
}
}
addListener() 함수는 controller 의 속성이 변할 때 마다 특정 함수를 실행할 수 있도록 콜백 함수를 등록할 수 있다.
이 콜백에 setState()를 실행하여 controller 의 속성이 변경될 때 마다 build()를 재실행하도록 한다.
TabController의 속성이 변경될 때 마다 실행할 함수이다.
addListener를 사용해서 listener를 등록하면 위젯이 삭제될 때 항상 등록된 listener도 같이 삭제해 줘야 한다.
BottomNavigationBar에서 현재 선택된 상태로 표시해야 하는 BottomNavigationBarItem의 index 이다. TabBarView와 같은 탭의 인덱스를 바라보게 해줘야 한다.
BottomNavigationBarItem(아이콘)이 눌릴 때 마다 실행되는 함수이다. 매개변수로 눌린 탭의 인덱스를 전달해 준다.
탭을 눌렀을 때 TabBarView와 화면을 동기화해줘야 하니 animateTo() 함수를 사용해서 자연스러운 애니메이션으로 지정한 탭으로 TabBarView가 전환되게 한다.

레이아웃 처럼 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,
),
),
],
));
}
}
List<Widget> renderChildren() {
return [
HomeScreen(number: 1), // 홈스크린, 지금은 숫자 1로 설정
Container(
// 설정 스크린 탭
child: Center(
child: Text(
'Tab 2',
style: TextStyle(
color: Colors.white,
),
),
),
),
];
}

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

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. 표시하는 값
),
],
);
}
}
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값 초기화
});
}
.
.
.
}
Slider 위젯 의 현재값이 변경될 때 마다 threshold 변수 에 변경된 값val을 저장하고, setState() 함수를 실행해서 build() 함수를 재실행해준다.Slider 위젯은 변경된 threshold 변수의 값을 기반으로 화면에 다시 그려진다.
터미널에 해당 명령어를 입력한다.
flutter pub add 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),
];
}
.
.
.
}
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();
}
.
.
.
}
ShakeDetector의 autoStart 생성자를 이용하면 코드가 실행되는 순간부터 흔들기를 감지한다.
waitForStart 생성자를 이용하면 코드만 등록을 해두고 추후에 흔들기 감지를 시작하는 코드를 따로 실행해 줄 수 있다.
shakeSlopTimeMS: 로 감지 주기를 설정할 수 있다.
shakeThresholdGravity 로 감지 민감도를 설정할 수 있다. 이 값은 SettingsScreen 위젯의 Slider 위젯에서 받아온다.
onPhoneShake 로 흔들기를 감지했을때 실행되는 함수를 등록한다.
dart 에서 기본으로 제공하는 dart:math 패키지의 Random 클래스를 이용해서 난수를 생성한다.
Random클래스
nextInt() 함수를 제공하는데 이 nextInt() 함수의첫번째 매개변수에 생성될 최대 int 값을 넣어주면 된다.
즉,rand.nextInt(5)이렇게 하면 0~5까지의 난수가 생성된다.
여기다 1을 더하면 1~6까지 난수를 생성할 수 있다.
이번 프로젝트는 가속도계, 자이로스코프를 활용하므로 일반 컴퓨터IDE 에서는 실험해볼 수가 없다. 따라서 스마트폰과 무선 디버깅 연결을 해서 테스트를 해야 한다.

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