Foggy day

[Flutter] SingleChildScrollView - 사용법 본문

Flutter/Flutter widget

[Flutter] SingleChildScrollView - 사용법

jinhan38 2023. 3. 22. 21:52

 

 

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

 

 

1. child 

2. padding

3. ScrollController

4. scrollDirection

5. physics

6. keyboardDismissBehavior

7. primary

8. reverse

 

 

 

1. child

SingleChildScrollView는 child위젯을 받는 스크롤이 가능한 위젯입니다. ListView가 children을 받는 것과는 다르게 child를 받기 때문에 보통 child로 Column, Row, ListBody를 많이 사용합니다. 예제에서는 ListBody를 사용했습니다. Column과 ListBody의 큰 차이점이라면 Column은 가로넓이를 자식에게 맞추지만 ListBody는 최대로 늘어납니다. 그리고 자식 위젯의 width까지 꽉 채웁니다(fill).

 

예제에서는 ListBody에 10개의 Container를 추가했습니다. 각 컨테이너의 컬러를 랜덤으로 생성해 줬고, 높이 300, 패딩 20을 설정했습니다.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SingleChildScrollScreen"),
      ),
      body: SingleChildScrollView(
        child: ListBody(
          children: _listItem(),
        ),
      ),
    );
  }

  List<Widget> _listItem() {
    List<Widget> widgets = [];
    for (int i = 0; i < 10; i++) {
      widgets.add(
        Container(
          margin: const EdgeInsets.all(20),
          height: 300,
          alignment: Alignment.center,
          color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
          child: Text(
            "$i",
            style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
          ),
        ),
      );
    }
    return widgets;
  }

 

 

 

 

2. padding

SingleChildScrollView의 padding은 child의 영역을 컨트롤합니다. EdgeInsets.all(30)을 줌으로써 ListBody의 상하좌우에 30씩 간격을 설정했습니다. 위의 이미지와 비교해 보면 Container들 사이의 간격은 그대로인 것을 알 수 있습니다. 

 

 

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SingleChildScrollScreen"),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(30),
        child: ListBody(
          children: _listItem(),
        ),
      ),
    );
  }

 

 

 

3. ScrollController

ScrollController는 SingleChildScrollView의 정보들을 가지고 있습니다. 스크롤이 발생할 때 스크롤 정보를 실시간으로 측정할 수 있고, 특정 위치로 이동을 시킬 수도 있습니다. ScrollController는 사용한 후에 반드시 dispose를 해줘야 합니다.

 

  late ScrollController _controller;

  @override
  void initState() {
    _controller = ScrollController();
    _controller.addListener(() {
      /// 스크롤을 할 때 마다 호출

      /// 스크롤 된 값
      print('offset : ${_controller.offset}');

      /// 스크롤에 대한 여러 정보를 가지고 있습니다.
      /// 전체 길이, offset, 방향 등
      print('position : ${_controller.position}');

      /// 컨트롤러가 SingleChildScrollView에 연결이 됐는지 안돼는지
      _controller.hasClients;
    });

    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SingleChildScrollScreen"),
      ),
      body: SingleChildScrollView(
        controller: _controller,
        padding: const EdgeInsets.all(30),
        child: ListBody(
          children: [
          
            /// 특정 위치로 바로 이동
            ElevatedButton(
              onPressed: () {
                _controller.jumpTo(1000);
              },
              child: const Text("jumpTo"),
            ),
            
            /// 특정 위치로 애니메이션으로 이동
            ElevatedButton(
              onPressed: () {
                _controller.animateTo(2500,
                    duration: const Duration(milliseconds: 1000),
                    curve: Curves.fastOutSlowIn);
              },
              child: const Text("animateTo"),
            ),
            ..._listItem(),
          ],
        ),
      ),
    );
  }

 

위의 예제와 같이 controller를 연결해 준 후 버튼을 클릭해서 특정 위치로 이동해 보겠습니다. 스크롤 이동은 두 가지 방식이 있습니다. 바로 이동하는 방식과 애니메이션 효과를 주면서 이동하는 방식이 있습니다. Curves는 여러 가지가 있으므로 공식문서를 참고하여 필요한 타입으로 사용하시면 됩니다.

https://api.flutter.dev/flutter/animation/Curves-class.html

 

 

 

 

4. scrollDirection

scrollDirection은 스크롤의 방향을 결정합니다. 기본값은 세로이며 필요에 따라서 가로로 설정할 수 있습니다. 가로로 설정한다면 자식 위젯을 Row로 사용하곤 합니다.

 

  • Axis.vertical(기본값) : 세로
  • Axis.horizontal : 가로
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SingleChildScrollScreen"),
      ),
      body: SingleChildScrollView(
        controller: _controller,
        scrollDirection: Axis.horizontal,
        padding: const EdgeInsets.all(30),
        child: _row(),
      ),
    );
  }

  Widget _row() {
    return Row(
      children: _listItem(),
    );
  }

 

 

 

5. physics

