다트에서 커스텀 스크롤(Custom Scroll) 구현 방법은?
_____1. Q: “커스텀 스크롤”이란 무엇인가요?
A: Flutter에서 기본 제공되는 ListView·GridView 대신 Sliver(슬리버)를 기반으로 스크롤 영역을 직접 구성하고 물리(ScrollPhysics), 스크롤바 모양, 오버스크롤 효과 등을 세밀히 제어하는 기법입니다.
2. Q: CustomScrollView와 Sliver의 관계는?
A: CustomScrollView는 여러 Sliver 위젯을 한 데 묶어 스크롤 가능한 영역을 만듭니다. SliverAppBar, SliverList, SliverGrid, SliverToBoxAdapter 등을 조합해 복합 레이아웃을 구현할 수 있습니다.
3. Q: 기본 ListView 대신 CustomScrollView를 사용하려면?
A:
```dart
CustomScrollView(
slivers: [
SliverAppBar(
title: Text('커스텀 스크롤 예제'),
floating: true,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('아이템 $index')),
childCount: 50,
),
),
],
)
```
- SliverAppBar: 스크롤에 따라 축소/고정되는 앱 바
- SliverList: 일반 리스트
- SliverGrid: 그리드 형태
4. Q: ScrollController로 스크롤 위치를 제어하려면?
A:
```dart
final controller = ScrollController();
// 위젯
CustomScrollView(
controller: controller,
slivers: [/* ... */],
);
// 특정 위치로 이동
controller.animateTo(
200.0,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
```
- scrollController.position.pixels 읽기
- listener 등록: controller.addListener(...)
5. Q: ScrollPhysics를 커스터마이징하려면?
A:
```dart
class BouncingScrollPhysicsAndroid extends BouncingScrollPhysics {
@override
BouncingScrollPhysics applyTo(ScrollPhysics ancestor) {
return BouncingScrollPhysicsAndroid(parent: buildParent(ancestor));
}
@override
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
// offset 조정 로직
return super.applyPhysicsToUserOffset(position, offset * 0.5);
}
}
// 적용
CustomScrollView(
physics: BouncingScrollPhysicsAndroid(),
slivers: [/* ... */],
);
```
6. Q: ScrollBehavior를 바꿔 오버스크롤·글리프 효과를 제어하려면?
A:
```dart
class NoGlowScrollBehavior extends ScrollBehavior {
@override
Widget buildViewportChrome(
BuildContext context, Widget child, AxisDirection axisDirection) {
return child; // 글리프(흔들림) 제거
}
}
// 최상위 적용
MaterialApp(
scrollBehavior: NoGlowScrollBehavior(),
home: /* ... */,
);
```
7. Q: 커스텀 스크롤바(Custom Scrollbar) 추가 방법은?
A:
```dart
Scrollbar(
controller: controller,
thumbVisibility: true, // 항상 보이기(2.0.0+)
thickness: 6,
radius: Radius.circular(3),
child: CustomScrollView(
controller: controller,
slivers: [/* ... */],
),
);
```
- thumbVisibility: 스크롤바 상시 표시
- thickness·radius로 모양 변경
8. Q: SliverToBoxAdapter 사용 예제는?
A: 고정 높이 위젯을 슬리버에 삽입할 때 사용합니다.
```dart
SliverToBoxAdapter(
child: Container(
height: 150,
color: Colors.blueAccent,
child: Center(child: Text('커스텀 배너')),
),
),
```
A: FutureBuilder·StreamBuilder 등으로 데이터를 불러온 뒤 SliverChildListDelegate를 사용해 리스트 갱신:
```dart
SliverList(
delegate: SliverChildListDelegate(
items.map((item) => ListTile(title: Text(item))).toList(),
),
)
```
10. Q: 성능 최적화 팁은?
A:
- SliverChildBuilderDelegate 사용: 화면에 보이는 아이템만 렌더링
- const 생성자 활용: 위젯 재생성 최소화
- keepAlive(using AutomaticKeepAliveClientMixin)로 복잡한 자식 위젯 재생성 방지
- 이미지 캐싱: CachedNetworkImage 등 활용
11. Q: 자주 발생하는 오류 및 해결책은?
A:
- “ScrollController not attached” 오류: ListView/CustomScrollView에 controller 등록 여부 확인
- Nested Scroll 충돌: primary: false, shrinkWrap: true 옵션 검토
- Sliver 위젯을 직접 자식으로 쓰지 않을 때: SliverToBoxAdapter로 감싸기
12. Q: 예제 전체 코드 (기본 뼈대)
A:
```dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
scrollBehavior: NoGlowScrollBehavior(),
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State
final ScrollController _controller = ScrollController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Scrollbar(
controller: _controller,
thumbVisibility: true,
child: CustomScrollView(
controller: _controller,
physics: BouncingScrollPhysicsAndroid(),
slivers: [
SliverAppBar(
title: Text('커스텀 스크롤 예제'),
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
background: Image.network(
'https://picsum.photos/400/200',
fit: BoxFit.cover,
),
),
pinned: true,
),
SliverToBoxAdapter(
child: Container(
height: 100,
color: Colors.amber,
child: Center(child: Text('배너 섹션')),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('아이템 $index')),
childCount: 30,
),
),
],
),
),
);
}
}
// 글로우 제거
class NoGlowScrollBehavior extends ScrollBehavior {
@override
Widget buildViewportChrome(
BuildContext context, Widget child, AxisDirection axisDirection) {
return child;
}
}
// 물리 효과 커스텀
class BouncingScrollPhysicsAndroid extends BouncingScrollPhysics {
BouncingScrollPhysicsAndroid({ScrollPhysics? parent})
: super(parent: parent);
@override
BouncingScrollPhysicsAndroid applyTo(ScrollPhysics? ancestor) {
return BouncingScrollPhysicsAndroid(parent: buildParent(ancestor));
}
@override
double applyPhysicsToUserOffset(
ScrollMetrics position, double offset) {
// 범위 내로만 반동 폭 제한
return super.applyPhysicsToUserOffset(position, offset * 0.7);
}
}
```
— 끝 —
작성자:
김하은 [비회원]
| 작성일자: 1년 전
2024-09-19 01:52:47
조회수: 116 | 댓글: 0 | 좋아요: 0 | 싫어요: 0
조회수: 116 | 댓글: 0 | 좋아요: 0 | 싫어요: 0
내용이 부정확하다면 싫어요를 클릭해주세요.