Flutter/Sample

[Flutter] photo picker - ①

찌김이 2022. 8. 12. 23:25
728x90
반응형

photo_manager 를 이용하여 간단한 photo picker 를 구현하려고 합니다.

photo_manager 의 기본 셋팅과 사용법은 아래 링크에서 확인해주세요.

 

 

[Flutter] photo_manager

디바이스의 이미지 정보를 불러오고 싶을 때 유용한 photo_manager 에 대해서 포스팅 합니다. photo_manager | Flutter Package A Flutter plugin that provides assets abstraction management APIs on Android,..

dalgoodori.tistory.com

 

구현하려는 photo picker 의 기능은 다음과 같으며 이번 포스팅에서는 빨간 글씨로 된 부분까지만 구현하겠습니다.

  • 기기에 저장되어있는 이미지 목록을 불러온다.
  • 이때 이미지는 그리드 형태로 한줄에 3개 보여줍니다.
  • 드롭다운 형태로 앨범 이동이 가능해야 하며 '모든 사진'을 볼 수 있어야 한다.
  • '모든 사진' 선택 시 첫번째 칸에는 카메라 촬영을 통해 이미지를 가지고 올 수 있어야 한다.
  • 이미지 선택 시 순서가 매겨지며 dim 처리가 되고 상단에 작은 이미지로 보인다.
  • 이미지 선택 해제는 선택된 이미지를 누르거나 상단의 이미지의 x 버튼을 눌렀을 때 해제된다.
  • 이미지 선택 개수 제한을 정할 수 있어야 한다.
  • 확인 버튼을 누르면 선택한 이미지 정보를 이전 페이지에 전달한다.
  • 이미지를 선택하지 않고 확인 버튼을 누르면 이미지를 선택해달라는 alert 을 띄운다.

 

필요한 것들을 정리해봅니다.

  • 파일 접근에 대한 권한 확인
  • 권한 수락 후 PhotoManager.getAssetPathList() 를 통해 받아오는 AssetPathEntity 목록
  • 드롭다운으로 앨범 이동을 구현해야 하니 앨범 id 와 앨범 name 을 가진 클래스의 목록
  • 선택된 앨범
  • 선택된 앨범의 이미지 목록 
  • 이미지 목록의 현재 페이지

 

album.dart

  • 앨범 id 와 앨범 name 을 가진 클래스를 만들어 줍니다.
class Album {
  String id;
  String name;

  Album({
    required this.id,
    required this.name,
  });
}

 

sample_screen.dart

  • 상태를 활용해야 하기 때문에 StatefulWidget 으로 만듭니다. 
  • 위에서 언급한 필요한 변수들을 선언합니다.
import 'package:flutter/material.dart';

import 'album.dart';

class PhotoSelectScreen extends StatefulWidget {
  const PhotoSelectScreen({Key? key}) : super(key: key);

  @override
  State<PhotoSelectScreen> createState() => _PhotoSelectScreenState();
}

class _PhotoSelectScreenState extends State<PhotoSelectScreen> {
  List<AssetPathEntity>? _paths; // 모든 파일 정보
  List<Album> _albums = []; // 드롭다운 앨범 목록
  late List<AssetEntity> _images; // 앨범의 이미지 목록
  int _currentPage = 0; // 현재 페이지
  late Album _currentAlbum; // 드롭다운 선택된 앨범
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child : Text('Photo Picker')
      ),
    );
  }
}

 

권한 확인

  • 파일 접근에 대한 권한을 가져오기 위해 initState 에서 권한을 확인합니다.
  • initState 에서는 비동기 작업을 할 수 없으니 따로 만들어서 권한을 확인합니다.
@override
void initState() {
  super.initState();
  checkPermission();
}

// 권한 확인
Future<void> checkPermission() async {
  final PermissionState ps = await PhotoManager.requestPermissionExtend();
  
  if (ps.isAuth) {
    // 권한 수락
    await getAlbum();
  } else {
    // 권한 거절
    await PhotoManager.openSetting(); 
  }
}

 

AssetPathEntity 목록, 드롭다운 앨범 목록

  • PhotoManager.getAssetPathList() 를 통해 모든 파일의 정보를 불러옵니다.
  • 이 정보를 이용하여 드롭다운에 사용될 앨범의 목록을 만들어 줍니다. 
  • AssetPathEntity 에서 '모든 사진'은 isAll 이 true 일 때이며 기본 name 인 'Recents' 가 아닌 '모든 사진' 으로 넣어줍니다.
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);
}

 

앨범의 이미지 목록

  • 앨범의 이미지 목록을 불러오는 함수이며 호출되는 시기는 다음과 같습니다.
  • 이 페이지에 처음 진입했을 때
  • 드롭다운으로 다른 앨범을 선택했을 때
  • 스크롤이 끝에 다다랐을 때
Future<void> getPhotos(
    Album album, {
    bool albumChange = false,
}) async {

  _currentAlbum = album;
  albumChange ? _currentPage = 0 : _currentPage++;

  final loadImages = await _paths!
      .singleWhere((AssetPathEntity e) => e.id == album.id)
      .getAssetListPaged(
    page: _currentPage,
    size: 20,
  );

  setState(() {
    if (albumChange) {
      _images = loadImages;
    } else {
      _images.addAll(loadImages);
    }
  });
}
  • Album 과 앨범의 변경 여부를 판단하는 albumChange 를 인자로 받으며 디폴트 값으로 false 로 지정했습니다.
  • 이미지 목록은 _paths 를 singleWhere 를 이용하여 AssetPathEntity 의 id 값을 통해 getAssetListPaged() 으로 불러옵니다. 
  • 앨범이 변경되었다면 불러온 이미지를 _images 에 set 해주고 아니라면 addAll 해준 후 setState 해줍니다.

 

