Foggy day

[Flutter] PageView Widget - 사용법 본문

Flutter/Flutter widget

[Flutter] PageView Widget - 사용법

jinhan38 2023. 3. 27. 00:09

 

 

 

이번 포스팅에서는 PageView Widget에 대해 알아보겠습니다.

 

PageView는 페이지를 좌우나 상하로 애니메이션 전환할 수 있는 위젯입니다. 대부분의 앱에서 사용하는 기능입니다. 

 

 

 

1. 기본 사용법

2. 스크롤 방향 전환(scrollDirection)

3. 기타 특성 

4. PageController 정보 확인

5. PageController 페이지 이동

 

 

 

1. 기본 사용법

기본적인 사용법은 간단합니다. PageView를 생성하고 children으로 사용할 위젯들을 넣어주기만 하면 됩니다. 그러면 좌우로 스와이프 해서 화면을 변경할 수 있는 PageView가 됩니다.

import 'package:flutter/material.dart';

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

  @override
  State<PageViewScreen> createState() => _PageViewScreenState();
}

class _PageViewScreenState extends State<PageViewScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("PageViewScreen"),
      ),
      body: _pageView(),
    );
  }

  Widget _pageView() {
    return PageView(
      children: [
        Container(color: Colors.red),
        Container(color: Colors.green),
        Container(color: Colors.orange),
      ],
    );
  }
}

 

 

 

2. 스크롤 방향 전환(scrollDirection)

페이지뷰의 스크롤 방향은 기본적으로 좌우입니다. 만약 위아래로 변경하고 싶은 경우 scrollDirection 특성을 사용하면 됩니다. 

scrollDirection: Axis.vertical

 

 

 

3. 기타 특성 

특성들 중에 pageSnapping 특성은 PageController로 offset을 이동시킬 때 설정이 필요합니다. pageSnapping을 false로 설정해야 jumpTo와 animateTo 함수를 사용할 수 있습니다.

  /// children들의 순서를 반대로 변경
  reverse: false,

  onPageChanged: (index) {
    /// 페이지가 변경될 때 마다 호출됨
  },

  /// PageController의 viewportFraction과 함께 사용됨
  /// viewportFraction으로 작아진 위젯들 때문에 페이지뷰 좌우에 패딩이 발생
  /// padEnds를 false로 주면 페이지뷰 좌우 패딩 제거
  // padEnds: false,

  /// 페이지뷰의 화면 전환 효과 제거
  /// 스크롤뷰처럼 동작하게 됨 
  // pageSnapping: false,

 

 

 

4. PageController 정보 확인

PageController에는 정보와 기능이 있습니다. PageController의 listener는 프레임 변경마다 콜백을 호출하기 때문에 실시간으로 PageView의 상태를 알 수 있습니다.

 

  • pageController.page : PageView의 현재 page를 알 수 있습니다. 만약 1페이지에서 2페이지로 이동한다면, 이동 전의 page = 1, 이동 후의 page = 2가 됩니다. listener를 이용해서 실시간 콜백으로 확인할 수도 있습니다. 변경 도중에 콜백을 통해 page를 확인해 보면 프레임의 변경마다 1~2의 사이의 값이 호출됩니다. controller의 listener에서 print를 찍어봤습니다.
  @override
  void initState() {
    pageController.addListener(() {
      print('page : ${pageController.page}');
    });
    super.initState();
  }
  I/flutter (14266): page : 1.00555419921875
  I/flutter (14266): page : 1.024993896484375
  I/flutter (14266): page : 1.051849365234375
  I/flutter (14266): page : 1.0870361328125002
  I/flutter (14266): page : 1.1277770996093752
  I/flutter (14266): page : 1.1685180664062502
  I/flutter (14266): page : 1.2037048339843752
  I/flutter (14266): page : 1.2962957535239736
  I/flutter (14266): page : 1.381852844947847
  I/flutter (14266): page : 1.4594063752736453
  I/flutter (14266): page : 1.5289818794575059
  I/flutter (14266): page : 1.5906523631529235
  I/flutter (14266): page : 1.6451024021984901
  I/flutter (14266): page : 1.6928097238671787
  
  ...

 

  • pageControll.offset : offset은 PageView 시작점부터(0) 현재 페이지의 떨어진 거리입니다.
    PageView의 가로 사이즈가 400이라고 가정해보겠습니다. 그럼
    0 페이지에서의 offset은 0.
    1 페이지에서의 offset은 400.
    2 페이지에서의 offset은 800이 됩니다. 
    그리고 이 또한 실시간으로 값을 알 수 있습니다.
  @override
  void initState() {
    pageController.addListener(() {
      print('offset : ${pageController.offset}');
    });
    super.initState();
  }

