반응형
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!),
),
],
);
},
),
),
],
),
);
}
}
참고
반응형