physics는 스크롤뷰가 사용자의 입력에 어떻게 반응할지를 결정합니다. 좀 더 구체적으로는 스크롤의 top과 bottom에 도달했을 때 어떻게 애니메이션을 보여줄 것인지, 혹은 스크롤을 비활성화시킬 것인지 등을 설정할 수 있습니다. 

 

  • AlwaysScrollableScrollPhysics : 항상 스크롤이 가능하도록 설정 
  • BouncingScrollPhysics :  IOS처럼 끝부분에 도달했을 때 바운싱 애니메이션 형태
  • NeverScrollableScrollPhysics : 스크롤 불가능하도록 설정
  • PageScrollPhysics : 페이지를 이동하는 것 같은 애니메이션
  • ClampingScrollPhysics : Android에서 사용하는 애니메이션
  • RangeMaintainingScrollPhysics : UI적으로는 ClampingScrollPhysics과 같은 형태입니다. 차이점은 overscroll이 발생하지 않습니다. ClampingScrollPhysics은 끝부분에 도달했을 때 애니메이션과 함께 약간의 overscroll이 발생합니다. 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SingleChildScrollScreen"),
      ),
      body: SingleChildScrollView(
        controller: _controller,
        physics: BouncingScrollPhysics(),
        padding: const EdgeInsets.all(30),
        child: _listBody(),
      ),
    );
  }

 

 

 

6. keyboardDismissBehavior

keyboardDismissBehavior는 스크롤을 드래그할 때 키보드 내릴지 말지 결정합니다.

ScrollViewKeyboardDismissBehavior.manual과  ScrollViewKeyboardDismissBehavior.onDrag 두 개의 타입이 있습니다. manual은 드래그를 해도 이미 올라와 있는 키보드는 내려가지 않습니다. 수동으로 알아서 내려줘야 합니다. onDrag는 스크롤이 되면 키보드를 내립니다. 

 

 keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,

 

 

 

7. primary

IOS에서만 동작하는 기능입니다. IOS에서 상태바를 눌렀을 때 스크롤이 최상단으로 이동합니다. 만약 primary특성을 사용한다면  ㄴScrollController는 사용할 수 없습니다.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SingleChildScrollScreen"),
      ),
      body: GestureDetector(
        child: SingleChildScrollView(
          primary: true,
          child: _listBody(),
        ),
      ),
    );
  }

 

 

 

8. reverse

스크롤을 거꾸로 시작할 수 있게 하는 기능입니다. true로 입력하면 스크롤이 바닥부터 시작됩니다. 기본 값은 false이며 상단부터 시작됩니다. 

 

 

 

 

 

Full Code

import 'dart:math';

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

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

  @override
  State<SingleChildScrollScreen> createState() =>
      _SingleChildScrollScreenState();
}

class _SingleChildScrollScreenState extends State<SingleChildScrollScreen> {
  late ScrollController _controller;
  final RestorableDouble _scrollOffset = RestorableDouble(0);

  int listCount = 10;

  @override
  void initState() {
    _controller = ScrollController();
    _controller.addListener(() {
      /// 스크롤을 할 때 마다 호출

      /// 스크롤 된 값
      // print('offset : ${_controller.offset}');
      _scrollOffset.value = _controller.offset;

      /// 스크롤에 대한 여러 정보를 가지고 있습니다.
      /// 전체 길이, offset, 방향 등
      // print('position : ${_controller.position}');

      /// 컨트롤러가 SingleChildScrollView에 연결이 됐는지 안돼는지
      _controller.hasClients;
    });

    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SingleChildScrollScreen"),
      ),
      body: GestureDetector(
        child: SingleChildScrollView(
          controller: _controller,
          physics: const BouncingScrollPhysics(),
          padding: const EdgeInsets.all(30),
          keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
          // primary: true,
          reverse: false,
          child: _listBody(),
        ),
      ),
    );
  }

  Widget _row() {
    return Row(
      children: _listItem(),
    );
  }

  Widget _listBody() {
    return ListBody(
      children: [
        TextFormField(),

        /// 특정 위치로 바로 이동
        ElevatedButton(
          onPressed: () {
            _controller.jumpTo(1000);
          },
          child: const Text("jumpTo"),
        ),

        /// 특정 위치로 애니메이션으로 이동
        ElevatedButton(
          onPressed: () {
            _controller.animateTo(2500,
                duration: const Duration(milliseconds: 1000),
                curve: Curves.fastOutSlowIn);
          },
          child: const Text("animateTo"),
        ),

        ElevatedButton(
            onPressed: () {
              setState(() {
                listCount--;
              });
            },
            child: Text("제거")),

        ..._listItem(),
      ],
    );
  }

  List<Widget> _listItem() {
    List<Widget> widgets = [];
    for (int i = 0; i < listCount; i++) {
      widgets.add(
        Container(
          margin: const EdgeInsets.all(20),
          height: 300,
          alignment: Alignment.center,
          color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
          child: Text(
            "$i",
            style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
          ),
        ),
      );
    }
    return widgets;
  }
}

 

 

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

[Flutter] GridView - 사용법  (0) 2023.03.24
[Flutter] ListView - 사용법  (2) 2023.03.23
[Flutter] Image Widget - 사용법  (0) 2023.03.22
[Flutter] Text Widget - 사용법  (0) 2023.03.21
[Flutter] PopupMenuButton - 사용법  (0) 2023.03.21