Switcher | CoUI
LogoCoUI

Switcher

슬라이드 전환과 드래그 제스처로 여러 뷰 사이를 오가는 스위처 컴포넌트

Switcher#

여러 자식 뷰 중 하나를 한 번에 보여주고, 활성 인덱스가 바뀌면 슬라이드 / 페이드 전환을 애니메이션하는 컴포넌트입니다. 사용자는 드래그하여 인접한 뷰 사이를 스크럽할 수 있습니다.

Switcher는 레거시 Flutter Switcher와 Web Switcher를 통일한 컴포넌트입니다. Flutter와 Web이 동일한 파라미터 이름·동작·시각을 갖습니다 — 네 방향 슬라이드, 드래그 제스처, 동일한 전환 시간.

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 온보딩 단계처럼 순차적 뷰 사이를 슬라이드 전환할 때
  • 목록/그리드 뷰 모드 전환에 부드러운 애니메이션이 필요할 때
  • 드래그로 인접 뷰를 스크럽하는 인터랙션이 필요할 때

대신 다른 컴포넌트를 사용하세요:

  • Tabs: 라벨 기반의 콘텐츠 섹션 전환에
  • Carousel: 자동 재생 / 다수 아이템 슬라이드쇼에
  • Swiper: 카드 스택 형태의 스와이프 인터랙션에

기본 사용법 (Basic Usage)#

Switcher(
  index: _index,
  direction: CoreSwitcherDirection.left,
  onIndexChanged: (next) => setState(() => _index = next),
  items: const [
    Text('Page 1'),
    Text('Page 2'),
    Text('Page 3'),
  ],
)
Switcher(
  index: _index,
  direction: CoreSwitcherDirection.left,
  onIndexChanged: (next) => setState(() => _index = next),
  items: [
    div([text('Page 1')]),
    div([text('Page 2')]),
    div([text('Page 3')]),
  ],
)

Props / Parameters#

Switcher#

속성타입기본값설명
itemsList<W>필수전환 대상 자식 뷰 목록
indexint0현재 보이는 자식 인덱스
direction CoreSwitcherDirection left 새 뷰가 슬라이드해 들어오는 방향
gestureEnabled bool true 드래그 / 스와이프 내비게이션 활성화 여부
onIndexChanged void Function(int)? null 드래그가 다른 뷰에 안착했을 때 새 인덱스로 호출
switcherStyle CoreSwitcherStyle? null 인스턴스별 전환 스타일

CoreSwitcherDirectionup / down / left / right 네 방향을 지원합니다.

스타일 시스템 (Style System)#

Switcher의 전환 chrome은 단일 CoreSwitcherStyle 슬롯으로 흐릅니다 .

시맨틱 vs 스타일#

  • 시맨틱 / behaviour: 위젯 파라미터로 직접 (index, direction, gestureEnabled, items)
  • chrome / dimensional: switcherStyle: CoreSwitcherStyle? 한 곳으로

Resolve chain#

CoreSwitcherStyle.defaultDuration / defaultCurve  // 디자인 시스템 기본값
  → CoreSwitcherTheme.style                       // 프로젝트 공통
  → widget.switcherStyle                          // 인스턴스별

CoreSwitcherStyle 필드#

필드타입설명
duration Duration? Slide / fade transition duration.
curve CoreCubicBezier? Slide / fade transition easing, as cubic-bezier control points. null → [defaultCurve].
Switcher(
  switcherStyle: const CoreSwitcherStyle(
    curve: CoreEasing.easeOutBack,
    duration: Duration(milliseconds: 200),
  ),
  items: const [Text('A'), Text('B')],
)

curve 는 플랫폼 중립인 cubic-bezier 제어점 4개(CoreEasing.*)입니다 — Flutter 는 이것을 Curve 로, Web 은 같은 제어점을 트랙 slide 와 슬롯 fade 양쪽의 cubic-bezier(...) timing function 으로 변환합니다.

Flutter Switcher 에는 Curve 를 직접 받는 curve 파라미터가 남아 있지만 @Deprecated 이며 switcherStyle 슬롯이 그 자리를 대신합니다 (Flutter Curve 는 Web 이 미러할 수 없는 타입이라 전환 chrome 의 단일 진입점이 될 수 없습니다). 남아 있는 동안에는 슬롯보다 우선합니다.

빠른 오버라이드 (Chain)#

이미 만든 Switcher 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

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

  @override
  State<SwitcherChainExample> createState() => _SwitcherChainExampleState();
}

class _SwitcherChainExampleState extends State<SwitcherChainExample> {
  int _index = 0;

