Flutter - Layout 설계 - ListView/GridView/TabBarView/SafeArea/TabPageSelector

반응형

 

Overview

레이아웃 설계 시, 어떻게 화면을 구성해야할지에 대한 포스팅을 다룬 내용입니다.

이 포스팅에서는 크게 GridView와 ListView로 항목을 나열하고,

TabBarView를 통해 탭을 슬라이드하여 페이지를 이동할 수 있도록 합니다.


DefaultTabController

  • 탭 컨트롤러의 상태를 관리하는 위젯
  • 일반적으로 TabBar 와 TabBarView 를 함께 사용
  • 여러 화면 또는 내용 섹션이 있는 탭 인터페이스를 만듬

속성

  • length
    • 필수 속성
    • 탭 표시줄의 탭 수 지정
    • 반드시 음수가 아닌 정수의 값으로 설정
  • initialIndex
    • 선택한 탭의 초기 인덱스 지정
    • 기본값은 0, 첫 번째 탭을 표시
  • child
    • 탭 컨트롤러에 액세스할 수 있는 하위 위젯 트리 표시
    • 일반적으로 TabBar 또는 TabBarView 를 포함

TabBarView

  • TabBar의 다른 탭에 해당하는 일련의 화면 또는 내용 섹션을 표시하는 데 사용
  • 일반적으로 TabBar와 함께 사용
  • TabController에 의해 제어

속성

  • child
    • 각 탭의 내용으로 표시되는 위젯 목록 표시

TabPageSelector

  • 탭 인디케이터를 제공하는 위젯
  • 탭 바 또는 페이지 컨트롤러와 함께 사용
  • 사용자가 현재 선택한 탭을 시각적으로 표시하는 데 사용
  • 각 탭에 대한 인디케이터를 제공하여 사용자가 현재 선택된 탭을 알 수 있도록 도와주는 기능 제공

속성

  • controller
    • 페이지 컨트롤러를 지정
    • TabPageSelector와 페이지 컨트롤러를 연결
  •  color
    • 선택되지 않은 탭의 인디케이터 색상 지정
  • selectedColor
    • 선택된 탭의 인디케이터 색상 지정
  • padding
    • 인디케이터의 여백 지정
    • 주로 내부적인 여백 값을 설정하여 인디케이터와 주변 컨텐츠 간의 간격 조정
  • indicatorSize
    • 인디케이터의 크기 지정

SafeArea

  • 화면의 안전 영역을 고려하여 자식 위젯을 배치하는 데 사용
  • 안전 영역 : 디바이스 화면의 가장자리에 위치하는 시스템 및 하드웨어 요소로 인해 컨텐츠가 잘려 표시될 수 있는 영역을 의미
  • 자식 위젯을 화면의 경계 영역을 피해 컨텐츠를 안전하게 표시할 수 있는 기능 제공

속성

  • left, right, top, bottom
    • 각각 왼쪽, 오른쪽, 위쪽, 아래쪽 가장자리에서 안전 영역을 고려할지 여부 지정
  • minimum
    • 모든 가장자리에서 최소 안전영역을 고려할지 여부 지정
    • EdgeInsets.all(값)을 사용하여 모든 가장자리에 동일한 안전 영역 값 적용
  • maintainBottomViewPadding
    • 하단의 내비게이션 바에 대한 여백을 유지할지 여부 지정
    • 기본값 false
    • true로 지정할 경우, 하단 내비게이션 바에 대한 여백 유지

ListView

  • 선형으로 정렬된 스크롤이 가능한 위젯
  • 가장 일반적으로 사용되는 스크롤 위젯
  • 다양한 사용자 지정 옵션을 사용하여 많은 양의 데이터를 효율적으로 표시하는데 사용

