본문 바로가기
IT/Flutter

<Flutter> Animations sample 적용하기 ( AnimatedContainer )

by 세계 최고의 AI Engineer_naknak 2023. 3. 23.

네, 이번 시간에는 Flutter에 존재하는 개념에 대한 공부가 아니라 Flutter 공식 문서에서 찾을 수 있는 Flutter samples에 있는 깃허브에 있는 animations를 해석하고 적용하는 과정을 포스팅하려고 합니다!

본 포스팅의 목적은 영어로 쓰여진 깃허브 자료를 해석하고 실제 적용할 수 있는 실력을 기르기 위함에 있습니다!

시간이 좀 걸리더라도 다른 자료를 보지 않고 차근 차근 진행해보도록 하겠습니다!

 

https://docs.flutter.dev/get-started/learn-more

 

Learn more

More resources to help you learn Flutter.

docs.flutter.dev

Flutter samples에 가서 보면

 

다음과 같은 웹페이지가 로딩되고 4번째에 있는 Animation Samples를 클릭해서 Source Code 버튼을 클릭하면

https://github.com/flutter/samples/tree/main/animations

 

GitHub - flutter/samples: A collection of Flutter examples and demos

A collection of Flutter examples and demos. Contribute to flutter/samples development by creating an account on GitHub.

github.com

깃허브로 들어갈 수 있습니다.

그럼 Readme.md 먼저 읽어 볼까요?

Animation Samples

Sample apps that showcase Flutter's animation features

애니메이션 샘플인데 샘플 앱이 플루터의 애니메이션 특징을 보여주는 쇼케이스... 음... 예시라고 번역하면 괜찮지 않을까요..ㅎㅎ?

 

Goals

  • Demonstrate the building blocks for animations and how they work together.
  • Provide samples for common patterns and use-cases.

목적에 대해서도 말해주네요. 애니메이션들에 대해 구축된 블럭들과 그 블럭들이 어떻게 같이 일하는지를 입증, 증명, 뭐 보여준다고 하네요. 2번째 목표는 공통의 패턴과 사용사례들에 대한 샘플을 제공하기 위함에 있다고 해요.

 

Samples

Basics

Building blocks and patterns

  1. AnimatedContainerDemo: Demonstrates how to use AnimatedContainer.
  2. PageRouteBuilderDemo: Demonstrates how to use Tween and Animation to build a custom page route transition.
  3. AnimationControllerDemo: Demonstrates how to use an AnimationController.
  4. TweenDemo: Demonstrates how to use a Tween with an AnimationController.
  5. AnimatedBuilderDemo: Demonstrates how to use an AnimatedBuilder with an AnimationController.
  6. CustomTweenDemo: Demonstrates how to extend Tween.
  7. TweenSequenceDemo: Demonstrates how to use TweenSequence to build a button that changes between different colors.
  8. FadeTransitionDemo: Demonstrates how to use FadeTransition.

그러니까 여기에 구현된 샘플에는 1번 AnimatedContainer가 있고 2번에는 pageRouteBuilder가 있답니다. 

2번은 Tween과 Animation을 어떻게 사용하는지를 알려주는데 얘네들이 custom page route transition 을 빌드 해준다고 하네요? 네, 직접 보기 전까지는 뭔말인지 와닿지가 않네요!

암튼 애니메이션의 basic에는 총 8가지가 있어요~!

Misc

Other uses-cases and examples

  • RepeatingAnimationDemo: Demonstrates how to repeat an animation.
  • ExpandCardDemo: Demonstrates how to use AnimatedCrossFade to fade between two widgets and change the size.
  • CarouselDemo: Demonstrates how to use PageView with a custom animation.
  • FocusImageDemo: Demonstrates how to measure the size of a widget and expand it using a PageRouteBuilder.
  • PhysicsCardDragDemo: Demonstrates how to run an AnimationController with a spring simulation.
  • CardSwipeDemo: A swipeable card that demonstrates how to use gesture detection to drive an animation.
  • AnimatedList: Demonstrates how to use AnimatedList.
  • AnimatedPositionedDemo: Demonstrates how to use AnimatedPositioned.
  • AnimatedSwitcherDemo: Demonstrates how to use AnimatedSwitcher.
  • HeroAnimationDemo: Demonstrates how to use Hero animation.
  • CurvedAnimationDemo: Demonstrates how to use different curves in CurvedAnimation.

네, misc는 miscellaneous의 약어이고 "여러가지 종류의" 라는 뜻을 가지고 있어요. 기타라고 생각하면 쉽겠네요!

그래서 기타를 살펴보면 기본에서 포함되지 않은 여러가지 종류의 애니메이션이 있는 걸 확인할 수 있어요!

Other Resources

네, 여러 자료가 하이퍼링크로 되있는 것도 확인할 수 있어요.