  static const List<String> _labels = ['View A', 'View B', 'View C'];

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        SizedBox(
          width: CoreSpace.space256,
          height: CoreSize.size96,
          child:
              Switcher(
                index: _index,
                onIndexChanged: (next) => setState(() => _index = next),
                items: [
                  for (final label in _labels) Center(child: Text(label).titleMedium),
                ],
              ).withStyle(
                const CoreSwitcherStyle(
                  duration: Duration(milliseconds: CoreDuration.slow),
                  curve: CoreEasing.enter,
                ),
              ),
        ),
        const Gap.space16(),
        Wrap(
          alignment: WrapAlignment.center,
          children: [
            for (var idx = 0; idx < _labels.length; idx++)
              Padding(
                padding: const EdgeInsets.symmetric(
                  horizontal: CoreSpace.space4,
                ),
                child: Button(
                  variant: idx == _index ? CoreButtonVariant.primary : CoreButtonVariant.outline,
                  onPressed: () => setState(() => _index = idx),
                  child: Text(_labels[idx]),
                ),
              ),
          ],
        ),
      ],
    );
  }
}
class SwitcherChainExample extends StatefulComponent {
  const SwitcherChainExample({super.key});

  @override
  State<SwitcherChainExample> createState() => _SwitcherChainExampleState();
}

class _SwitcherChainExampleState extends State<SwitcherChainExample> {
  int _index = 0;

  static const List<String> _labels = ['View A', 'View B', 'View C'];

  @override
  Component build(BuildContext context) {
    return div(
      [
        div(
          [
            Switcher(
              index: _index,
              onIndexChanged: (next) => setState(() => _index = next),
              items: [
                for (final label in _labels)
                  div(
                    [Text(label).titleMedium],
                    classes: 'flex items-center justify-center h-full',
                  ),
              ],
            ).withStyle(
              const CoreSwitcherStyle(
                duration: Duration(milliseconds: CoreDuration.slow),
                curve: CoreEasing.enter,
              ),
            ),
          ],
          styles: const Styles(
            raw: {'width': '16rem', 'height': '6rem'},
          ),
        ),
        Gap.space16(),
        div(
          [
            for (var idx = 0; idx < _labels.length; idx++)
              Button(
                variant: idx == _index ? CoreButtonVariant.primary : CoreButtonVariant.outline,
                onPressed: () => setState(() => _index = idx),
                child: Text(_labels[idx]),
              ),
          ],
          classes: 'flex flex-row flex-wrap justify-center gap-${CoreSpace.scale.space8}',
        ),
      ],
      classes: 'flex flex-col items-center',
    );
  }
}

변형 (Variants)#

슬라이드 방향#

direction으로 새 뷰가 들어오는 방향을 지정합니다.

Switcher(direction: CoreSwitcherDirection.up, items: [...])
Switcher(direction: CoreSwitcherDirection.right, items: [...])

드래그 비활성화#

gestureEnabled: false로 드래그를 막고 프로그래매틱 전환만 허용합니다.

Switcher(gestureEnabled: false, items: [...])

동작 스펙 (Behavior)#

인터랙션#

  • 프로그래매틱 전환: index 변경 시 새 뷰가 direction 방향으로 슬라이드
  • 드래그: gestureEnabledtrue면 마우스 / 터치 드래그로 인접 뷰 스크럽
  • 스냅: 드래그 해제 시 가장 가까운 뷰로 스냅하고 onIndexChanged 호출

애니메이션#

  • 슬라이드 + 페이드 전환: 기본 300ms (CoreDuration.moderate), easing 은 CoreEasing.move
  • 드래그 중에는 전환이 비활성화되어 포인터를 1:1로 따라감

사용 가이드라인 (Usage Guidelines)#

✅ Do#

onIndexChanged로 드래그 결과를 상위 상태에 반영

Switcher(
  index: _index,
  onIndexChanged: (next) => setState(() => _index = next),
  items: const [Text('A'), Text('B')],
)

드래그가 안착한 인덱스를 상태에 반영해야 다음 빌드에서 일관된 뷰가 보인다.


❌ Don't#

라벨 기반 콘텐츠 섹션 전환에 Switcher 사용

// ❌ 라벨 탭 UI 가 필요한 경우
Switcher(
  items: const [Text('Account'), Text('Password')],
)

라벨 탭이 필요하면 Tabs가 적합하다. Switcher 는 슬라이드 전환이 핵심이다.

접근성 (Accessibility)#

  • 비활성 뷰는 aria-hidden="true" (Web) 로 표시
  • 루트 컨테이너는 role="group" (Web)

크로스 플랫폼 차이점 (Platform Differences)#

Switcher는 Flutter와 Web이 동일한 items / index / direction / gestureEnabled / onIndexChanged / switcherStyle API와 동작을 갖습니다. Flutter는 커스텀 RenderObject로 자식 크기를 보간하고, Web은 CSS transform 트랙 슬라이드로 동일한 시각 결과를 냅니다. 전환 곡선은 양쪽 모두 CoreSwitcherStyle.curve (기본 CoreEasing.move) 를 읽습니다 — Flutter는 Curve로, Web은 cubic-bezier(...) timing function으로 변환합니다.

  • Tabs: 라벨 기반 콘텐츠 섹션 전환
  • Carousel: 자동 재생 슬라이드쇼
  • Swiper: 카드 스택 스와이프