Study

41. 패럴랙스 스크롤: 시각적 효과 극대화하기 | Flutter

구구 구구 2024. 8. 10. 11:00
반응형

카툰 스타일, 사이버펑크 스타일, dall-e

 

Flutter로 패럴랙스 스크롤 효과 구현하기

 

01. 서론

1) Flutter와 패럴랙스 효과 소개

Flutter는 Google에서 개발한 오픈 소스 UI 소프트웨어 개발 키트로, 단일 코드베이스를 사용하여 iOS, Android, 웹 및 데스크톱 애플리케이션을 개발할 수 있게 해줍니다. Flutter의 주요 장점 중 하나는 풍부한 위젯과 강력한 커스터마이징 기능을 제공한다는 점입니다. 이를 통해 개발자는 다양한 시각적 효과를 손쉽게 구현할 수 있습니다.

 

패럴랙스 효과는 스크롤 시 배경 이미지가 화면의 다른 부분보다 더 천천히 움직이게 하여 깊이감을 주는 시각적 효과입니다. 이 효과는 사용자 경험을 향상시키고 콘텐츠에 생동감을 더해줍니다. 패럴랙스 효과는 웹과 모바일 애플리케이션에서 자주 사용되며, 특히 이미지 중심의 콘텐츠에 유용합니다.

2) 패럴랙스 효과의 장점 및 활용 예시

패럴랙스 효과의 주요 장점은 시각적 깊이감을 제공하여 사용자에게 더 몰입감 있는 경험을 제공한다는 점입니다. 또한, 이 효과는 콘텐츠를 더욱 매력적으로 보이게 하여 사용자 참여도를 높일 수 있습니다. 예를 들어, 여행 애플리케이션에서는 패럴랙스 효과를 사용하여 여행지의 이미지를 더욱 인상 깊게 보여줄 수 있습니다. 사용자 스크롤 시 배경 이미지가 천천히 움직여 마치 해당 장소를 실제로 탐험하는 듯한 느낌을 줄 수 있습니다.

 

02. 패럴랙스 스크롤링 리스트 생성

1) ParallaxRecipe 위젯 생성 및 구조 설명

패럴랙스 효과를 구현하기 위해 먼저 `ParallaxRecipe`라는 위젯을 생성합니다. 이 위젯은 기본적으로 `SingleChildScrollView`와 `Column`을 사용하여 스크롤 가능한 리스트를 구현합니다. `ParallaxRecipe` 위젯의 구조는 다음과 같습니다:

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

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: List.generate(
          10,
          (index) => LocationListItem(
            imageUrl: 'https://example.com/image$index.jpg',
            name: 'Location $index',
            country: 'Country $index',
          ),
        ),
      ),
    );
  }
}
            

이 코드는 10개의 `LocationListItem`을 생성하여 스크롤 가능한 리스트를 만듭니다. 각 아이템은 고유한 이미지 URL, 장소 이름, 국가 이름을 가집니다.

2) SingleChildScrollView와 Column을 사용한 리스트 구현

`SingleChildScrollView`와 `Column`을 사용하여 스크롤 가능한 리스트를 구현합니다. `SingleChildScrollView`는 화면에 표시되는 콘텐츠가 길어질 때 스크롤을 가능하게 합니다. `Column`은 수직 방향으로 위젯을 배치하는 데 사용됩니다.

 

03. 리스트 아이템 구성

1) LocationListItem 위젯 생성

`LocationListItem` 위젯은 패럴랙스 효과를 적용할 리스트 아이템을 정의합니다. 이 위젯은 이미지를 배경으로 하고 그 위에 텍스트와 그라데이션을 겹쳐 배치합니다. `LocationListItem` 위젯은 다음과 같이 생성됩니다:

@immutable
class LocationListItem extends StatelessWidget {
  const LocationListItem({
    Key? key,
    required this.imageUrl,
    required this.name,
    required this.country,
  }) : super(key: key);

  final String imageUrl;
  final String name;
  final String country;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
      child: AspectRatio(
        aspectRatio: 16 / 9,
        child: ClipRRect(
          borderRadius: BorderRadius.circular(16),
          child: Stack(
            children: [
              _buildParallaxBackground(context),
              _buildGradient(),
              _buildTitleAndSubtitle(),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildParallaxBackground(BuildContext context) {
    return Positioned.fill(
      child: Image.network(
        imageUrl,
        fit: BoxFit.cover,
      ),
    );
  }

  Widget _buildGradient() {
    return Positioned.fill(
      child: DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: [Colors.transparent, Colors.black.withOpacity(0.7)],
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            stops: [0.6, 0.95],
          ),
        ),
      ),
    );
  }

  Widget _buildTitleAndSubtitle() {
    return Positioned(
      left: 20,
      bottom: 20,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            name,
            style: TextStyle(
              color: Colors.white,
              fontSize: 20,
              fontWeight: FontWeight.bold,
            ),
          ),
          Text(
            country,
            style: TextStyle(
              color: Colors.white,
              fontSize: 14,
            ),
          ),
        ],
      ),
    );
  }
}
            