0 -> 1페이지로 이동하는 도중 

I/flutter (14266): offset : 1.142578125
I/flutter (14266): offset : 3.427734375
I/flutter (14266): offset : 10.295758928571445
I/flutter (14266): offset : 17.904575892857167
I/flutter (14266): offset : 31.238839285714334
I/flutter (14266): offset : 43.807198660714334
I/flutter (14266): offset : 60.5691964285715
I/flutter (14266): offset : 79.23967633928578
I/flutter (14266): offset : 99.42940848214295
I/flutter (14266): offset : 119.61914062500011
I/flutter (14266): offset : 136.38113839285725
I/flutter (14266): offset : 153.1431361607144

...

 

 

그 외에도 pageController.position을 사용하면 몇 가지 정보를 더 얻을 수 있습니다. 

  • pageController.position.maxScrollExtent : 페이지뷰 offset의 최대 사이즈
  • pageController.position.minScrollExtent : 페이지뷰 offset의 최소 사이즈
  • pageController.position.atEdge :
    true = 시작 페이지나 마지막 페이지의 끝에 도달한 경우
    false : 끝에 도달하지 않은 경우
  • pageController.position.userScrollDirection : 유저의 스크롤/스와이프 방향
    reverse = page값이 커지는 경우
    forward : page값이 작아지는 경우
  // 페이지뷰 offset의 최대 사이즈
  pageController.position.maxScrollExtent;

  // 페이지뷰 offset의 최소 사이즈
  pageController.position.minScrollExtent;

  // 끝에 도달한 경우 호출됨
  pageController.position.atEdge;

  // 유저의 스크롤/스와이프 방향,
  // reverse : page값이 커지는 경우
  // forward : page값이 작아지는 경우
  pageController.position.userScrollDirection;

 

 

 

5. PageController 페이지 이동

PageController는 PageView의 page를 이동시킬 수도 있습니다. page 변경 함수는 6개가 있습니다.

  • nextPage : 다음 페이지로 이동
  • previousPage : 이전 페이지로 이동
  • jumpTo : 입력한 offset으로 이동, 애니메이션 없음
  • jumpToPage : 입력한 page로 이동, 애니메이션 없음
  • animateTo : 입력한 offset으로 이동, 애니메이션 있음
  • animateToPage : 입력한 page로 이동, 애니메이션 있음

이중에서 jumpTo와 animateTo는 페이지뷰의 특정 offset으로 이동시키는 함수입니다. 페이지를 변경시키는 것이 아니기 때문에 pageSnapping 특성을 false로 해야 합니다. 

 

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("PageViewScreen"),
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Expanded(child: _pageView()),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                onPressed: () {
                  pageController.previousPage(
                      duration: const Duration(milliseconds: 600),
                      curve: Curves.easeIn);
                },
                child: const Text("previous"),
              ),
              ElevatedButton(
                onPressed: () {
                  pageController.nextPage(
                      duration: const Duration(milliseconds: 600),
                      curve: Curves.easeIn);
                },
                child: const Text("next"),
              ),
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                onPressed: () {
                  pageController.jumpTo(100);
                },
                child: const Text("jumpTo"),
              ),
              ElevatedButton(
                onPressed: () {
                  pageController.jumpToPage(1);
                },
                child: const Text("jumpToPage"),
              ),
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                onPressed: () {
                  pageController.animateTo(
                    200,
                    curve: Curves.easeIn,
                    duration: const Duration(milliseconds: 600),
                  );
                },
                child: const Text("animateTo"),
              ),
              ElevatedButton(
                onPressed: () {
                  pageController.animateToPage(
                    2,
                    curve: Curves.easeIn,
                    duration: const Duration(milliseconds: 600),
                  );
                },
                child: const Text("animateToPage"),
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _pageView() {
    return PageView(
      controller: pageController,

      // 페이지뷰의 화면 전환 애니메이션 제거
      // 스크롤뷰처럼 동작하게 됨
      pageSnapping: false,

      children: [
        Container(color: Colors.red),
        Container(color: Colors.green),
        Container(color: Colors.orange),
      ],
    );
  }

 

 

 

 

최종 동영상

 

 

 

 

Full code

import 'package:flutter/material.dart';

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

  @override
  State<PageViewScreen> createState() => _PageViewScreenState();
}

