728x90
반응형
이전 포스팅에서 photo_manager 를 통해 기기의 이미지 정보들을 불러와서 일부 기능들을 구현했습니다.
파란 부분까지가 이전 포스팅에서 구현했던 부분이고 이번 포스팅에서는 이미지 선택, 해제 부분을 구현하겠습니다.
- 기기에 저장되어있는 이미지 목록을 불러온다.
- 이때 이미지는 그리드 형태로 한 줄에 3개 보여줍니다.
- 드롭다운 형태로 앨범 이동이 가능해야 하며 '모든 사진'을 볼 수 있어야 한다.
- '모든 사진' 선택 시 첫 번째 칸에는 카메라 촬영을 통해 이미지를 가지고 올 수 있어야 한다.
- 이미지 선택 시 순서가 매겨지며 dim 처리가 되고 상단에 작은 이미지로 보인다.
- 이미지 선택 해제는 선택된 이미지를 누르거나 상단의 이미지의 x 버튼을 눌렀을 때 해제된다.
- 이미지 선택 개수 제한을 정할 수 있어야 한다.
- 확인 버튼을 누르면 선택한 이미지 정보를 이전 페이지에 전달한다.
- 이미지를 선택하지 않고 확인 버튼을 누르면 이미지를 선택해달라는 alert 을 띄운다.
필요한 기능들에 대해 정리해봅니다.
- 카메라 촬영으로 이미지 정보를 가져오는 라이브러리가 필요합니다.
- 이미지 선택 액션을 정의해야 합니다.
- 이미지 선택 시 보이는 수평 스크롤 뷰가 필요합니다.
카메라 촬영 라이브러리는 image_picker 를 이용하여 구현하려고 합니다.
image_pikcer 로도 이미지 선택하는 기능을 만들 수 있지만 위에 적은 요구사항들을 충족시키기엔 어렵습니다.
기본 사용법은 아래 링크에서 확인해주세요.
두 개의 라이브러리를 사용하려고 하니 문제점이 생깁니다.
- 이미지를 선택하거나 카메라 촬영을 통해 가져온 이미지를 보여주는 수평 스크롤 뷰입니다.
- image_picker 를 이용하여 카메라 촬영을 통해 가져오는 이미지는 XFile 입니다.
- photo_manager 를 이용하여 가져오는 이미지는 AssetEntity 입니다.
- 서로 다른 객체지만 보이는 화면은 같아야 합니다.
- 그냥 쉽게 생각해서 두 개를 포함하는 객체를 만들었습니다.
selected_image.dart
- 카메라를 촬영하거나 이미지를 선택하면 저장되는 객체입니다.
- 카메라를 이용해서 가져온 이미지는 entity 가 null 입니다.
- 이미지를 선택해서 가져온 이미지는 file 이 null 입니다.
import 'package:image_picker/image_picker.dart';
import 'package:photo_manager/photo_manager.dart';
class SelectedImage {
AssetEntity? entity;
XFile? file;
SelectedImage({
required this.entity,
required this.file,
});
}
sample_screen.dart
- 선택한 이미지의 상태를 저 정할 변수를 선언해줍니다.
- 이 상태를 이용하여 수평 스크롤 뷰를 만듭니다.
final List<SelectedImage> _selectedImages = [];
horizon_photo.dart
- 선택된 사진을 표현하는 수평 스크롤 뷰를 만듭니다.
- 현재 선택된 이미지의 상태, 스크롤 컨트롤러, 삭제를 처리하는 함수를 받습니다.
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart';
import 'selected_image.dart';
class HorizonPhoto extends StatelessWidget {
List<SelectedImage> selectedImages;
ScrollController scrollController;
ValueChanged<SelectedImage> deleteTap;
HorizonPhoto({
required this.selectedImages,
required this.scrollController,
required this.deleteTap,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return selectedImages.isNotEmpty
? SizedBox(
height: 120,
child: ListView(
controller: scrollController,
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
children: selectedImages
.map((e) => Stack(
alignment: Alignment.center,
children: [
const SizedBox(
width: 100,
height: 100,
),
_horizonPhotoItem(e),
_deleteButton(e),
],
))
.toList(),
),
)
: const SizedBox();
}
Widget _horizonPhotoItem(SelectedImage image) {
return Align(
alignment: Alignment.center,
child: SizedBox(
width: 80,
height: 80,
child: image.entity != null
? AssetEntityImage(
image.entity!,
fit: BoxFit.cover,
)
: Image.file(
File(image.file!.path),
fit: BoxFit.cover,
),
),
);
}
Widget _deleteButton(SelectedImage image) {
return Positioned(
top: 10,
right: 1,
child: GestureDetector(
onTap: () => deleteTap(image),
child: const Icon(
Icons.cancel,
color: Colors.black87,
size: 25,
),
),
);
}
}
grid_photo.dart
- 이미지 선택, 해제를 구현하기 위해 기존 코드를 수정합니다.
- 현재 선택된 이미지인 selectedImages 와 선택/해제를 처리하는 함수를 받습니다.
- GridView 에서 카메라 버튼을 만드는 부분이 있는데 AseetEntity 의 id 가 'camera' 로 구분하여 만듭니다.
- 카메라 버튼을 누르면 _loadCamera() 에서 image_picker 를 이용하여 카메라를 이용해 이미지 정보를 가져옵니다.
- photo_manager 로 만든 _gridPhotoItem 에서는 selectedImages 에 해당된다면 dim처리와 카운팅을 합니다.
- 이미지 선택/해제, 카메라로 이미지를 가져오면 onTap 을 실행합니다.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:photo_manager/photo_manager.dart';
import 'selected_image.dart';
class GridPhoto extends StatelessWidget {
List<AssetEntity> images;
List<SelectedImage> selectedImages;
ValueChanged<SelectedImage> onTap;
GridPhoto({
required this.images,
required this.selectedImages,
required this.onTap,
Key? key,
}) : super(key: key);
final picker = ImagePicker();
void _loadCamera() async {
final file = await picker.pickImage(source: ImageSource.camera);
if (file != null) {
final item = SelectedImage(entity: null, file: file);
onTap(item);
}
}
void _selectImage(AssetEntity e) {
final item = SelectedImage(entity: e, file: null);
onTap(item);
}
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
children: images.map((e) {
if (e.id == 'camera') {
return _cameraButton();
} else {
return _gridPhotoItem(e);
}
}).toList(),
);
}
Widget _cameraButton() {
return GestureDetector(
onTap: _loadCamera,
child: Container(
color: Colors.black,
child: const Icon(
CupertinoIcons.camera,
color: Colors.white,
size: 50,
),
),
);
}
Widget _gridPhotoItem(AssetEntity e) {
return GestureDetector(
onTap: () {
_selectImage(e);
},
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Stack(
children: [
Positioned.fill(
child: AssetEntityImage(
e,
isOriginal: false,
fit: BoxFit.cover,
),
),
_dimContainer(e),
_selectNumberContainer(e)
],
),
),
);
}
Widget _dimContainer(AssetEntity e) {
final isSelected = selectedImages.any((element) => element.entity == e);
return Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: isSelected ? Colors.black38 : Colors.transparent,
border: Border.all(
color: isSelected ? Colors.lightBlue : Colors.transparent,
width: 5,
),
),
),
);
}
Widget _selectNumberContainer(AssetEntity e) {
final num = selectedImages.indexWhere((element) => element.entity == e) + 1;
return Positioned(
right: 10,
top: 10,
child: num != 0
? Container(
padding: const EdgeInsets.all(10.0),
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
child: Text(
'$num',
style: const TextStyle(color: Colors.white),
),
)
: const SizedBox());
}
}
sample_screen.dart
- 이미지 목록을 불러오는 getPhotos 의 변동사항입니다.
- 선택한 앨범이 '모든 사진' 을 나타내는 isAll 속성을 확인합니다.
- isAll 속성이 true 라면 카메라 버튼을 추가하기 위해 AssetEntity id 가 'camera' 인 더미를 만듭니다.
Future<void> getPhotos(
Album album, {
bool albumChange = false,
}) async {
_currentAlbum = album;
albumChange ? _currentPage = 0 : _currentPage++;
final getAlbum = _paths!.singleWhere((element) => element.id == album.id);
final loadImages = await getAlbum.getAssetListPaged(
page: _currentPage,
size: 20,
);
setState(() {
if (albumChange) {
_images = _isAllCheck(loadImages, getAlbum.isAll);
} else {
_images.addAll(loadImages);
}
});
}
List<AssetEntity> _isAllCheck(List<AssetEntity> loadImages, bool isAll) {
if (isAll) {
const dummy = AssetEntity(id: 'camera', typeInt: 0, width: 0, height: 0);
loadImages.insert(0, dummy);
}
return loadImages;
}
- HorizonPhoto 의 deleteTap 과 GridPhoto 의 onTap 이 호출하는 _selectImage 가 추가되었습니다.
- 선택한 이미지가 이미 추가된 이미지인지 확인하기 위해 _addedImageCheck 를 호출합니다.
- _addedImageCheck 가 true 라면 삭제하고 false 면 추가합니다.
- 이미지 추가를 한다면 _scrollController 를 이용해 수평 스크롤 끝으로 이동시킵니다.
void _selectImage(SelectedImage image) {
final addedImageCheck =
_selectedImages.any((e) => _addedImageCheck(image, e));
setState(() {
if (addedImageCheck) {
_selectedImages.removeWhere((e) => _addedImageCheck(image, e));
} else {
final item = SelectedImage(entity: image.entity, file: image.file);
_selectedImages.add(item);
}
});
if (!addedImageCheck && _scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
}
bool _addedImageCheck(SelectedImage image, SelectedImage compareImage) {
return image.entity == compareImage.entity &&
image.file == compareImage.file;
}
- sample_screen.dart 의 전체 코드입니다
import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photo_picker/horizon_photo.dart';
import 'album.dart';
import 'grid_photo.dart';
import 'selected_image.dart';
class SampleScreen extends StatefulWidget {
const SampleScreen({Key? key}) : super(key: key);
@override
State<SampleScreen> createState() => _SampleScreenState();
}
class _SampleScreenState extends State<SampleScreen> {
List<AssetPathEntity>? _paths;
List<Album> _albums = [];
late List<AssetEntity> _images;
int _currentPage = 0;
late Album _currentAlbum;
final List<SelectedImage> _selectedImages = [];
final _scrollController = ScrollController();
Future<void> checkPermission() async {
final PermissionState ps = await PhotoManager.requestPermissionExtend();
if (ps.isAuth) {
await getAlbum();
} else {
await PhotoManager.openSetting();
}
}
Future<void> getAlbum() async {
_paths = await PhotoManager.getAssetPathList(
type: RequestType.image,
);
_albums = _paths!.map((e) {
return Album(
id: e.id,
name: e.isAll ? '모든 사진' : e.name,
);
}).toList();
await getPhotos(_albums[0], albumChange: true);
}
List<AssetEntity> _isAllCheck(List<AssetEntity> loadImages, bool isAll) {
if (isAll) {
const dummy = AssetEntity(id: 'camera', typeInt: 0, width: 0, height: 0);
loadImages.insert(0, dummy);
}
return loadImages;
}
Future<void> getPhotos(
Album album, {
bool albumChange = false,
}) async {
_currentAlbum = album;
albumChange ? _currentPage = 0 : _currentPage++;
final getAlbum = _paths!.singleWhere((element) => element.id == album.id);
final loadImages = await getAlbum.getAssetListPaged(
page: _currentPage,
size: 20,
);
setState(() {
if (albumChange) {
_images = _isAllCheck(loadImages, getAlbum.isAll);
} else {
_images.addAll(loadImages);
}
});
}
void _selectImage(SelectedImage image) {
final addedImageCheck =
_selectedImages.any((e) => _addedImageCheck(image, e));
setState(() {
if (addedImageCheck) {
_selectedImages.removeWhere((e) => _addedImageCheck(image, e));
} else {
final item = SelectedImage(entity: image.entity, file: image.file);
_selectedImages.add(item);
}
});
if (!addedImageCheck && _scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
}
bool _addedImageCheck(SelectedImage image, SelectedImage compareImage) {
return image.entity == compareImage.entity &&
image.file == compareImage.file;
}
@override
void initState() {
super.initState();
checkPermission();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Container(
child: _albums.isNotEmpty
? DropdownButton(
value: _currentAlbum,
items: _albums
.map((e) => DropdownMenuItem(
value: e,
child: Text(e.name),
))
.toList(),
onChanged: (value) => getPhotos(value!, albumChange: true),
)
: const SizedBox()),
),
body: SafeArea(
child: Stack(
children: [
Column(
children: [
HorizonPhoto(
selectedImages: _selectedImages,
scrollController: _scrollController,
deleteTap: _selectImage,
),
Expanded(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scroll) {
final scrollPixels = scroll.metrics.pixels /
scroll.metrics.maxScrollExtent;
if (scrollPixels > 0.7) getPhotos(_currentAlbum);
return false;
},
child: _paths == null
? const Center(child: CircularProgressIndicator())
: GridPhoto(
images: _images,
selectedImages: _selectedImages,
onTap: _selectImage,
),
),
),
],
),
],
),
),
);
}
}
- 디자인은... 좀 그렇지만 원하는 대로 작동이 되는 걸 볼 수 있습니다.
728x90
반응형
'Flutter > Sample' 카테고리의 다른 글
[Flutter] photo picker - ① (7) | 2022.08.12 |
---|