Must Have 코드팩토리의 플러터 프로그래밍 - 14 ~ 15
book
Must Have 코드팩토리의 플러터 프로그래밍 14 ~ 15
14장 [Project] 오늘도 출첵
P.384 (14.1.1)
Geolocator 위치 서비스 권한 확인하기
void main() {
final isLocationEnabled = await Geolocator.isLocationServiceEnabled(); // 위치서비스 활성여부 - boolean
final checkedPermission = await Geolocator.checkPermission(); // 권한 확인 - LocationPermission enum
final checkedPermission = await Geolocator.requestPermission(); // 권한 요청 - LocationPermission enum
}
P.408 (14.5)
테스트하기 : 최종 완성본 결과
// lib/main.dart
import 'package:chool_check/screen/home_screen.dart';
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: HomeScreen(),
),
);
}
// lib/screen/home_screen.dart
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatelessWidget {
static final LatLng companyLatLng = LatLng(
37.5233273, // 위도
126.921252, // 경도
);
static final Marker marker = Marker(
markerId: MarkerId('company'),
position: companyLatLng,
);
static final Circle circle = Circle(
circleId: CircleId('choolCheckCircle'),
center: companyLatLng,
fillColor: Colors.blue.withOpacity(0.5),
radius: 100,
strokeColor: Colors.blue,
strokeWidth: 1,
);
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: FutureBuilder<String>(
future: checkPermission(),
builder: (context, snapshot) {
if (!snapshot.hasData &&
snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
if(snapshot.data == '위치 권한이 허가 되었습니다.') {
return Column(
children: [
Expanded(
flex: 2,
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: companyLatLng,
zoom: 16,
),
myLocationEnabled: true,
markers: Set.from([marker]),
circles: Set.from([circle]),
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.timelapse_outlined,
color: Colors.blue,
size: 50.0,
),
const SizedBox(height: 20.0),
ElevatedButton(
onPressed: () async {
final curPosition = await Geolocator.getCurrentPosition();
final distance = Geolocator.distanceBetween(
curPosition.latitude,
curPosition.longitude,
companyLatLng.latitude,
companyLatLng.longitude,
);
bool canCheck =
distance < 100;
showDialog(
context: context,
builder: (_) {
return AlertDialog(
title: Text('출근하기'),
content: Text(
canCheck ? '출근을 하시겠습니까?' : '출근할수 없는 위치입니다.',
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text('취소'),
),
if (canCheck)
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text('출근하기'),
),
],
);
},
);
},
child: Text('출근하기!'),
),
],
),
),
],
);
}
return Center(
child: Text(
snapshot.data.toString(),
),
);
}
),
);
}
AppBar renderAppBar() {
return AppBar(
title: Text(
'오늘도 출근',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
backgroundColor: Colors.white,
);
}
Future<String> checkPermission() async {
final isLocationEnabled = await Geolocator.isLocationServiceEnabled();
if (!isLocationEnabled) {
return '위치 서비스를 활성화해주세요.';
}
LocationPermission checkedPermission = await Geolocator.checkPermission();
if (checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
if (checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요.';
}
}
if (checkedPermission == LocationPermission.deniedForever) {
return '앱의 위치 권한을 설정에서 허가해주세요.';
}
return '위치 권한이 허가 되었습니다.';
}
}
15장 [Project] 포토 스티커
P.442 (15.5)
테스트하기 : 최종 완성본 결과
// lib/main.dart
import 'dart:io';
import 'package:image_editor/screen/home_screen.dart';
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: HomeScreen(),
),
);
}
// lib/screen/home_screen.dart
import 'package:flutter/material.dart';
import 'package:image_editor/component/main_app_bar.dart';
import 'package:image_editor/model/sticker_model.dart';
import 'dart:io';
import 'package:image_picker/image_picker.dart';
import 'package:image_editor/component/footer.dart';
import 'package:image_editor/model/sticker_model.dart';
import 'package:image_editor/component/emoticon_sticker.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter/rendering.dart';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
import 'dart:typed_data';
import 'package:image_gallery_saver/image_gallery_saver.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
XFile? image;
Set<StickerModel> stickers = {};
String? selectedId;
GlobalKey imgKey = GlobalKey();
void onPickImage() async {
final image = await ImagePicker()
.pickImage(source: ImageSource.gallery);
setState(() {
this.image = image;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: [
renderBody(),
Positioned(
top: 0,
left: 0,
right: 0,
child: MainAppBar(
onPickImage: onPickImage,
onSaveImage: onSaveImage,
onDeleteItem: onDeleteItem,
),
),
if (image != null)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Footer(
onEmoticonTap: onEmoticonTap,
),
),
],
),
);
}
Widget renderBody() {
if (image != null) {
return RepaintBoundary(
key: imgKey,
child: Positioned.fill(
child: InteractiveViewer(
child: Stack(
fit: StackFit.expand,
children: [
Image.file(
File(image!.path),
fit: BoxFit.cover,
),
...stickers.map(
(sticker) => Center(
child: EmoticonSticker(
key: ObjectKey(sticker.id),
onTransform: () {
onTransform(sticker.id);
},
imgPath: sticker.imgPath,
isSelected: selectedId == sticker.id,
),
),
),
],
),
),
),
);
} else {
return Center(
child: TextButton(
style: TextButton.styleFrom(
primary: Colors.grey,
),
onPressed: onPickImage,
child: Text('이미지 선택하기'),
),
);
}
}
void onEmoticonTap(int index) async {
setState(() {
stickers = {
...stickers,
StickerModel(
id: Uuid().v4(),
imgPath: 'asset/img/emoticon_$index.png',
),
};
});
}
void onSaveImage() async {
RenderRepaintBoundary boundary = imgKey.currentContext!
.findRenderObject() as RenderRepaintBoundary;
ui.Image image = await boundary.toImage();
ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData!.buffer.asUint8List();
await ImageGallerySaver.saveImage(pngBytes, quality: 100);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('저장되었습니다!'),
),
);
}
void onDeleteItem() async {
setState(() {
stickers = stickers.where((sticker) => sticker.id != selectedId).toSet();
});
}
void onTransform(String id){
setState(() {
selectedId = id;
});
}
}
// lib/component/emoticon_sticker.dart
import 'package:flutter/material.dart';
class EmoticonSticker extends StatefulWidget {
final VoidCallback onTransform;
final String imgPath;
final bool isSelected;
const EmoticonSticker({
required this.onTransform,
required this.imgPath,
required this.isSelected,
Key? key,
}) : super(key: key);
@override
State<EmoticonSticker> createState() => _EmoticonStickerState();
}
class _EmoticonStickerState extends State<EmoticonSticker> {
double scale = 1;
double hTransform = 0;
double vTransform = 0;
double actualScale = 1;
@override
Widget build(BuildContext context) {
return Transform(
transform: Matrix4.identity()
..translate(hTransform, vTransform)
..scale(scale, scale),
child: Container(
decoration: widget.isSelected
? BoxDecoration(
borderRadius: BorderRadius.circular(4.0),
border: Border.all(
color: Colors.blue,
width: 1.0,
),
)
: BoxDecoration(
border: Border.all(
width: 1.0,
color: Colors.transparent,
),
),
child: GestureDetector(
onTap: () {
widget.onTransform();
},
onScaleUpdate: (ScaleUpdateDetails details) {
setState(() {
scale =
details.scale * actualScale;
vTransform += details.focalPointDelta.dy;
hTransform += details.focalPointDelta.dx;
});
},
onScaleEnd: (ScaleEndDetails details) {
actualScale = scale;
},
child: Image.asset(
widget.imgPath,
),
),
),
);
}
}
// lib/component/footer.dart
import 'package:flutter/material.dart';
typedef OnEmoticonTap = void Function(int id);
class Footer extends StatelessWidget {
final OnEmoticonTap onEmoticonTap;
const Footer({
required this.onEmoticonTap,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white.withOpacity(0.9),
height: 150,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(
7,
(index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: GestureDetector(
onTap: () {
onEmoticonTap(index + 1);
},
child: Image.asset(
'asset/img/emoticon_${index + 1}.png',
height: 100,
),
),
),
),
),
),
);
}
}
// lib/component/main_app_bar.dart
import 'package:flutter/material.dart';
class MainAppBar extends StatelessWidget {
final VoidCallback onPickImage;
final VoidCallback onSaveImage;
final VoidCallback onDeleteItem;
const MainAppBar({
required this.onPickImage,
required this.onSaveImage,
required this.onDeleteItem,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 100,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
IconButton(
onPressed: onPickImage,
icon: Icon(
Icons.image_search_outlined,
color: Colors.grey[700],
),
),
IconButton(
onPressed: onDeleteItem,
icon: Icon(
Icons.delete_forever_outlined,
color: Colors.grey[700],
),
),
IconButton(
onPressed: onSaveImage,
icon: Icon(
Icons.save,
color: Colors.grey[700],
),
),
],
),
);
}
}
// lib/model/sticker_model.dart
class StickerModel {
final String id;
final String imgPath;
StickerModel({
required this.id,
required this.imgPath,
});
@override
bool operator ==(Object other) {
return (other as StickerModel).id == id;
}
@override
int get hashCode => id.hashCode;
}
참고
이 글은 골든래빗 《Must Have 코드팩토리의 플러터 프로그래밍 2판》의 스터디 내용 입니다.
스터디
Q.
A.