`LocationListItem`은 `Padding`, `AspectRatio`, `ClipRRect`, `Stack`을 사용하여 구성됩니다. 배경 이미지, 그라데이션, 텍스트를 포함하여 사용자에게 시각적으로 매력적인 아이템을 제공합니다.

2) 배경 이미지, 텍스트, 그라데이션 적용

배경 이미지, 텍스트, 그라데이션을 적용하여 리스트 아이템을 구성합니다. `Positioned.fill`을 사용하여 이미지가 전체 영역을 차지하도록 설정하고, `BoxDecoration`과 `LinearGradient`를 사용하여 상단에서 하단으로 어두워지는 그라데이션을 적용합니다. `Positioned`를 사용하여 텍스트가 이미지의 왼쪽 아래에 배치되도록 설정합니다.

 

04. 패럴랙스 효과 구현

1) Flow 위젯과 FlowDelegate 사용 방법

패럴랙스 효과를 구현하기 위해 `Flow` 위젯과 `FlowDelegate`를 사용합니다. `Flow`는 고도로 맞춤화된 레이아웃을 생성할 수 있는 위젯으로, 자식 위젯들의 위치와 크기를 제어할 수 있습니다. `FlowDelegate`는 `Flow` 위젯의 레이아웃 로직을 정의하는 데 사용됩니다.

class ParallaxFlowDelegate extends FlowDelegate {
  ParallaxFlowDelegate({
    required this.scrollable,
    required this.listItemContext,
    required this.backgroundImageKey,
  }) : super(repaint: scrollable.position);

  final ScrollableState scrollable;
  final BuildContext listItemContext;
  final GlobalKey backgroundImageKey;

  @override
  BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {
    return BoxConstraints.tightFor(
      width: constraints.maxWidth,
    );
  }

  @override
  void paintChildren(FlowPaintingContext context) {
    final scrollableBox = scrollable.context.findRenderObject() as RenderBox;
    final listItemBox = listItemContext.findRenderObject() as RenderBox;
    final listItemOffset = listItemBox.localToGlobal(
      listItemBox.size.centerLeft(Offset.zero),
      ancestor: scrollableBox,
    );
    final viewportDimension = scrollable.position.viewportDimension;
    final scrollFraction =
        (listItemOffset.dy / viewportDimension).clamp(0.0, 1.0);
    final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1);
    final backgroundSize =
        (backgroundImageKey.currentContext!.findRenderObject() as RenderBox).size;
    final listItemSize = context.size;
    final childRect =
        verticalAlignment.inscribe(backgroundSize, Offset.zero & listItemSize);

    context.paintChild(
      0,
      transform: Transform.translate(offset: Offset(0.0, childRect.top)).transform,
    );
  }

  @override
  bool shouldRepaint(ParallaxFlowDelegate oldDelegate) {
    return scrollable != oldDelegate.scrollable ||
        listItemContext != oldDelegate.listItemContext ||
        backgroundImageKey != oldDelegate.backgroundImageKey;
  }
}
            

`ParallaxFlowDelegate`는 스크롤 위치에 따라 배경 이미지의 위치를 조정합니다. 이를 통해 사용자에게 깊이감을 주는 시각적 효과를 제공합니다.

2) 스크롤 위치에 따른 배경 이미지의 위치 조정

패럴랙스 효과를 구현하기 위해 스크롤 위치에 따라 배경 이미지가 이동하도록 설정합니다. `ParallaxFlowDelegate`의 `paintChildren` 메서드는 스크롤 위치에 따라 배경 이미지의 위치를 계산하고 이동시킵니다.

final scrollableBox = scrollable.context.findRenderObject() as RenderBox;
final listItemBox = listItemContext.findRenderObject() as RenderBox;
final listItemOffset = listItemBox.localToGlobal(
  listItemBox.size.centerLeft(Offset.zero),
  ancestor: scrollableBox,
);
final viewportDimension = scrollable.position.viewportDimension;
final scrollFraction = 
    (listItemOffset.dy / viewportDimension).clamp(0.0, 1.0);