좋아요! 그러면 이제 basic부터 하나씩 확인해볼까요?

깃헙에 lib\src\basics에 가면 9가지 .dart파일이 있는데 basics.dart 파일은 왜 필요한지 잘모르겠어요, 혹시 아시는 분이 계시다면 댓글로 알려주시면 감사할 거 같아요!

(저 알았어요! 조금 있다가 설명할게요!)

 

먼저 pubspec.yaml에 dependency를 추가해줘야 해요.

dependencies:
  flutter:
    sdk: flutter
  flutter_animate: ^4.1.0
  go_router: ^6.0.0
  window_size: # plugin is not yet part of the flutter framework
    git:
      url: https://github.com/google/flutter-desktop-embedding.git
      path: plugins/window_size

요런식으로 먼저 추가해주시고 저장해주시면 flutter pub get으로 알아 필요한 plugin을 설치해줄거예요!

 

그리고 깃헙에 나와있는 걸 그대로 따라해볼게요.

lib에 bascis와 misc 폴더를 생성시켜주세요. 

이제 저 폴더 안에 dart 클래스 파일을 생성해서 사용할거예요!

그럼 먼저 다른 클래스와 상관없이 독립적으로 구현되는 AnimatedContainer 먼저 살펴볼게요!

 

AnimatedContainer

아하! 글만보고는 어떤 건지 감이 안왔는데 구현된 걸 보니까 이게 어떤 애니메이션인지 정확하게 알거 같아요!

일단 AnimatedContainer은 화면 이동시 넣는 애니메이션이 아니예요! 

버튼을 누르면 해당 컨테이너가 부드럽게 전환되는 애니메이션이에요! 

https://github.com/flutter/samples/blob/main/animations/lib/src/basics/animated_container.dart

 

GitHub - flutter/samples: A collection of Flutter examples and demos

A collection of Flutter examples and demos. Contribute to flutter/samples development by creating an account on GitHub.

github.com

전체 코드예요. 이제부터 어지러우실 수도 있으니(제가 설명을 너무 어지럽게 하거든요ㅠ) 전체 코드를 보시면서 따라오셨으면 좋겠어요.

그럼 이제 코드를 보면서 어떻게 적용하는지 살펴볼게요!

animated_container.dart 파일에 보면 저작권에 대해서 주석이 달려 있어요.

// Copyright 2019 The Flutter team. All rights reserved.

// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.

라고 하네요. ㅎㅎ 

암튼 전체 코드말고 부분 부분적으로 살펴볼게요!

double generateBorderRadius() => Random().nextDouble() * 64;
double generateMargin() => Random().nextDouble() * 64;
Color generateColor() => Color(0xFFFFFFFF & Random().nextInt(0xFFFFFFFF));

먼저 저 컨테이너의 경계선의 둥근 정도, 여백, 색을 랜덤으로 정해주는 메소드라는 걸 확인할 수 있어요.

 

  @override
  void initState() {
    super.initState();
    color = Colors.deepPurple;
    borderRadius = generateBorderRadius();
    margin = generateMargin();
  }

  void change() {
    setState(() {
      color = generateColor();
      borderRadius = generateBorderRadius();
      margin = generateMargin();
    });
  }

그래서 보면 animated_container가 처음 생성될 때 container에 대한 색, 둥근정도, 여백을 랜덤으로 설정하고 change() 메서드 안에 setState를 통해서 change()가 invoke 될 때마다 container의 모양을 바꿀 수 있게 짜놓은 거 같아요.

    // This widget is built using an AnimatedContainer, one of the easiest to use
    // animated Widgets. Whenever the AnimatedContainer's properties, such as decoration,
    // change, it will handle animating from the previous value to the new value. You can
    // specify both a Duration and a Curve for the animation.
    // This Widget is useful for designing animated interfaces that just need to change
    // the properties of a container. For example, you could use this to design expanding
    // and shrinking cards. 
 
이런 주석이 달려 있어요. 가장 쉽게 사용할 수 있는 위젯이라고 하면서 컨테이너의 속성을 변환시킬 때 유용하다고 하네요 뭐, 확장거나 움츠려드는 카드 같은 거에 사용할 수 있다고 해요.
child: AnimatedContainer( // widget 입니다
                  margin: EdgeInsets.all(margin), // 아까 랜덤으로 설정했던 margin
                  decoration: BoxDecoration( 
                    color: color, // 아까 랜덤으로 설정했던 color
                    borderRadius: BorderRadius.circular(borderRadius),
                  ), // 아까 랜덤으로 설정했던 borderRadius
                  duration: const Duration(milliseconds: 400),
                ), // Creates a container that animates its parameters implicitly. 
              ),  // 라고 하는데 매개변수에서 설정하는 값만큼 애니매션되는 컨테이너를 만드는 거라네요
              	// 그러니까 만들어질 때 걸리는 시간이라고 생각하면 될거 같아요
                
        ElevatedButton(
              child: const Text(
                'change',
              ),
              onPressed: () => change(),  // 클릭 시 change()에 있는 setState()에 의해 
              							// 새로고침된다고 생각하면 쉬울 거 같아요!
            ),

