Mobile/Flutter

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

luvris2 2024. 3. 19. 17:57
반응형

개요

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

참고

반응형