드롭다운 구현

  • 예제에서는 AppBar 를 통해 구현하겠습니다.
  •  _albums 가 비어 있지 않다면 드롭다운 버튼이 생겨납니다.
  • 드롭다운 메뉴를 변경하면 onChanged 가 호출되고 그때마다 getPhotos 를 호출합니다.

 

  @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: (Album? value) => getPhotos(value!, albumChange: true),
            ) : const SizedBox()),
      ),
      body: Center(
        child : Text('Photo Picker')
      ),
    );
  }

 

이미지를 보여줄 GridView 구현

  • List<AssetEntity> 를 인자로 받으며 이후 이미지 선택 처리를 위해 StatefulWidget 으로 만듭니다.
  • 받은 이미지를 AssetEntityImage 를 통해 구현하면 끝납니다.
import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart';

class GridPhoto extends StatefulWidget {
  List<AssetEntity> images;

  GridPhoto({
    required this.images,
    Key? key,
  }) : super(key: key);

  @override
  State<GridPhoto> createState() => _GridPhotoState();
}

class _GridPhotoState extends State<GridPhoto> {
  @override
  Widget build(BuildContext context) {
    return GridView(
      physics: const BouncingScrollPhysics(),
      gridDelegate:
          const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
      children: widget.images.map((AssetEntity e) {
        return AssetEntityImage(
          e,
          isOriginal: false,
          fit: BoxFit.cover,
        );
      }).toList(),
    );
  }
}

 

스크롤 감지 구현

  • NotificationListener 위젯을 통하여 구현합니다.
  • onNotification 을 통해 스크롤의 끝과 스크롤의 현재 위치를 받을 수 있습니다.
  • 현재 스크롤 위치와 스크롤 끝을 나눕니다. 스크롤이 끝에 가까워질수록 1에 근접합니다. 
  • 스크롤이 다다를 때쯤인 0.7 정도에 다음 페이지의 이미지를 불러옵니다.
  • child 는 위에서 구현한 GridPhoto 로 해놓습니다.
@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: NotificationListener<ScrollNotification>(
      onNotification: (ScrollNotification scroll) {
      
        // 현재 스크롤 위치 - scroll.metrics.pixels
        // 스크롤 끝 위치 scroll.metrics.maxScrollExtent
        final scrollPixels =
            scroll.metrics.pixels / scroll.metrics.maxScrollExtent;

        if (scrollPixels > 0.7) getPhotos(_currentAlbum);

        return false;
      },
      child: SafeArea(
        child: _paths == null
            ? const Center(child: CircularProgressIndicator())
            : GridPhoto(images: _images),
      ),
    ),
  );
}

 

결과

  • 정상적으로 구동되는 것을 볼 수 있습니다.
  • 나머지 부분은 다음 포스팅에서 진행하겠습니다!

 

 

 

지금까지 구현한 코드입니다.

sample_screen.dart

import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart';

import 'album.dart';
import 'grid_photo.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;

  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);
  }

  Future<void> getPhotos(
      Album album, {
        bool albumChange = false,
      }) async {

    _currentAlbum = album;
    albumChange ? _currentPage = 0 : _currentPage++;

    final loadImages = await _paths!
        .singleWhere((element) => element.id == album.id)
        .getAssetListPaged(
      page: _currentPage,
      size: 20,
    );

    setState(() {
      if (albumChange) {
        _images = loadImages;
      } else {
        _images.addAll(loadImages);
      }
    });
  }

  @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: NotificationListener<ScrollNotification>(
        onNotification: (ScrollNotification scroll) {
          final scrollPixels =
              scroll.metrics.pixels / scroll.metrics.maxScrollExtent;

          print('scrollPixels = $scrollPixels');
          if (scrollPixels > 0.7) getPhotos(_currentAlbum);

          return false;
        },
        child: SafeArea(
          child: _paths == null
              ? const Center(child: CircularProgressIndicator())
              : GridPhoto(images: _images),
        ),
      ),
    );
  }
}

 

album.dart

class Album {
  String id;
  String name;

  Album({
    required this.id,
    required this.name,
  });
}

 

grid_photo.dart

import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart';

class GridPhoto extends StatefulWidget {
  List<AssetEntity> images;

  GridPhoto({
    required this.images,
    Key? key,
  }) : super(key: key);

  @override
  State<GridPhoto> createState() => _GridPhotoState();
}

class _GridPhotoState extends State<GridPhoto> {
  @override
  Widget build(BuildContext context) {
    return GridView(
      physics: const BouncingScrollPhysics(),
      gridDelegate:
          const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
      children: widget.images.map((e) {
        return AssetEntityImage(
          e,
          isOriginal: false,
          fit: BoxFit.cover,
        );
      }).toList(),
    );
  }
}
728x90
반응형

'Flutter > Sample' 카테고리의 다른 글

[Flutter] photo picker - ②  (0) 2022.08.14