이런 식으로 AnimatedContainer 위젯을 사용하면 귀엽게 animates 되는 컨테이너를 사용할 수 있게 되요!

저기 margin이나 color 같이 랜덤으로 부여되는 값을 설정하면 목적에 맞춰서 customize 할 수도 있겠네요! 

귀엽게 활용할 수 있는 위젯 같아요.

 

비즈니스 로직

다음 위젯으로 넘어가기 전에 main.dart에 구현된 비즈니스 로직을 살펴보도록 할게요!

void setupWindow() {
  if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
    WidgetsFlutterBinding.ensureInitialized();
    setWindowTitle('Animation Samples');
    setWindowMinSize(const Size(windowWidth, windowHeight));
    setWindowMaxSize(const Size(windowWidth, windowHeight));
    getCurrentScreen().then((screen) {
      setWindowFrame(Rect.fromCenter(
        center: screen!.frame.center,
        width: windowWidth,
        height: windowHeight,
      ));
    });
  }
}

이 코드는 직관적으로 해석할 수 있어요. 운영체제에 따라서 크기를 정해주는 메소드예요.

또 유용하게 사용할 수 있는 부분이 있다면

class Demo {
  final String name;
  final String route;
  final WidgetBuilder builder;

  const Demo({
    required this.name,
    required this.route,
    required this.builder,
  });
}

C언어에서는 structure라고 하고 Java에서는 속성만을 저장시키는 enum class 예요.

Flutter team에서는 위에 있는 다양한 위젯을 구현해서 보여줘야하는데 저 많은 위젯들을 main 홈페이지에 한번에 뿌리기 위해서 Demo라는 Class를 선언해줬어요!

그래서 이 Demo Class를 사용한 배열의 일부를 살펴볼게요.

basicDemos = [
  Demo(
    name: 'AnimatedContainer', // Demo에 있는 속성에 name, route와
    route: AnimatedContainerDemo.routeName,
    builder: (context) => const AnimatedContainerDemo(), // builder를 할당하는 걸 확인할 수 있습니다. 
  ), ...];

자 여기서 route에 담기는 routeName은 어떻게 import 해줬을까요? 여기서! 제가 아까 질문 했던 내용의 답이 나옵니다!

AnimatedContainerDemo는 다른 .dart 파일에 선언된 클래스이기 때문에 main.dart에서 사용하려면 import를 통해서 해당 dart파일을 main에 알려야합니다. 그런데 알려야 하는 파일이 basics위젯들과 misc 위젯들이잖아요? 굉장히 많죠>??

그래서

#basics.dart

export 'animated_builder.dart';
export 'animated_container.dart';
export 'animation_controller.dart';
export 'custom_tween.dart';
export 'fade_transition.dart';
export 'page_route_builder.dart';
export 'tween_sequence.dart';
export 'tweens.dart';

이 파일을 import 'src/basics/basics.dart'; import 해줘서 코드를 굉장히 깔끔하게 처리해주는 거죠!!!

크으~ 정말 멋지지 않나요? 완전 멋있어요!

 

암튼,

basicDemos[] 이 배열을 화면에 List 형식으로 뿌리고 클릭하면 builder와 route 정보를 받아서 해당 위젯으로 이동할 수 있도록 해주는 거예요! 

 

다음이 방금 제가 설명한 말을 구현한 코드예요!

ListView(
        children: [
          ListTile(title: Text('Basics', style: headerStyle)),
          ...basicDemos.map((d) => DemoTile(demo: d)), ),
          // listview를 통해서 basicDemos에 있는 요소를 화면에 뿌려줘요
          // 그리고 각 요소들은 d라는 값에 저장되고 그 d는 DemoTile이라는 클래스 속성에 map되죠. 
          // map은 뭐랄까... 할당된다고 생각해볼까요?
          
          
class DemoTile extends StatelessWidget {
  final Demo demo;

  const DemoTile({required this.demo, super.key});

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(demo.name),  // d에서 받아온 name을 제목으로
      onTap: () {
        context.go('/${demo.route}');  // d에 저장된 route로 context.go 하라고 하네요
      },
    );
  }
}

자, 여기서부터는 코드에 대한 제 주관적인 해석이 들어가니 알아서 거르셔서 들어주시면 감사하겠습니다!

 

main.dart에 보면 궁금한게 있었어요. 

먼저 코드를 보면

void main() {
  setupWindow();
  runApp(const AnimationSamples());
}

라고 합니다. 그러니까 실행을 하면 제일 먼저 AnimationSamples()라는 클래스가 생성되는 거죠.