final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1);
final backgroundSize = 
    (backgroundImageKey.currentContext!.findRenderObject() as RenderBox).size;
final listItemSize = context.size;
final childRect = 
    verticalAlignment.inscribe(backgroundSize, Offset.zero & listItemSize);

context.paintChild(
  0,
  transform: Transform.translate(offset: Offset(0.0, childRect.top)).transform,
);
            

스크롤 비율에 따라 배경 이미지의 위치를 조정하여 패럴랙스 효과를 구현합니다.

 

05. 코드 예제 및 설명

1) ParallaxRecipe 코드 예제

`ParallaxRecipe` 위젯은 전체 스크롤링 리스트를 구성하는 역할을 합니다. 이 위젯은 `SingleChildScrollView`와 `Column`을 사용하여 여러 개의 `LocationListItem`을 포함합니다.

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

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: List.generate(
          10,
          (index) => LocationListItem(
            imageUrl: 'https://example.com/image$index.jpg',
            name: 'Location $index',
            country: 'Country $index',
          ),
        ),
      ),
    );
  }
}
            

이 코드는 10개의 `LocationListItem`을 생성하여 스크롤 가능한 리스트를 만듭니다. 각 아이템은 고유한 이미지 URL, 장소 이름, 국가 이름을 가집니다.

2) LocationListItem 코드 예제

`LocationListItem` 위젯은 리스트 아이템의 구체적인 레이아웃과 스타일을 정의합니다. 배경 이미지, 그라데이션, 텍스트를 포함하여 구성됩니다.

@immutable
class LocationListItem extends StatelessWidget {
  const LocationListItem({
    Key? key,
    required this.imageUrl,
    required this.name,
    required this.country,
  }) : super(key: key);

  final String imageUrl;
  final String name;
  final String country;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
      child: AspectRatio(
        aspectRatio: 16 / 9,
        child: ClipRRect(
          borderRadius: BorderRadius.circular(16),
          child: Stack(
            children: [
              _buildParallaxBackground(context),
              _buildGradient(),
              _buildTitleAndSubtitle(),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildParallaxBackground(BuildContext context) {
    return Positioned.fill(
      child: Image.network(
        imageUrl,
        fit: BoxFit.cover,
      ),
    );
  }

  Widget _buildGradient() {
    return Positioned.fill(
      child: DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: [Colors.transparent, Colors.black.withOpacity(0.7)],
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            stops: [0.6, 0.95],
          ),
        ),
      ),
    );
  }

  Widget _buildTitleAndSubtitle() {
    return Positioned(
      left: 20,
      bottom: 20,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            name,
            style: TextStyle(
              color: Colors.white,
              fontSize: 20,
              fontWeight: FontWeight.bold,
            ),
          ),
          Text(
            country,
            style: TextStyle(
              color: Colors.white,
              fontSize: 14,
            ),
          ),
        ],
      ),
    );
  }
}
            

`LocationListItem`은 `Padding`, `AspectRatio`, `ClipRRect`, `Stack`을 사용하여 구성됩니다. 배경 이미지, 그라데이션, 텍스트를 포함하여 사용자에게 시각적으로 매력적인 아이템을 제공합니다.

3) 패럴랙스 효과를 위한 FlowDelegate 코드 예제

패럴랙스 효과를 구현하기 위해 `Flow`와 `FlowDelegate`를 사용합니다. `ParallaxFlowDelegate`는 스크롤 위치에 따라 배경 이미지의 위치를 조정합니다.

class ParallaxFlowDelegate extends FlowDelegate {
  ParallaxFlowDelegate({
    required this.scrollable,
    required this.listItemContext,
    required this.backgroundImageKey,
  }) : super(repaint: scrollable.position);

  final ScrollableState scrollable;
  final BuildContext listItemContext;
  final GlobalKey backgroundImageKey;

  @override
  BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {
    return BoxConstraints.tightFor(
      width: constraints.maxWidth,
    );
  }

  @override
  void paintChildren(FlowPaintingContext context) {
    final scrollableBox = scrollable.context.findRenderObject() as RenderBox;
    final listItemBox = listItemContext.findRenderObject() as RenderBox;
    final listItemOffset = listItemBox.localToGlobal(
      listItemBox.size.centerLeft(Offset.zero),
      ancestor: scrollableBox,
    );
    final viewportDimension = scrollable.position.viewportDimension;
    final scrollFraction =
        (listItemOffset.dy / viewportDimension).clamp(0.0, 1.0);
    final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1);
    final backgroundSize =
        (backgroundImageKey.currentContext!.findRenderObject() as RenderBox).size;
    final listItemSize = context.size;
    final childRect =
        verticalAlignment.inscribe(backgroundSize, Offset.zero & listItemSize);

