자이로스코프,가속도 센서를 이용한 주사위 앱
기기의 자이로스코프 센서, 가속도 센서를 이용해서 모바일 기기의 흔듦을 감지해서 주사위를 굴리는 앱을 만든다 또한 사용자가 민감도를 조절하는 기능도 구현한다
가속도계
핸드폰에는 가속도계가 존재한다 물체가 특정방향으로 이동하는 가속도가 어느 정도인지 숫자로 측정하는 장치이다 3개의 축으로 되어있고 x축 좌우이동, y축 위 아래이동, z축 앞뒤이동(핸드폰을 전면에서 봤을때 기준)이다 이 값의 결과는 double로 받는다
자이로스코프
자이로스코프는 가속도계의 단점을 보안해서 x,y,z축의 회전을 측정할 수 있다
x축은 핸드폰을 좌우로 회전, y축은 위아래로 회전, z는 앞뒤로 회전을 의미한다
Sensor_Plus 패키지 사용
위 자이로스코프와 가속도 센서를 이용할려면 sensor_plus 패키지가 필요하다 또한 각각의 센서들은 x,y,z의 움직임을 반환하는데 이를 정규화해줘야 한다 여기서는 shake 패키지를 이용해서 미리 정규화된 패키지를 사용한다
1
2
3
4
5
6
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
sensors_plus: ^6.1.1 #pubspec.yaml에 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// main.dart
import 'package:flutter/material.dart';
import 'package:sensors_plus/sensors_plus.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: SensorExample(),
);
}
}
class SensorExample extends StatefulWidget {
@override
_SensorExampleState createState() => _SensorExampleState();
}
class _SensorExampleState extends State<SensorExample> {
late Stream<AccelerometerEvent> _accelerometerStream;
@override
void initState() {
super.initState();
_accelerometerStream = accelerometerEvents;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sensors Plus Example'),
),
body: StreamBuilder<AccelerometerEvent>(
stream: _accelerometerStream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final AccelerometerEvent event = snapshot.data!;
return Center(
child: Text(
'X: ${event.x.toStringAsFixed(2)}\n'
'Y: ${event.y.toStringAsFixed(2)}\n'
'Z: ${event.z.toStringAsFixed(2)}',
style: const TextStyle(fontSize: 24),
textAlign: TextAlign.center,
),
);
},
),
);
}
}
프로젝트 설정
1. 상수 추가하기
프로젝트에서 사용할 글자크기, 주색상등을 미리 정의할 수 있다 초기에 정해놓으면 나중에 수정할 때 하나만 수정해도 전체 반영이 가능하다
lib폴더에서 const 폴더를 생성후 colors.dart 파일을 생성한다
1
2
3
4
5
6
//colors.dart
import 'package:flutter/material.dart';
const backgroundColor = Color(0xFF0E0E);
const primaryColor = Colors.white;
const secondaryColor = Colors.grey;
2. 이미지 추가
asset 폴더를 생성하고 img폴더도 생성후 이미지를 추가한다
3. pubspec.yaml 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
shake: ^2.2.0 # 여기를 추가하면 sensors_puls도 자동설치됨
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/
4. Theme 설정하기
HomeScreen.dart는 StatelessWidget으로 일단 만들고 main.dart에 테마를 추가한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import 'package:dice/const/colors.dart';
import 'package:flutter/material.dart';
import 'package:dice/screen/HomeScreen.dart';
void main() {
runApp(
MaterialApp(
theme: ThemeData( //테마 색상을 추가
scaffoldBackgroundColor: backgroundColor,
sliderTheme: SliderThemeData(
thumbColor: primaryColor,
activeTickMarkColor: primaryColor,
inactiveTickMarkColor: primaryColor.withOpacity(0.3)
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
selectedItemColor: primaryColor,
unselectedItemColor: secondaryColor,
backgroundColor: backgroundColor,
)
),
home: HomeScreen(),
),
);
}
dice 앱 생성
이 앱에서는 BottomNavigationBar위젯을 사용해서 화면을 전환하기 때문에 이전과 다른 구조로 형태를 만들어야한다 HomeScreen과 SettingScreen을 TabBarView를 이용해서 RootScreen 위젯에 위치시킨다
1. RootScreen 구현하기
RootScreen은 BottomNavigationBar와 TabBarView를 포함한다 BottomNavigationBar를 아래에 위치시키고 남는 공간에 TabBarView를 위치시켜 네비바를 눌러 탭바 내용물을 바꾸는 구조이다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// RootScreen.dart
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 { //TickerProviderStateMixin을 사용해야 vsync기능을 사용할 수 있다
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: renderBottomNavigation(),
);
}
List<Widget> renderChildren() {
return [];
}
BottomNavigationBar renderBottomNavigation() {
return BottomNavigationBar(items: []);
}
}
TabController에서 vsync기능을 사용할려면 TickerProviderStateMixin을 사용해야한다 TickerProviderStateMixin는 애니메이션 효율을 올려준다 TabController의 length 매개변수에는 탭의 개수를 넣는다 생성된 TabController는 TabBarView의 controller 매개변수에 입력을 하면 TabBarView 조작이 가능하다
또한 이제 메인이 RootScreen이 되므로 main.dart에서 home: RootScreen으로 변경한다
2. BottomNavigationBar
하단에 주사위를 보이는 버튼과 민감도 설정버튼을 보여주는 BottomNavigationBar 위젯을 추가한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BottomNavigationBar renderBottomNavigation() {
return BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(
Icons.edgesensor_high_outlined,
),
label: '주사위',
),
BottomNavigationBarItem(
icon: Icon(
Icons.settings,
),
label: '설정',
)
]
);
}
3. renderChildren
TabBarView의 children을 제공해줘야한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
List<Widget> renderChildren() {
return [
Container(
child: Center(
child: Text(
'tab1',
style: TextStyle(
color: Colors.white,
),
),
),
),
Container(
child: Center(
child: Text(
'tab2',
style: TextStyle(
color: Colors.white,
),
),
),
),
];
}
TabBarView에 tab1과 tab2를 띄웠다 좌우 스와이프로 이동가능하다 하지만 아래 bottomnavigationbar는 연동이 안되어서 연동을 추가적으로 해야한다
4. BottomNavigationBar & TabBarView 연동하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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 { //TickerProviderStateMixin을 사용해야 vsync기능을 사용할 수 있다
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: renderBottomNavigation(),
);
}
List<Widget> renderChildren() {
return [
Container(
child: Center(
child: Text(
'tab1',
style: TextStyle(
color: Colors.white,
),
),
),
),
Container(
child: Center(
child: Text(
'tab2',
style: TextStyle(
color: Colors.white,
),
),
),
),
];
}
BottomNavigationBar renderBottomNavigation() {
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: '설정',
)
]
);
}
}
위 코드에서
1
2
3
4
5
6
7
8
9
10
11
12
13
14
controller!.addListener(tabListener); // 컨트롤러 속성이 변경할때 실행할 함수 등록
}
tabListener() {
setState(() {
});
} // 화면 변경이 일어날때 실행된는 상수
@override //위젯이 삭제될때 controller를 같이 삭제한다
void dispose() {
controller!.removeListener(tabListener); // 리스너에 등록한 함수 취소
super.dispose();
}
이부분은 주석처리를 해도 실행에는 영향을 끼지지 않는다 메모리 관리를 위해서 쓰여진 코드이다
5. HomeScreen
HomeScreen에서 주사위에서 나오는 이미지와 결과를 띄운다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import 'package:flutter/material.dart';
import 'package: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/dice_${number}.png"),
),
SizedBox(height:32),
Text('lucky number',
style: TextStyle(
color: secondaryColor,
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
SizedBox(height: 12),
Text(
number.toString(),
style: TextStyle(
color: primaryColor,
fontSize: 60.0,
fontWeight: FontWeight.w200,
),
)
],
);
}
}
6. SettingsScreen
이제 흔들림 강도를 조절할 세팅페이지 SettingsScreen.dart를 생성한다 tab2에 들어갈 페이지이다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//SettingsScreen.dart
import 'package:flutter/material.dart';
import 'package:dice/const/colors.dart';
class SettingsScreen extends StatelessWidget{
final double threhold; //Slider의 값
final ValueChanged<double> onThresholdChange; //slider가 변경될때마다 실행되는 함수
const SettingsScreen({
Key? key,
required this.threhold,
required this.onThresholdChange,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(left: 20),
child: Row(children: [
Text(
'민감도',
style: TextStyle(
color: secondaryColor,
fontSize: 20,
fontWeight: FontWeight.w700,
),
)
],)
),
Slider(
min: 1, //슬라이더 최소
max: 10, //슬라이더 최대값
divisions: 10, // 구간
value: threhold, //값
onChanged: onThresholdChange, // 변할 때 실행할 함수
label: threhold.toStringAsFixed(1), // 표시값
)
],
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//RootScreen.dart
import "package:flutter/material.dart";
import "package:dice/screen/HomeScreen.dart";
import "package:dice/screen/SettingsScreen.dart";
class RootScreen extends StatefulWidget{
const RootScreen({Key? key}) : super(key : key);
@override
State<RootScreen> createState() => _RootScreenState();
}
class _RootScreenState extends State<RootScreen> with
TickerProviderStateMixin { //TickerProviderStateMixin을 사용해야 vsync기능을 사용할 수 있다
TabController? controller; //사용할 컨트롤러 선언
double threshold = 5; //기본 값 추가
@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: renderBottomNavigation(),
);
}
List<Widget> renderChildren() {
return [
HomeScreen(number:1), // 주사위를 보여주는 위젯
SettingsScreen(threshold: threshold, // 주사위 민감도 설정 위젯
onThresholdChange: onThresholdChange
)
];
}
void onThresholdChange(double val) { //함수 정의
setState(() {
threshold = val;
});
}
BottomNavigationBar renderBottomNavigation() {
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: '설정',
)
]
);
}
}
7.shake 플러그인 적용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class _RootScreenState extends State<RootScreen> with
TickerProviderStateMixin { //TickerProviderStateMixin을 사용해야 vsync기능을 사용할 수 있다
TabController? controller; //사용할 컨트롤러 선언
double threshold = 5; //기본 값 추가
int number = 1; // 변수 추가
생략
List<Widget> renderChildren() {
return [
HomeScreen(number: number), // 주사위를 보여주는 위젯 기본값1에서 number로 교체
SettingsScreen(threshold: threshold, // 주사위 민감도 설정 위젯
onThresholdChange: onThresholdChange
)
];
}
매개변수의 값을 변경할 때마다 다른 결과를 화면에서 보여준다
이제 shake 플러그인을 사용해서 기기를 흔들때 실행할 함수를 등록한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
//RootScreen.dart
import "dart:ui";
import "package:flutter/material.dart";
import "package:dice/screen/HomeScreen.dart";
import "package:dice/screen/SettingsScreen.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 { //TickerProviderStateMixin을 사용해야 vsync기능을 사용할 수 있다
TabController? controller; //사용할 컨트롤러 선언
double threshold = 5; //기본 값 추가
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() {
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: renderBottomNavigation(),
);
}
List<Widget> renderChildren() {
return [
HomeScreen(number: number), // 주사위를 보여주는 위젯
SettingsScreen(threshold: threshold, // 주사위 민감도 설정 위젯
onThresholdChange: onThresholdChange
)
];
}
void onThresholdChange(double val) { //함수 정의
setState(() {
threshold = val;
});
}
BottomNavigationBar renderBottomNavigation() {
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: '설정',
)
]
);
}
}