class AnimationSamples extends StatelessWidget {
  const AnimationSamples({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Animation Samples',
      theme: ThemeData(
        colorSchemeSeed: Colors.deepPurple,
        useMaterial3: true,
      ),
      routerConfig: router,
    );
  }
}

AnimationSamples 클래스에 특별한건 router라는 메소드죠. 부연 설명으로 router은 주소를 통해서 값을 전달하거나 다음 이벤트를 발생시키는 녀석이라고 생각하시면 좋을 거 같아요.

여기서 봐야할 부분은 routerConfig: router라는 녀석입니다. routerConfig는 An object to configure the underlying [Router].

라고 합니답. router를 구성하는 객체라고 하네요? 스킵하죠. 저는 모르겠거등요.

그래서 router를 살펴볼까요?

final router = GoRouter( // GoRouter는 페이지 이동시 URL 기반의 API를 이용해서 쉽게 이동할 수 있도록 도와주는 패키지입니다
  routes: [
    GoRoute(
      path: '/', // HomePage()로 가기 위해서 특별한 URL이 필요 없으니 path를 /로 해줍니다
      builder: (context, state) => const HomePage(),
      routes: [ 
        for (final demo in basicDemos) 
          GoRoute(
            path: demo.route,
            builder: (context, state) => demo.builder(context),
          ),
      ],
    ),
  ],
);

여기서부터 주관이 들어갑니다 그러니까 HomePage로 GoRoute를 통해 먼저 이동하고 HomePage  내에서는 새로운 GoRoute를 만들어 줘서 여러 경로를 route[] 안에 저장합니다. 저장되는 요소는 basicDemos안에 있는 애니메이트 위젯들이 가진 demo 라는 enum class 입니다. basicDemos에 들어 있는 요소들이 가진 route와 builder를 넣어줍니다.

네, 그렇습니다. HomePage에  GoRoute에는 basics에 있는 여러 위젯으로 가는 route와 Widgetbuilder인 builder가 저장되어 있는 겁니다!

그래서 아까 봤던 HomePage Class를 보면

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    final headerStyle = Theme.of(context).textTheme.titleLarge;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Animation Samples'),
      ),
      body: ListView(
        children: [
          ListTile(title: Text('Basics', style: headerStyle)),
          ...basicDemos.map((d) => DemoTile(demo: d)),
          // ListTile(title: Text('Misc', style: headerStyle)),
          // ...miscDemos.map((d) => DemoTile(demo: d)),
        ],
      ),
    );
  }
}

class DemoTile extends StatelessWidget {
  final Demo demo;

  const DemoTile({required this.demo, super.key});

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(demo.name),
      onTap: () {
        context.go('/${demo.route}');
      },
    );
  }
}

ListView에서 DemoTile에 정보를 저장하고 DemoTile에서 탭하면 demo.route를 통해 해당 위젯으로 이동할 수 있게 되는 겁니다. context.go가 뭔데? route랑 무슨 연관이 있는데? 라는 질문이 생기더라구요.

그래섯 go로 가봤습니다.

  /// Navigate to a location.
  void go(String location, {Object? extra}) =>
      GoRouter.of(this).go(location, extra: extra);

놀랍네요!!! 매개변수로 받은 String을 이용해서 GoRouter를 통해서 스크린을 이동하는 군요!  GoRouter.of가 받는 매개변수 타입은 BuildContext예요. 그래서 제가 억지로 끼워맞췄을 때 this에 전달되는 값은 homepage()의 context이고 여기에 GoRouter에 대한 정보가 담겨 있어요. 그래서 GoRouter.of(this).go(loacation, extra: extra);에서 보면 go(location에 이동하기 원하는 애니메이트 위젯의 route 주소가 담기게 되잖아요?

 

예를 들어 AnimationContainer의 페이지 이동을 위한 route는 basics/animated_container예요. 이걸 context.go("basics/animated_container") 이렇게 매개변수로 주면 HomePage에 저장된 GoRouter로 context를 통해 접근하고 GoRouter - route[]에 저장된 요소들 중에서 route가 같은 요소를 가져옵니다. 그리고 저장된 route와 builde를 통해서 AnimationContainer 스크린으로 전환할 수 있도록 하는 거죠!!!!!!!!!!!!

아닐 수도 있지만 그래도 코드의 흐름상 맞지 않을까요?

 

AnimatedContainer만 포스트했는데 글이 이렇게 길어질지는 생각도 못했어요. 그런데 이번엔 main.dart에 있는 코드를 분석하고 흐름을 파악하느라 시간이 조금 걸려서 그렇지 다음 포스트 할 때는 그렇게 시간이 많이 걸리지 않을 거라고 생각해요!

 

그럼 다음에 다시 찾아오겠습니다!

댓글