Flutter - Scroll Controller 사용 방법, 스크롤 특정 위치로 이동하기, 샘플 코드

반응형

개요

Scroll Controller(스크롤 컨트롤러)를 이용하여 스크롤의 특정 위치로 이동하는 방법을 알아보자.

포스텅에서 설명하는 스크롤 컨트롤러는 위치 조정에 대한 메서드와 속성을 다룬다.

 

포스팅에서 다루는 프로젝트는 깃허브에서 다운 받을 수 있다.

 

GitHub - luvris2/flutter-example

Contribute to luvris2/flutter-example development by creating an account on GitHub.

github.com


Scroll Controller

설명 및 정의

스크롤 컨트롤러는 스크롤 가능한 위젯을 제어하는 기능을 한다.

주로 스크롤 위치 조정과 스크롤 시 애니메이션 제어 등의 기능을 제공한다.

 

스크롤 컨트롤러 선언 & 정의 방법은 아래와 같다.

ScrollController 변수명 = ScrollController();

생성 속성

  • initialScrollOffset
    • 초기 스크롤의 위치를 의미한다.
    • 입력 타입은 double이다.
  • keepScrollOffset
    • 페이지가 이동되었을 경우 스크롤 위치 유지 여부를 설정한다.
    • 입력 타입은 bool이다.
    • 스크롤 위치는 PageStorage에 저장되어 해당 페이지로 다시 전환될 때 복원된다.

* PageStorage : 위젯이 삭제된 후 지속 상태를 선택할 수 있는 하위 트리를 설정한다. 위젯보다 오래 지속될 수 있는 값을 저장하고 복원하는데 사용하는 클래스이다.

// 속성 설정 예시
ScrollController scrollController = ScrollController(
    initialScrollOffset: 100.0, // 초기 스크롤 위치는 100에서 시작
    keepScrollOffset: true, // 페이지 이동 시 현재 스크롤 위치 유지
);

스크롤 위치 제어 메서드

이동 방법은 크게 두 가지로 나뉜다. 선호도에 맞게 사용하면 된다.

포스팅에서는 부드럽게 애니메이션을 주어 이동할 것이므로 animateTo 메서드를 사용하려 한다.

  • jumpTo : 특정 위치로 즉시 이동한다.
  • animateTo : 애니메이션 효과를 사용하여 특정 위치로 이동한다.
/* 스크롤 위치 제어 메서드 설정 예시 */
// jumpTo 메서드 : 스크롤의 위치를 100으로 즉시 이동
scrollController.jumpTo(100.0);

// animateTo 메서드 : 스크롤의 위치를 100으로 천천히 0.5초동안 애니메이션을 주며 이동
scrollController.animateTo(100.0,
    duration: const Duration(milliseconds: 500), curve: Curves.ease);

샘플 코드 : 스크롤 제어하기

요구 사항

  • 리스트의 내용을 스크롤 할 수 있어야 한다.
  • 스크롤의 위치를 맨 위, 맨 밑 등으로 제어할 수 있어야 한다.

UI 설계

 

[ body ]

리스트 내에 표시할 내용

  • ListView : 리스트를 표시하기 위한 위젯
    • Container : 각각의 내용물의 영역을 표시하기 위한 위젯
      • Text : 각각의 내용물의 내용을 표시하기 위한 위젯

[ fab ]

스크롤 위치 제어에 사용될 fab (플로팅 액션 버튼)

  • Column : 플로팅 액션 버튼을 여러 개 생성하기 위한 위젯
    • FloatingActionButton : 사용자와 상호작용하여 스크롤 제어 기능 수행을 위한 위젯

기능 설계

리스트의 내용은 총 10개로 제한하며, 각 리스트에는 1부터 10까지의 내용을 포함한다.

각각의 내용을 포함하는 위젯의 높이는 200으로 고정하며, 위 아래의 여백은 각각 50으로 정의한다.

(총 높이 300, 하나씩 리스트를 이동하기 위함)

스크롤을 제어하는 기능은 다음과 같이 분류한다.

  • 맨 위로 이동 : 스크롤의 가장 위로 이동한다.
  • 위로 이동 : 이전 내용물의 위치로 이동한다.
  • 중간으로 이동 : 스크롤의 중앙의 위치로 이동한다.
  • 아래로 이동 : 다음 내용물의 위치로 이동한다.
  • 맨 아래로 이동 : 스크롤의 가장 아래로 이동한다.
  • 특정 위치로 이동 : 지정한 특정 위치로 이동한다. 포스팅에서는 150의 위치로 지정하였다.

코딩

사용될 함수 선언

  • 스크롤 컨트롤러
  • 리스트 내용
ScrollController scrollController = ScrollController();
List value = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

 

바디 영역과 fab 영역 나누기

  • body : ListView를 표시할 영역
  • floatingActionButton : 스크롤 제어 기능을 수행하는 버튼 표시 영역
@override
Widget build(BuildContext context) {
    return Scaffold(
      body: mainBody(),
      floatingActionButton: fab(),
    );
}

 

바디 영역

  • 리스트뷰 빌더를 사용하여 리스트의 값을 인덱스를 이용하 차례대로 생성
    • 가독성을 위해 컨테이너 영역을 위젯 함수로 분리하였음