class _PageViewScreenState extends State<PageViewScreen> {
  PageController pageController = PageController(
    initialPage: 0,
    keepPage: true,
    viewportFraction: 1,
  );

  @override
  void initState() {
    pageController.addListener(() {
      // PageView의 현재 page
      // ex) 1 -> 2 페이지로 변경 시
      // 프레임이 변경될 때 마다 실시간으로 page가 어디까지 변경됐는지를 알 수 있음
      pageController.page;

      // PageView 시작점부터(0) 현재 페이지의 떨어진 거리
      // ex) PageView의 가로 사이즈 400
      // 0 페이지의 offset = 0
      // 1 페이지의 offset = 400
      // 2 페이지의 offset = 800
      pageController.offset;

      // 페이지뷰 offset의 최대 사이즈
      pageController.position.maxScrollExtent;

      // 페이지뷰 offset의 최소 사이즈
      pageController.position.minScrollExtent;

      // true : 시작 페이지나 마지막 페이지의 끝에 도달한 경우
      // false : 끝에 도달하지 않은 경우
      pageController.position.atEdge;

      // 유저의 스크롤/스와이프 방향,
      // reverse : page값이 커지는 경우
      // forward : page값이 작아지는 경우
      pageController.position.userScrollDirection;

      // PageController가 PageView에 추가가 됐는지 확인
      pageController.hasClients;
      setState(() {});
    });

    super.initState();
  }

  @override
  void dispose() {
    pageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("PageViewScreen"),
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Expanded(child: _pageView()),
          const SizedBox(height: 10),
          Text(
            "page : ${pageController.hasClients ? pageController.page?.toStringAsFixed(2) : 0}",
            style: const TextStyle(fontSize: 25),
          ),
          Text(
            "offset : ${pageController.hasClients ? pageController.offset.toStringAsFixed(2) : 0}",
            style: const TextStyle(fontSize: 25),
          ),
          const SizedBox(height: 10),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                onPressed: () {
                  pageController.previousPage(
                      duration: const Duration(milliseconds: 600),
                      curve: Curves.easeIn);
                },
                child: const Text("previous"),
              ),
              ElevatedButton(
                onPressed: () {
                  pageController.nextPage(
                      duration: const Duration(milliseconds: 600),
                      curve: Curves.easeIn);
                },
                child: const Text("next"),
              ),
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                onPressed: () {
                  pageController.jumpTo(100);
                },
                child: const Text("jumpTo"),
              ),
              ElevatedButton(
                onPressed: () {
                  pageController.jumpToPage(1);
                },
                child: const Text("jumpToPage"),
              ),
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                onPressed: () {
                  pageController.animateTo(
                    200,
                    curve: Curves.easeIn,
                    duration: const Duration(milliseconds: 600),
                  );
                },
                child: const Text("animateTo"),
              ),
              ElevatedButton(
                onPressed: () {
                  pageController.animateToPage(
                    2,
                    curve: Curves.easeIn,
                    duration: const Duration(milliseconds: 600),
                  );
                },
                child: const Text("animateToPage"),
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _pageView() {
    return PageView(
      controller: pageController,
      scrollDirection: Axis.horizontal,

      // children들의 순서를 반대로 변경
      reverse: false,

      onPageChanged: (index) {
        // 페이지가 변경될 때 마다 호출됨
      },

      // PageController의 viewportFraction과 함께 사용됨
      // viewportFraction으로 작아진 위젯들 때문에 페이지뷰 좌우에 패딩이 발생
      // padEnds를 false로 주면 페이지뷰 좌우 패딩 제거
      // padEnds: false,

      // 페이지뷰의 화면 전환 효과 제거
      // 스크롤뷰처럼 동작하게 됨
      pageSnapping: false,

      children: [
        Container(color: Colors.red),
        Container(color: Colors.green),
        Container(color: Colors.orange),
      ],
    );
  }
}