Skip to content

Commit d764940

Browse files
authored
feat: 마음 기록 화면 완성 (#58)
1 parent 2af9779 commit d764940

File tree

9 files changed

+373
-58
lines changed

9 files changed

+373
-58
lines changed
Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 6 additions & 0 deletions
Loading
File renamed without changes.

frontend/ongi/ios/Runner/Info.plist

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,11 @@
5252
</array>
5353
<key>UIStatusBarHidden</key>
5454
<false/>
55+
<key>NSCameraUsageDescription</key>
56+
<string>마음 기록을 위해 카메라 권한이 필요합니다.</string>
57+
<key>NSMicrophoneUsageDescription</key>
58+
<string>마음 기록을 위해 오디오 권한이 필요합니다.</string>
59+
<key>NSLocationWhenInUseUsageDescription</key>
60+
<string>마음 기록을 위해 위치 권한이 필요합니다.</string>
5561
</dict>
5662
</plist>
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:camera/camera.dart';
3+
import 'package:flutter_svg/flutter_svg.dart';
4+
import 'package:ongi/core/app_colors.dart';
5+
import 'dart:io';
6+
7+
class AddRecordScreen extends StatefulWidget {
8+
const AddRecordScreen({super.key});
9+
10+
@override
11+
State<AddRecordScreen> createState() => AddRecordScreenState();
12+
}
13+
14+
class AddRecordScreenState extends State<AddRecordScreen> with TickerProviderStateMixin {
15+
late CameraController controller;
16+
List<CameraDescription>? cameras;
17+
bool isInitialized = false;
18+
bool hasError = false;
19+
String errorMessage = '';
20+
int currentCameraIndex = 0;
21+
CameraController? frontCameraController;
22+
bool showFrontCamera = false;
23+
String? backCapturedImagePath;
24+
bool _isPhotoTaken = false;
25+
26+
late AnimationController _frontAnimationController;
27+
late Animation<double> _frontAnimation;
28+
bool _isFrontAnimating = false;
29+
String? _animatingFrontImagePath;
30+
31+
@override
32+
void initState() {
33+
super.initState();
34+
_initializeCamera();
35+
36+
_frontAnimationController = AnimationController(
37+
duration: const Duration(milliseconds: 500),
38+
vsync: this,
39+
);
40+
41+
_frontAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
42+
CurvedAnimation(
43+
parent: _frontAnimationController,
44+
curve: Curves.easeInOut,
45+
),
46+
);
47+
}
48+
49+
Future<void> _initializeCamera() async {
50+
try {
51+
cameras = await availableCameras();
52+
53+
if (cameras != null && cameras!.isNotEmpty) {
54+
controller = CameraController(
55+
cameras![currentCameraIndex],
56+
ResolutionPreset.high,
57+
enableAudio: false,
58+
);
59+
60+
await controller.initialize();
61+
62+
setState(() {
63+
isInitialized = true;
64+
});
65+
} else {
66+
setState(() {
67+
hasError = true;
68+
errorMessage = '사용 가능한 카메라가 없습니다.';
69+
});
70+
}
71+
} catch (e) {
72+
setState(() {
73+
hasError = true;
74+
errorMessage = '카메라 오류가 발생했습니다.';
75+
});
76+
}
77+
}
78+
79+
Future<void> _initializeFrontCamera() async {
80+
frontCameraController = CameraController(
81+
cameras![1],
82+
ResolutionPreset.medium,
83+
enableAudio: false,
84+
);
85+
86+
await frontCameraController!.initialize();
87+
88+
setState(() {
89+
showFrontCamera = true;
90+
});
91+
}
92+
93+
Future<void> _startFrontAnimation(String imagePath) async {
94+
_frontAnimationController.reset();
95+
96+
setState(() {
97+
_isFrontAnimating = true;
98+
_animatingFrontImagePath = imagePath;
99+
});
100+
101+
_frontAnimationController.forward().then((_) {
102+
setState(() {
103+
_isFrontAnimating = false;
104+
});
105+
});
106+
}
107+
108+
@override
109+
void dispose() {
110+
controller.dispose();
111+
frontCameraController?.dispose();
112+
_frontAnimationController.dispose();
113+
super.dispose();
114+
}
115+
116+
@override
117+
Widget build(BuildContext context) {
118+
if (hasError) {
119+
return Scaffold(
120+
appBar: AppBar(title: const Text('카메라')),
121+
body: Center(
122+
child: Column(
123+
mainAxisAlignment: MainAxisAlignment.center,
124+
children: [
125+
const Icon(Icons.error, size: 64, color: Colors.red),
126+
const SizedBox(height: 16),
127+
Text(
128+
errorMessage,
129+
style: const TextStyle(fontSize: 16),
130+
textAlign: TextAlign.center,
131+
),
132+
const SizedBox(height: 16),
133+
ElevatedButton(
134+
onPressed: () {
135+
setState(() {
136+
hasError = false;
137+
isInitialized = false;
138+
});
139+
_initializeCamera();
140+
},
141+
child: const Text('다시 시도'),
142+
),
143+
],
144+
),
145+
),
146+
);
147+
}
148+
149+
return Scaffold(
150+
backgroundColor: Colors.white,
151+
body: Stack(
152+
children: [
153+
SizedBox(
154+
width: double.infinity,
155+
child: Column(
156+
crossAxisAlignment: CrossAxisAlignment.start,
157+
mainAxisAlignment: MainAxisAlignment.center,
158+
children: [
159+
const SizedBox(height: 140),
160+
const Padding(
161+
padding: EdgeInsets.only(left: 32),
162+
child: Text(
163+
'지금 마음을',
164+
style: TextStyle(
165+
fontSize: 55,
166+
color: AppColors.ongiOrange,
167+
fontWeight: FontWeight.w200,
168+
height: 1,
169+
),
170+
),
171+
),
172+
const Padding(
173+
padding: EdgeInsets.only(left: 32),
174+
child: Text(
175+
'나눠볼까요?',
176+
style: TextStyle(
177+
fontSize: 55,
178+
color: AppColors.ongiOrange,
179+
fontWeight: FontWeight.w800,
180+
),
181+
),
182+
),
183+
const SizedBox(height: 20),
184+
Center(
185+
child: Padding(
186+
padding: const EdgeInsets.symmetric(horizontal: 16),
187+
child: AspectRatio(
188+
aspectRatio: 1 / 1.2,
189+
child: ClipRRect(
190+
borderRadius: BorderRadius.circular(25),
191+
child: Stack(
192+
children: [
193+
SizedBox.expand(
194+
child: AnimatedSwitcher(
195+
duration: const Duration(milliseconds: 300),
196+
child: backCapturedImagePath != null
197+
? SizedBox.expand(
198+
key: const ValueKey('captured_image'),
199+
child: Image.file(
200+
File(backCapturedImagePath!),
201+
fit: BoxFit.cover,
202+
),
203+
)
204+
: isInitialized
205+
? SizedBox.expand(
206+
key: const ValueKey('camera_preview'),
207+
child: FittedBox(
208+
fit: BoxFit.cover,
209+
child: SizedBox(
210+
width: controller.value.previewSize?.height ?? 100,
211+
height: controller.value.previewSize?.width ?? 100,
212+
child: CameraPreview(controller),
213+
),
214+
),
215+
)
216+
: const SizedBox.expand(
217+
key: ValueKey('camera_loading'),
218+
child: Center(
219+
child: CircularProgressIndicator(
220+
color: AppColors.ongiOrange,
221+
),
222+
),
223+
),
224+
),
225+
),
226+
if (_animatingFrontImagePath != null)
227+
AnimatedBuilder(
228+
animation: _frontAnimation,
229+
builder: (context, child) {
230+
final animationValue = _isFrontAnimating
231+
? _frontAnimation.value
232+
: 1.0;
233+
234+
final screenWidth = MediaQuery.of(
235+
context,
236+
).size.width;
237+
238+
final startTop = 0.0;
239+
final startLeft = 0.0;
240+
final startWidth = screenWidth - 32.0;
241+
final startHeight = startWidth / (1 / 1.2);
242+
243+
final endTop = 15.0;
244+
final endLeft = 15.0;
245+
final endWidth = 120.0;
246+
final endHeight = 144.0;
247+
248+
final currentTop = startTop + (endTop - startTop) * animationValue;
249+
final currentLeft = startLeft + (endLeft - startLeft) * animationValue;
250+
final currentWidth = startWidth + (endWidth - startWidth) * animationValue;
251+
final currentHeight = startHeight + (endHeight - startHeight) * animationValue;
252+
253+
return Positioned(
254+
top: currentTop,
255+
left: currentLeft,
256+
child: Container(
257+
width: currentWidth,
258+
height: currentHeight,
259+
decoration: BoxDecoration(
260+
borderRadius: BorderRadius.circular(25),
261+
border: Border.all(
262+
color: AppColors.ongiOrange,
263+
width: 2.5,
264+
),
265+
),
266+
child: ClipRRect(
267+
borderRadius: BorderRadius.circular(22.5),
268+
child: Image.file(
269+
File(_animatingFrontImagePath!),
270+
fit: BoxFit.cover,
271+
),
272+
),
273+
),
274+
);
275+
},
276+
),
277+
],
278+
),
279+
),
280+
),
281+
),
282+
),
283+
const Spacer(),
284+
Center(
285+
child: Opacity(
286+
opacity: (_isPhotoTaken || !isInitialized) ? 0.3 : 1.0,
287+
child: IconButton(
288+
icon: SvgPicture.asset("assets/images/camera_button.svg"),
289+
onPressed: (_isPhotoTaken || !isInitialized)
290+
? null
291+
: () async {
292+
try {
293+
setState(() {
294+
_isPhotoTaken = true;
295+
});
296+
297+
final XFile image = await controller.takePicture();
298+
299+
setState(() {
300+
backCapturedImagePath = image.path;
301+
});
302+
303+
if (currentCameraIndex == 0 && cameras!.length > 1) {
304+
await _initializeFrontCamera();
305+
final XFile frontImage = await frontCameraController!.takePicture();
306+
await _startFrontAnimation(frontImage.path);
307+
}
308+
} catch (e) {
309+
setState(() {
310+
_isPhotoTaken = false;
311+
});
312+
}
313+
},
314+
),
315+
),
316+
),
317+
const Spacer(),
318+
],
319+
),
320+
),
321+
Positioned(
322+
top: 80,
323+
right: 30,
324+
child: IconButton(
325+
icon: SvgPicture.asset(
326+
'assets/images/close_icon_black.svg',
327+
width: 28,
328+
),
329+
onPressed: () {
330+
Navigator.of(context).pop();
331+
},
332+
iconSize: 36,
333+
),
334+
),
335+
],
336+
),
337+
);
338+
}
339+
}

0 commit comments

Comments
 (0)