컨테이너 영역

  • 리스트뷰 빌더에서 넘겨받은 값을 이용하여 내용 구성
  • 각 리스트마다 색상을 다루게 주어 시각적으로 식별할 수 있도록 함
  // 바디 영역
  Widget mainBody() {
    return ListView.builder(
      controller: scrollController,
      itemCount: value.length,
      itemBuilder: (context, index) {
        return listContainer(value[index], index);
      },
    );
  }
  
    // 바디 영역의 리스트 컨테이너
  Widget listContainer(value, index) {
    return Container(
      margin: const EdgeInsets.fromLTRB(0, 50, 0, 50),
      width: double.infinity,
      height: 200,
      color: Colors.amber[(index + 1) * 100],
      child: Center(
        child: Text(
          value.toString(),
          style: const TextStyle(fontSize: 30),
        ),
      ),
    );
  }

 

fab 영역

  • fab 위젯 선언 코드가 반복되므로 함수로 처리

위젯 생성 함수 (floatingActionButton)

  • fab 위젯 생성
  • 스크롤 제어 기능을 정의한 함수(activeScroll)를 호출하여 기능 수행

스크롤 제어 기능 정의 함수 (activeScroll)

  • 기능을 수행하기 위한 함수
    • 맨위로, 위로, 중간으로, 아래로, 맨아래로, 특정위치
  • 스크롤 이동 메서드가 반복되어 사용하며 코드가 지저분해지므로 스크롤 이동 메서드를 따로 함수로 처리

스크롤 이동 정의 함수 (moveScroll)

  • animateTo 메서드를 이용하여 특정 위치로 이동하는 기능을 수행하는 함수
  // 플로팅 액션 버튼 구성
  Widget fab() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.end,
      children: <Widget>[
        floatingActionButton("맨위로"),
        floatingActionButton("위로"),
        floatingActionButton("중간으로"),
        floatingActionButton("아래로"),
        floatingActionButton("맨아래로"),
        floatingActionButton("특정위치"),
      ],
    );
  }
  
  // 플로팅 액션 버튼 위젯
  Widget floatingActionButton(String text) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: FloatingActionButton(
          onPressed: () {
            activeScroll(text);
          },
          child: Text(text)),
    );
  }
  
  // 플로팅 액션 버튼 기능 정의
  void activeScroll(text) {
    if (text == "맨위로") {
      moveScroll(0);
    } else if (text == "위로") {
      moveScroll(scrollController.offset - 300);
    } else if (text == "중간으로") {
      moveScroll(scrollController.position.maxScrollExtent / 2);
    } else if (text == "아래로") {
      moveScroll(scrollController.offset + 300);
    } else if (text == "맨아래로") {
      moveScroll(scrollController.position.maxScrollExtent);
    } else if (text == "특정위치") {
      moveScroll(150);
    }
  }
  
  // 스크롤 이동 기능 정의
  void moveScroll(value) {
    scrollController.animateTo(value.toDouble(),
        duration: const Duration(milliseconds: 500), curve: Curves.ease);
  }

전체 소스 코드

import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      home: MainPage(),
    ),
  );
}

class MainPage extends StatefulWidget {
  const MainPage({super.key});

  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  ScrollController scrollController = ScrollController();
  List value = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: mainBody(),
      floatingActionButton: fab(),
    );
  }

  // 바디 영역
  Widget mainBody() {
    return ListView.builder(
      controller: scrollController,
      itemCount: value.length,
      itemBuilder: (context, index) {
        return listContainer(value[index], index);
      },
    );
  }

  // 바디 영역의 리스트 컨테이너
  Widget listContainer(value, index) {
    return Container(
      margin: const EdgeInsets.fromLTRB(0, 50, 0, 50),
      width: double.infinity,
      height: 200,
      color: Colors.amber[(index + 1) * 100],
      child: Center(
        child: Text(
          value.toString(),
          style: const TextStyle(fontSize: 30),
        ),
      ),
    );
  }

  // 플로팅 액션 버튼 구성
  Widget fab() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.end,
      children: <Widget>[
        floatingActionButton("맨위로"),
        floatingActionButton("위로"),
        floatingActionButton("중간으로"),
        floatingActionButton("아래로"),
        floatingActionButton("맨아래로"),
        floatingActionButton("특정위치"),
      ],
    );
  }

  // 플로팅 액션 버튼 위젯
  Widget floatingActionButton(String text) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: FloatingActionButton(
          onPressed: () {
            activeScroll(text);
          },
          child: Text(text)),
    );
  }

  // 플로팅 액션 버튼 기능 정의
  void activeScroll(text) {
    if (text == "맨위로") {
      moveScroll(0);
    } else if (text == "위로") {
      moveScroll(scrollController.offset - 300);
    } else if (text == "중간으로") {
      moveScroll(scrollController.position.maxScrollExtent / 2);
    } else if (text == "아래로") {
      moveScroll(scrollController.offset + 300);
    } else if (text == "맨아래로") {
      moveScroll(scrollController.position.maxScrollExtent);
    } else if (text == "특정위치") {
      moveScroll(150);
    }
  }

  // 스크롤 이동 기능 정의
  void moveScroll(value) {
    scrollController.animateTo(value.toDouble(),
        duration: const Duration(milliseconds: 500), curve: Curves.ease);
  }
}

참고

반응형