관리 메뉴

Bull

[Flutter::Animation] Hero 기법 본문

Software Framework/Flutter

[Flutter::Animation] Hero 기법

Bull_ 2024. 7. 25. 08:17

Flutter 애니메이션으로 자주 사용되는 기법은 Fade와 Hero가 있습니다. 이번엔 Hero가 뭔지 언제 주로 사용하는 지에 대한 기본적인 내용만 다뤄 보겠습니다.

Hero 기본 개념

Flutter의 Hero 위젯은 두 개의 화면 사이에서 애니메이션 효과를 통해 부드럽게 전환되는 위젯을 제공합니다. 주로 이미지나 특정 위젯이 한 화면에서 다른 화면으로 이동할 때 자연스러운 전환 효과를 구현하는 데 사용됩니다.

 

Hero 애니메이션은 소스 화면과 목적지 화면에서 동일한 Hero 태그를 가진 위젯을 식별하여 작동합니다. 이렇게 하면 Flutter는 두 화면 사이에서 위젯의 위치와 크기를 애니메이션으로 연결할 수 있습니다.

Hero의 이름이 Hero인 이유?

Hero class 공식 문서에서는 Hero 위젯을 날아간다라고 표현하였습니다. 공식적으로 이 주제에 대한 설명은 못찾았지만 우리가 아는 "영웅"의 역할이 슈퍼맨과 같이 날아가거나 주인공이 눈에 띄게 튀는 효과를 내서 Hero라는 이름이 붙었다고 확신해도 될 거 같습니다. GPT와 제 추측이지만 만약 이게 사실이 아니더라도 Hero가 어떤 위젯인지 표현할 때 알기 쉬운 비유법이라고 생각되니 저는 이렇게 생각해도 좋을 것 같습니다.

CODE

본격적으로 Hero의 CODE를 살펴보겠습니다.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hero Animation Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: FirstPage(),
    );
  }
}

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('First Page')),
      body: GridView.count(
        crossAxisCount: 3,
        children: <Widget>[
          GestureDetector(
            onTap: () {
              Navigator.push(context, MaterialPageRoute(builder: (_) {
                return DetailPage(
                  tag: 'hero-image-1',
                  imageUrl: 'https://picsum.photos/250?image=9',
                  description: 'Image 1 Description',
                );
              }));
            },
            child: Hero(
              tag: 'hero-image-1',
              child: Image.network('https://picsum.photos/250?image=9'),
            ),
          ),
          GestureDetector(
            onTap: () {
              Navigator.push(context, MaterialPageRoute(builder: (_) {
                return DetailPage(
                  tag: 'hero-image-2',
                  imageUrl: 'https://picsum.photos/250?image=10',
                  description: 'Image 2 Description',
                );
              }));
            },
            child: Hero(
              tag: 'hero-image-2',
              child: Image.network('https://picsum.photos/250?image=10'),
            ),
          ),
          GestureDetector(
            onTap: () {
              Navigator.push(context, MaterialPageRoute(builder: (_) {
                return DetailPage(
                  tag: 'hero-image-3',
                  imageUrl: 'https://picsum.photos/250?image=11',
                  description: 'Image 3 Description',
                );
              }));
            },
            child: Hero(
              tag: 'hero-image-3',
              child: Image.network('https://picsum.photos/250?image=11'),
            ),
          ),
        ],
      ),
    );
  }
}

class DetailPage extends StatelessWidget {
  final String tag;
  final String imageUrl;
  final String description;