    context.paintChild(
      0,
      transform: Transform.translate(offset: Offset(0.0, childRect.top)).transform,
    );
  }

  @override
  bool shouldRepaint(ParallaxFlowDelegate oldDelegate) {
    return scrollable != oldDelegate.scrollable ||
        listItemContext != oldDelegate.listItemContext ||
        backgroundImageKey != oldDelegate.backgroundImageKey;
  }
}
            

`ParallaxFlowDelegate`는 스크롤 위치에 따라 배경 이미지가 이동하도록 설정하여 패럴랙스 효과를 구현합니다. 이를 통해 사용자에게 깊이감을 주는 시각적 효과를 제공합니다.

 

06. 결론

1) 패럴랙스 효과의 구현 결과 요약

Flutter를 사용하여 패럴랙스 스크롤 효과를 구현하는 방법을 단계별로 설명했습니다. `SingleChildScrollView`와 `Column`을 사용하여 스크롤 가능한 리스트를 생성하고, `LocationListItem` 위젯을 통해 배경 이미지, 그라데이션, 텍스트를 포함한 리스트 아이템을 구성했습니다. `Flow`와 `FlowDelegate`를 사용하여 스크롤 위치에 따라 배경 이미지가 이동하는 패럴랙스 효과를 구현했습니다. 이를 통해 사용자에게 깊이감 있는 시각적 경험을 제공할 수 있습니다.

2) 추가적인 Flutter UI 효과 구현을 위한 팁

- 애니메이션 효과: Flutter의 애니메이션 위젯을 사용하여 더 복잡한 시각적 효과를 구현할 수 있습니다. 예를 들어, `AnimatedOpacity`를 사용하여 요소의 투명도를 조정하거나, `AnimatedContainer`를 사용하여 크기와 위치를 애니메이션으로 변경할 수 있습니다.

 

- 커스텀 위젯: Flutter의 강력한 커스터마이징 기능을 활용하여 독창적인 UI 요소를 설계해보세요. `CustomPainter`를 사용하여 복잡한 그래픽을 그리거나, `ClipPath`를 사용하여 커스텀 클리핑 경로를 정의할 수 있습니다.

 

- 반응형 디자인: 다양한 화면 크기와 해상도에 대응할 수 있도록 반응형 디자인을 구현하세요. `MediaQuery`와 `LayoutBuilder`를 사용하여 화면 크기에 따라 위젯의 레이아웃을 조정할 수 있습니다.


관련된 다른 글도 읽어보시길 추천합니다

 

2024.08.02 - [Study] - 37. Flutter에서 긴 리스트를 효율적으로 처리하는 방법 | Flutter

 

37. Flutter에서 긴 리스트를 효율적으로 처리하는 방법 | Flutter

Flutter에서 긴 리스트를 효율적으로 처리하는 방법 01. 서론1) Flutter에서 긴 리스트의 중요성Flutter는 모바일, 웹, 데스크탑 등 다양한 플랫폼에서 일관된 UI를 제공하는 오픈 소스 UI 프레임워크입

guguuu.com

2024.08.02 - [Study] - 36. Flutter에서 일정 간격이 있는 리스트 만들기 | Flutter

 

36. Flutter에서 일정 간격이 있는 리스트 만들기 | Flutter

Flutter로 일정 간격이 있는 리스트 만들기 01. 서론Flutter에서 리스트 간격의 필요성Flutter는 크로스플랫폼 모바일 애플리케이션 개발을 위한 강력한 프레임워크로, 다양한 UI 구성 요소를 제공합

guguuu.com

2024.08.02 - [Study] - 35. Flutter로 다양한 유형의 아이템이 포함된 리스트 만들기 | Flutter

 

35. Flutter로 다양한 유형의 아이템이 포함된 리스트 만들기 | Flutter

Flutter로 다양한 유형의 아이템이 포함된 리스트 만들기 01. 서론Flutter에서 혼합형 리스트의 필요성Flutter는 크로스플랫폼 모바일 애플리케이션 개발을 위한 강력한 프레임워크입니다. 많은 앱에

guguuu.com


읽어주셔서 감사합니다

공감은 힘이 됩니다

 

:)

반응형