Skip to content

Commit

Permalink
- Image Picker 대신 Photo Manager 사용
Browse files Browse the repository at this point in the history
- 이미지 로딩 개수 제한 걺 (50장씩 로딩)
- 스크롤 시 추가 로딩
- 프리뷰 상태에서 '사용' 선택시 사진 채택되며 갤러리에서 나가짐
- 프리뷰 상태에서 '다시 선택' 누를 시 프리뷰 종료되며 갤러리에 잔류
  • Loading branch information
JerraldKim committed Sep 7, 2024
1 parent a1a4f85 commit a6ae596
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 179 deletions.
14 changes: 7 additions & 7 deletions lib/feature/ImageConfirmationDialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class ImageConfirmationDialog extends StatelessWidget {
padding: const EdgeInsets.all(16.0),
child: Text(
'이 사진을 사용하시겠습니까?',
style: TextStyle(
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
Expand All @@ -41,17 +41,17 @@ class ImageConfirmationDialog extends StatelessWidget {
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
ref.read(imageFileProvider.notifier).clearImage();
Navigator.of(context).pop(); // 프리뷰 닫고 갤러리에 남기
},
child: Text('다시 선택'),
child: const Text('다시 선택'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
ref.read(imageFileProvider.notifier).state = imageFile;
Navigator.of(context).pop(); // 프리뷰 닫기
ref.read(imageFileProvider.notifier).state = imageFile; // 선택된 이미지 프로필 사진으로 지정하기
Navigator.of(context).pop(); // 갤러리에서 나가기
},
child: Text('사용'),
child: const Text('사용'),
),
],
),
Expand Down
123 changes: 123 additions & 0 deletions lib/feature/ImageGalleryScreen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:io';
import 'ImageImporter.dart';
import 'ImageConfirmationDialog.dart';

class ImageGalleryScreen extends StatefulWidget {
final WidgetRef ref;

ImageGalleryScreen({required this.ref});

@override
_ImageGalleryScreenState createState() => _ImageGalleryScreenState();
}

class _ImageGalleryScreenState extends State<ImageGalleryScreen> {
List<AssetEntity> images = [];
ScrollController _scrollController = ScrollController();
int currentPage = 0;
bool isLoading = false;
bool hasMore = true;

@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll); // 스크롤 리스너
_fetchPhotos(); // 초기 배치 로드
}

Future<void> _fetchPhotos({int limit = 50}) async {
if (isLoading || !hasMore) return;

setState(() {
isLoading = true;
});

// Photo Manager 사용해서 이미지 불러오기
List<AssetEntity> newImages = await widget.ref
.read(imagePickerServiceProvider)
.fetchImages(page: currentPage, limit: limit);

setState(() {
if (newImages.isEmpty) {
hasMore = false; // 더 이상 불러올 이미지 없음
} else {
images.addAll(newImages); // 리스트에 이미지 추가
currentPage++; // 다음 페이지로 넘어가기
}
isLoading = false;
});
}

void _onScroll() {
if (_scrollController.position.extentAfter < 300 && !isLoading && hasMore) {
_fetchPhotos(); // 추가 사진 불러오는 트리거
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('프로필 사진을 고르세요'),
),
body: GridView.builder(
controller: _scrollController,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4, // 한 줄에 이미지 4개
),
itemCount: images.length + (hasMore ? 1 : 0), // 로딩 아이콘 들어가는 공간 확보
itemBuilder: (context, index) {
if (index == images.length) {
// 로딩 아이콘 보여줌
return const Center(child: CircularProgressIndicator());
}

return FutureBuilder(
future: images[index].thumbnailDataWithSize(ThumbnailSize(200, 200)),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.data != null) {
return GestureDetector(
onTap: () async {
File? fullResolutionFile = await images[index].file;
if (fullResolutionFile != null) {
_showImageConfirmationDialog(context, fullResolutionFile);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('이미지를 불러 올 수 없습니다.')),
);
}
},
child: Image.memory(
snapshot.data!,
fit: BoxFit.cover,
),
);
} else {
return const CircularProgressIndicator();
}
},
);
},
),
);
}

void _showImageConfirmationDialog(BuildContext context, File imageFile) {
showDialog(
context: context,
builder: (BuildContext context) {
return ImageConfirmationDialog(imageFile: imageFile, ref: widget.ref);
},
);
}

@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
62 changes: 10 additions & 52 deletions lib/feature/ImageImporter.dart
Original file line number Diff line number Diff line change
@@ -1,55 +1,25 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:photo_manager/photo_manager.dart';
import 'dart:io';

final imagePickerServiceProvider = Provider((ref) => ImagePickerService());

final imageFileProvider =
StateNotifierProvider<ImageFileNotifier, File?>((ref) {
final imageFileProvider = StateNotifierProvider<ImageFileNotifier, File?>((ref) {
final imagePickerService = ref.watch(imagePickerServiceProvider);
return ImageFileNotifier(imagePickerService);
});

class ImagePickerService {
final ImagePicker _picker = ImagePicker();

Future<File?> pickImageFromGallery(BuildContext context) async {
try {
final XFile? pickedFile =
await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
return File(pickedFile.path);
} else {
_showErrorSnackbar(context, '선택된 사진이 없습니다.');
return null;
}
} catch (e) {
_showErrorSnackbar(context, '사진 불러오기에 실패했습니다: $e');
return null;
}
}

Future<File?> pickImageFromCamera(BuildContext context) async {
try {
final XFile? pickedFile =
await _picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) {
return File(pickedFile.path);
} else {
_showErrorSnackbar(context, '찍은 사진이 없습니다.');
return null;
Future<List<AssetEntity>> fetchImages({int page = 0, int limit = 50}) async {
final PermissionState permission = await PhotoManager.requestPermissionExtend();
if (permission.isAuth) {
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(type: RequestType.image);
if (albums.isNotEmpty) {
final AssetPathEntity album = albums.first;
return await album.getAssetListPaged(page: page, size: limit);
}
} catch (e) {
_showErrorSnackbar(context, '사진 찍기에 실패했습니다: $e');
return null;
}
}

void _showErrorSnackbar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
return [];
}
}

Expand All @@ -58,18 +28,6 @@ class ImageFileNotifier extends StateNotifier<File?> {

ImageFileNotifier(this._imagePickerService) : super(null);

Future<File?> pickImageFromGallery(BuildContext context) async {
final file = await _imagePickerService.pickImageFromGallery(context);
state = file;
return file; // 선택된 파일 return
}

Future<File?> pickImageFromCamera(BuildContext context) async {
final file = await _imagePickerService.pickImageFromCamera(context);
state = file;
return file; // 선택된 파일 return
}

void clearImage() {
state = null;
}
Expand Down
Loading

0 comments on commit a6ae596

Please sign in to comment.