  DetailPage({required this.tag, required this.imageUrl, required this.description});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Detail Page')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            SizedBox(height: 50),
            Hero(
              tag: tag,
              child: Container(
                child: Image.network(imageUrl, fit: BoxFit.cover),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: Text(
                description,
                style: TextStyle(fontSize: 18),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

이미지 소스는 url을 통해 가져오니 DartPad를 통하여 확인 가능합니다. 결과 내용을 설명해보겠습니다.

 

사진을 누르면 페이지가 설명페이지로 전환되며 기존에 첫 페이지에 있던 사진이 없어지지 않고 부드럽게 전환되며 정해진 위치로 오게됩니다. 저는 해당 이미지에 대해 강조해주는 느낌을 받았습니다. Hero 위젯의 역할이 이런 게 아닐까요?

 

Hero의 동작을 한 문장으로 표현해서 글만 보면 힘들 수 있는데 결과를 보면 바로 이해할 수 있을 것 같습니다.

CODE가 어떻게 흘러가는지, 어떤 약속을 지켜야 하는지도 중요합니다. 이번엔 그것에 대해서 말해보겠습니다.

 

본론만 먼저 말하자면 두 페이지 모두 Hero 위젯을 사용합니다. 여기서 중요한 건, tag프로퍼티를 사용했다는 점 인데요. 감이 오지 않나요? 바로 두 페이지간의 Hero 위젯에 tag 프로퍼티를 일치시켜 주어야합니다. 사실상 본론만으로 충분해요. 다른 개념은 라우트에 대한 기반지식이나 이미지 가져오고 파라미터 전달해주는 정도는 Hero 위젯과는 약간 동떨어진 내용일 수 있으니깐요. 기본으로 알아 두시는 게 좋습니다.

 

생소할 거 같은 부분만 살펴 보겠습니다.

 

Image.network: 이미지를 url을 통해 가져올 수 있습니다. 첫 번째 인자로 url을 받고 fit프로퍼티는 사진을 어떻게 컨테이너에 표현할 것인지를 나타냅니다. fit 프로퍼티에 대한 내용이 궁금하다면 Boxfit 공식 문서를 확인해주세요.

 

Route

Navigator.push(context, MaterialPageRoute(builder: (_) {
                return DetailPage(
                  tag: 'hero-image-2',
                  imageUrl: 'https://picsum.photos/250?image=10',
                  description: 'Image 2 Description',
                );
              }));

Flutter에서 기본으로 제공하는 라우트를 이동하는 방법입니다. Navigator.push를 통해 위젯을 스택에 쌓아서 화면으로 보여줍니다. 반환값은 우리가 정의했던 DetailPage로 넘어갈 수 있게 반환하고 인자로 tag, url, description을 전달해 줍니다. 원하는 사진을 눌렀을 때 GestureDetector class의 onTap으로 함수를 실행할 수 있는데 이 때, 각각의 이미지 tag에 대한 값을 전달해줄 수 있습니다.

 

이를 통해 DetailPage에서는 다시 Hero 위젯을 사용하여 전달받은 파라미터를 사용하여 눌렀던 이미지 내용과 동일한 tag를 사용해서 표현할 수 있습니다.

Behind the scenes

Hero 위젯은 어떻게 동작하는 걸까요? 한 번 Hero 공식 문서의 내용을 그대로 설명해보겠습니다.

https://docs.flutter.dev/ui/animations/hero-animations

Hero를 실행하기 전입니다. 소스 지점은 위젯이 표시되어 있고 목적 지점은 아직 존재하지 않습니다. 여기 Overlay 라는 위젯이 표시되는데요.

 

Overlay는 Flutter에서 위젯 트리를 통해 여러 위젯을 쌓아 올리는 데 사용되는 위젯입니다. 이는 일반적으로 앱의 위젯 계층 구조에서 가장 상위에 위치하며, 위젯을 임시로 표시하거나 특정 애니메이션 효과를 적용하는 데 사용합니다. Hero 애니메이션에서는 오버레이를 사용하여 애니메이션 중에 히어로 위젯을 화면 위에 떠있는 것처럼 보이게 합니다. 설명이 어렵다면 어떤 위젯을 화면에 표시하기 전에 아무것도 없는 장판같은 프레임이라고 봐도 무방합니다.

https://docs.flutter.dev/ui/animations/hero-animations


Navigator를 통해 라우트를 눌렀을 때 입니다. 여기서 t는 누르는 시점에서 초기부분이라는 것을 0에서 1로 표시합니다.

 

Dest Hero는 우리의 코드로 치면 DetailPage에 나와야 할 Hero 위젯을 우선 Overlay위에 놓습니다. Dest Route 부분은 아직 랜딩이 안됐네요.

https://docs.flutter.dev/ui/animations/hero-animations


이제 Hero 위젯은 Dest Route에 설정했던 Hero 위젯의 위치로 날아갑니다. Hero의 직사각형 경계는 Hero's createRectTween 속성에 지정된 Tween<Rect>를 사용하여 애니메이션화됩니다. 기본적으로 Flutter는 직사각형의 반대쪽 모서리를 곡선 경로를 따라 애니메이션화하는 MaterialRectArcTween 인스턴스를 사용합니다.

https://docs.flutter.dev/ui/animations/hero-animations


비행이 완료되면 Flutter는 Hero 위젯을 Overlay에서 목적지 경로로 이동합니다. 이제 Overlay는 비어 있습니다.
Dest Route의 마지막 위치에 Dest Hero가 나타납니다.