ScrollViewInterceptor
데스크톱 트랙패드·휠 스크롤을 가로채 스크롤 뷰를 구동하는 래퍼 (인프라)
목차
마우스 가운데 버튼을 누른 채 포인터를 이동하면 스크롤을 구동하는 인프라 래퍼입니다. 누른 위치와 현재 위치의 차이로 자동 스크롤 이동량을 계산합니다. 일반 휠·트랙패드 스크롤을 대체하는 위젯은 아니며
enabled: false면 이 부가 제스처를 설치하지 않습니다.
Live Preview#
코드 보기
class ScrollViewInterceptorDefaultExample extends StatelessComponent {
const ScrollViewInterceptorDefaultExample({super.key});
@override
Component build(BuildContext context) {
return div(
styles: Styles(raw: {'width': '260px'}),
[
ScrollViewInterceptor(
child: ScrollArea(
scrollAreaStyle: const CoreScrollAreaStyle(maxHeight: 160),
child: div(
classes: 'flex flex-col gap-${CoreSpace.scale.space8}',
[
for (var i = 1; i <= 20; i += 1) Text('항목 $i').bodyMedium,
],
),
),
),
],
);
}
}
class ScrollViewInterceptorDefaultExample extends StatelessWidget {
const ScrollViewInterceptorDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 260,
child: ScrollViewInterceptor(
child: ScrollArea(
scrollAreaStyle: const CoreScrollAreaStyle(maxHeight: 160),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: Theme.of(context).geometry.space(CoreSpace.space8),
children: [
for (var i = 1; i <= 20; i += 1) Text('항목 $i').bodyMedium,
],
),
),
),
);
}
}
사용법#
ScrollViewInterceptor(
child: ScrollArea(
scrollAreaStyle: const CoreScrollAreaStyle(maxHeight: 160),
child: Column(children: [/* 긴 콘텐츠 */]),
),
)
Props / Parameters#
W는 Flutter의 Widget, Web의 Component를 뜻하는 합성 슬롯 타입입니다.
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child | W | 필수 | 자동 스크롤을 적용할 콘텐츠 |
enabled |
bool |
true |
false 면 가로채지 않고 네이티브 스크롤로 패스 |
scrollViewInterceptorStyle |
CoreScrollViewInterceptorStyle? |
null |
dragSpeed·maxScrollSpeed 등 동작 단일 진입점 |
스타일 시스템 (Style System)#
CoreScrollViewInterceptorStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
dragSpeed |
double? |
Multiplier applied to the pointer offset before the easing curve. |
maxScrollSpeed |
double? |
Maximum absolute scroll velocity per frame (logical px). |
동작 스펙 (Behavior)#
가운데 버튼을 누르면 시작 위치를 저장하고 포인터 이동 중 그 위치와의 거리를 읽습니다. 거리에 dragSpeed 배율과 세제곱 곡선을 적용하고 프레임 시간으로 나눈 뒤 축별 이동량을
maxScrollSpeed로 제한합니다. 최대 이동량은 한 tick의 논리 px, 시간 계산은 밀리초 단위입니다. 버튼을 놓으면 타이머를 멈춥니다. enabled: false면 자식의 기본 스크롤 경로를 그대로 사용하며 자체 키보드 동작·값 변경 콜백은 없습니다.
사용 가이드라인 (Usage Guidelines)#
스크롤 뷰포트 자체가 필요하면 ScrollArea, 스크롤 위치에 따른 콘텐츠 빌드나 2축 이동이 필요하면 ScrollableClient를 사용합니다. ScrollViewInterceptor는 이미 존재하는 스크롤 경로에 가운데 버튼 제스처를 덧붙이는 역할입니다.
✅ Do#
가속하고 싶은 스크롤 영역 하나만 좁게 감싸기
ScrollViewInterceptor(
child: ScrollArea(
scrollAreaStyle: const CoreScrollAreaStyle(maxHeight: 240),
child: content,
),
)
Web은 가장 가까운 스크롤 가능한 조상을 명령형으로 움직이고 Flutter는 포인터 hit-test 경로에 스크롤 이벤트를 전달합니다. 실제로 가속하려는 스크롤 영역 가까이에 배치하고 중첩 스크롤에서 어떤 영역이 반응하는지 확인하세요.
❌ Don't#
유일한 스크롤 수단으로 의존하지 않기
// ❌ 필수 동선인데 ScrollViewInterceptor 에만 의존
ScrollViewInterceptor(
child: criticalScrollableContent,
)
마우스 가운데 버튼(Flutter PointerEvent.buttons != 4, Web MouseEvent.button != 1)에서만 동작합니다 — 터치·키보드·트랙패드 전용 사용자에게는 이 경로가 존재하지 않으므로, 감싼 스크롤 뷰 자체의 기본 스크롤이 항상 동작해야 하고 이 컴포넌트는 부가 가속 수단으로만 취급해야 합니다.
접근성 (Accessibility)#
역할#
양 플랫폼 모두 역할을 내보내지 않습니다. Flutter 는 Stacks 안에 Listener(그리고
자동 스크롤 중에만 커서 오버레이)를 두고 Semantics 를 만들지 않으며, Web 은 호출자가 넘긴
attributes 만 통과시키는 <div> 를 내보냅니다 — enabled: false
경로도 마찬가지로 빈
래퍼입니다. 시맨틱상 완전히 투명한 포인터 제스처 래퍼라, 감싼 콘텐츠의 시맨틱이 곧
전체 안내입니다.
키보드#
처리하는 키가 없습니다. Flutter 는 onPointerDown / onPointerMove / onPointerUp
만, Web 은 pointerdown / pointermove / pointerup
/ pointercancel 만 등록합니다
(Web 의 onKeyDown / onKeyUp 은 호출자 통과 슬롯). 자동 스크롤 제스처에 대응하는
키보드 경로는 양 플랫폼 모두 존재하지 않습니다.
포커스 관리#
포커스를 받지 않습니다. FocusNode / Focus / tabindex 가 없고 포커스 상태를 그리지도
않습니다. 이 컴포넌트가 렌더하는 유일한 상태는 자동 스크롤 중의 커서 변경입니다
(Flutter SystemMouseCursors.allScroll, Web cursor-all-scroll
클래스).
스크린 리더#
아무것도 안내하지 않습니다. 자동 스크롤을 쓸 수 있다는 사실, 세션이 시작됐다는 사실, 지금 스크롤 중이라는 사실 중 무엇도 알려지지 않습니다.
알려진 제약#
-
마우스 가운데 버튼 전용입니다 — Flutter 는
PointerEvent.buttons != 4, Web 은MouseEvent.button != 1에서 곧바로 빠져나옵니다. 터치 · 키보드 · 트랙패드 전용 입력에는 대응 경로가 없으므로, 마우스 가운데 버튼을 쓸 수 없는 사용자에게는 이 기능이 존재하지 않는 것과 같습니다. 감싼 스크롤 뷰 자체의 스크롤은 그대로 동작하니, 이 래퍼는 부가 가속 수단으로만 취급하고 필수 동선에 두지 마세요. -
Web 은
event.preventDefault()로 브라우저 자체의 미들클릭 자동 스크롤을 대체합니다. 그 기본 동작에 의존하던 사용자는 이 구현의 감도/속도 토큰을 대신 받게 됩니다. - 스크롤은 가장 가까운 스크롤 가능한 조상을 명령형으로 이동시키는 방식이라 포커스가 따라가지 않고 어떤 안내도 발생하지 않습니다 — 화면은 움직였는데 리더의 위치는 그대로입니다.
enabled: false면 양 플랫폼 모두 아무것도 하지 않는 통과 래퍼가 됩니다.
reduced motion 등 컴포넌트를 가로지르는 축은 전역 접근성 축에서 다룹니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 가운데 버튼 판별 | PointerEvent.buttons == 4 |
MouseEvent.button == 1 |
| 자동 이동 | Ticker의 경과 시간과 hit-test 대상 스크롤 이벤트 | 주기 Timer와 조상 DOM 스크롤 |
| 종료 이벤트 | pointer up | pointer up/cancel |
| 비활성 래퍼 | child를 직접 반환 |
passthrough 속성을 유지하는 <div> |
| 브라우저 기본 자동 스크롤 | 해당 없음 | 가운데 버튼 기본 동작을 preventDefault로 대체 |