속성

  • scrollDirection
    • 스크롤 방향 결정
    • 세로 : Axis.vertical(기본값)
    • 가로 : Axis.horizontal
  • shrinkWrap
    • 목록을 shrinkWarp 위젯으로 래핑할지 여부를 부울 값으로 설정
    • true : 목록은 콘텐츠를 표시하는데 필요한 공간만 차지
    • false : 사용 가능한 모든 공간 차지
  •  padding
    • 전체 목록 주변의 안쪽 여백 설정
  • itemCount
    • 목록의 항목 수
    • 목록이 정적이면 수동으로 설정
    • 동적인 경우 null로 설정하여 항목이 추가될 때 동적으로 추가
  • itemBuilder
    • 목록의 각 항목에 대해 호출되는 콜백 함수
    • 내용(BuildContext)r과 인덱스(index)라는 두 가지 인수를 사용
    • 함수는 항목을 나타내는 위젯 반환
  • separatorBuilder
    • ListView의 각 항목 사이에 구분 기호를 삽입하는데 사용
    • 주로 Divider 위젯으로 목록 각 항목 사이의 구분 기호로 사용
    • 위젯을 반환하는 함수를 사용하며 목록의 모든 항목에 대해 한 번씩 호출
  • controller
    • 목록의 스크롤 동작을 제어하는데 사용
    • 목록의 특정 위치로 스크롤할 수 있는 jumpTo, animateTo 같은 메서드 제공
    • 스크롤 이벤트를 수신할 수 있는 addListener 메서드 제공, 페이징 처리를 위해 주로 사용

ListTile


GridView

  • 스크롤 가능한 위젯 그리드를 2차원 레이아웃으로 표시
  • 그리드의 모양과 동작을 사용자가 지정하는 여러 속성 제공

속성

  • scrollDirection
    • 스크롤 방향 결정
    • 세로 : Axis.vertical(기본값)
    • 가로 : Axis.horizontal
  • crossAxisCount
    • 각 행(또는 scrollDirection에 따라 열)의 항목수 결정
    • 기본값 : 2
  • crossAxisSpacing
    • 행(또는 scrollDirection에 따라 열)에서 항목 사이의 간격 결정
    • 기본 값 : 0
  • mainAxisSpacing
    • 행(또는 scrollDirection에 따라 열) 사이의 간격 결정
    • 기본 값 : 0
  •  childAspectRatio
    • 그리드의 각 항목의 화면 비율 결정
    • 위젯의 너비와 높이 사이의 비율을 나타내는 이중 값 사용
  • padding
    • 전체 그리드 주변의 안쪽 여백 결정
  • controller
    • 목록의 스크롤 동작을 제어하는데 사용
    • 그리드의 특정 위치로 스크롤 하는 메서드 제공
    • 스크롤 이벤트를 수신할 수 있는 메서드 제공, 페이징 처리를 위해 주로 사용
  • gridDelegate
    • 그리드의 레이아웃 알고리즘 및 기타 시각적 속성 지정
    • 행당 항목 수, 각 항목의 종횡비 및 항목 사이의 안쪽 여백(패딩)과 같은 그리드의 모양을 사용자가 다양하게 지정할 수 있는 옵션 제공

코드

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Shazam',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MainViewPage(),
    );
  }
}

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

  @override
  State<MainViewPage> createState() => _MainViewState();
}

class _MainViewState extends State<MainViewPage> {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      // 초기 보여줄 화면 인덱스
      initialIndex: 1,
      // 총 갯수
      length: 2,
      child: Scaffold(
        body: Stack(
          children: [
            // 보여줄 화면의 페이지 인덱스 순서
            const TabBarView(
              children: [
                FirstTab(),
                SecondTab(),
              ],
            ),
            SafeArea(
              child: Padding(
                padding:
                    const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
                child: Column(
                  children: [
                    Container(
                      alignment: Alignment.topCenter,
                      child: const TabPage(),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  State<TabPage> createState() => _TabPageState();
}

class _TabPageState extends State<TabPage> {
  @override
  Widget build(BuildContext context) {
    return TabPageSelector(
      // 선택되지 않은 화면의 인디케이터 색상
      color: DefaultTabController.of(context).index == 1
          ? Colors.blue[300]
          : Colors.grey[400],
      // 선택된 화면의 인디케이터 색상
      selectedColor: DefaultTabController.of(context).index == 1
          ? Colors.white
          : Colors.blue,
      indicatorSize: 15,
    );
  }
}

// 첫번째 페이지
class FirstTab extends StatelessWidget {
  const FirstTab({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    const arrList = [
      {'number': '1'},
      {'number': '2'},
      {'number': '3'},
      {'number': '4'},
      {'number': '5'},
      {'number': '6'},
    ];

    return SafeArea(
      child: Expanded(
        child: GridView.builder(
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 2,
            childAspectRatio: 0.5,
          ),
          itemCount: arrList.length,
          itemBuilder: (context, index) {
            var arrNum = arrList[index];
            String num = arrNum['number']!;
            // 그리드 뷰
            return Container(
              alignment: Alignment.center,
              margin: const EdgeInsets.all(4.0),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: const BorderRadius.all(Radius.circular(8)),
                boxShadow: [
                  BoxShadow(
                    color: Colors.grey.withOpacity(0.5),
                    blurRadius: 1,
                    spreadRadius: 1,
                  ),
                ],
              ),
              child: Text(
                num,
                style: const TextStyle(fontSize: 25),
              ),
            );
          },
        ),
      ),
    );
  }
}

// 두번째 페이지
class SecondTab extends StatelessWidget {
  const SecondTab({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    const listData = {
      'a': [
        {'number': '1'},
        {'number': '2'},
        {'number': '3'},
        {'number': '4'},
        {'number': '5'},
        {'number': '6'},
      ],
      'b': [
        {'char': 'a'},
        {'char': 'b'},
        {'char': 'c'},
        {'char': 'd'},
        {'char': 'e'},
        {'char': 'f'},
      ],
    };

    return SafeArea(
      child: Column(
        children: [
          const Text(
            '첫번째 리스트',
            style: TextStyle(
              fontSize: 20,
            ),
          ),
          SizedBox(
            height: 100,
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: listData['a']!.length,
              itemBuilder: (context, index) {
                final items = listData['a']![index];
                final number = items['number'];
                return Row(
                  children: [
                    // 리스트뷰 1
                    Container(
                      alignment: Alignment.center,
                      width: 100,
                      height: 100,
                      margin: const EdgeInsets.all(4.0),
                      decoration: BoxDecoration(
                        color: Colors.white,
                        borderRadius:
                            const BorderRadius.all(Radius.circular(8)),
                        boxShadow: [
                          BoxShadow(
                            color: Colors.grey.withOpacity(0.5),
                            blurRadius: 1,
                            spreadRadius: 1,
                          ),
                        ],
                      ),
                      child: Text(number!),
                    ),
                  ],
                );
              },
            ),
          ),
          const Divider(),
          const Text(
            '두번째 리스트',
            style: TextStyle(
              fontSize: 20,
            ),
          ),
          SizedBox(
            height: 100,
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: listData['b']!.length,
              itemBuilder: (context, index) {
                final items = listData['b']![index];
                final char = items['char'];
                return Row(
                  children: [
                    // 리스트뷰 2
                    Container(
                      alignment: Alignment.center,
                      width: 100,
                      height: 100,
                      margin: const EdgeInsets.all(4.0),
                      decoration: BoxDecoration(
                        color: Colors.white,
                        borderRadius:
                            const BorderRadius.all(Radius.circular(8)),
                        boxShadow: [
                          BoxShadow(
                            color: Colors.grey.withOpacity(0.5),
                            blurRadius: 1,
                            spreadRadius: 1,
                          ),
                        ],
                      ),
                      child: Text(char!),
                    ),
                  ],
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

참고

 

반응형