Dandy Now!
  • [Flutter] "Do it! 플러터 앱 프로그래밍" - 애니메이션 활용하기 | 애니메이션 구현, 인트로 화면, 스크롤 시 역동적인 앱바
    2022년 02월 18일 23시 36분 40초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    "조준수. (2021). Do it! 플러터 앱 프로그래밍. 이지스퍼블리싱", 11장을 실습하였다. AnimatedContainer 위젯을 이용해서 애니메이션을 구현한다. 그래프 애니메이션, 애니메이션이 적용된 인트로 화면, 스크롤 시 역동적인 앱바를 만들어 보았다. 

     

    Do it! 플러터 앱 프로그래밍

    플러터 기본 & 고급 위젯은 물론오픈 API와 파이어베이스를 이용한 앱 개발부터 배포까지!플러터 SDK 2.x 버전을 반영한 개정판!이 책은 플러터의 기초부터 고급 활용법까지 다루어 다양한 영역에

    book.naver.com

     

    애니메이션 구현하기

    애니메이션은 수학적인 계산과 상상이 필요한 부분이다. 계산이나 작동 시간이 조금만 이상해도 어울리지 않는 느낌이 들기 때문이다. 또한 앱 동작이 느려질 수 있으므로 필요한 순간에만 사용하는 것이 좋다. 다음 주소에 접속하면 플러터 API의 Curves 클래스가 제공하는 다양한 애니메이션 모양을 확인할 수 있다.

    https://api.flutter.dev/flutter/animation/Curves-class.html

     

    그래프 애니메이션

    // main.dart
    import 'package:flutter/material.dart';
    import 'people.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: AnimationApp(),
        );
      }
    }
    
    class AnimationApp extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _AnimationApp();
    }
    
    class _AnimationApp extends State<AnimationApp> {
      List<People> peoples = new List.empty(growable: true);
      int current = 0;
    
      @override
      void initState() {
        peoples.add(People('이일남', 180, 92));
        peoples.add(People('이이남', 162, 55));
        peoples.add(People('이삼남', 177, 75));
        peoples.add(People('이사남', 159, 48));
        peoples.add(People('이오남', 194, 110));
        peoples.add(People('이육남', 140, 60));
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Animation Example'),
          ),
          body: Container(
            child: Center(
              child: Column(
                children: <Widget>[
                  // SizedBox 안에는 Row 이용해 텍스트 위젯 애니메이션을 구현하는 AnimatedContainer를 만든다.
                  SizedBox(
                    child: Row(
                      children: <Widget>[
                        SizedBox(
                            width: 100,
                            child: Text('이름 : ${peoples[current].name}')),
                        AnimatedContainer(
                          duration: Duration(seconds: 2), // 재생 시간 설정
                          curve: Curves.bounceIn, // 애니메이션 모양 설정
                          color: Colors.amber,
                          child: Text(
                            '키 ${peoples[current].height}',
                            textAlign: TextAlign.center,
                          ),
                          width: 50,
                          height: peoples[current].height,
                        ),
                        AnimatedContainer(
                          duration: Duration(seconds: 2),
                          curve: Curves.easeInCubic,
                          color: Colors.blue,
                          child: Text(
                            '몸무게 ${peoples[current].weight}',
                            textAlign: TextAlign.center,
                          ),
                          width: 50,
                          height: peoples[current].weight,
                        ),
                        AnimatedContainer(
                          duration: Duration(seconds: 2),
                          curve: Curves.linear,
                          color: Colors.pinkAccent,
                          child: Text(
                            'bmi ${peoples[current].bmi.toString().substring(0, 2)}',
                            textAlign: TextAlign.center,
                          ),
                          width: 50,
                          height: peoples[current].bmi,
                        ),
                      ],
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      crossAxisAlignment: CrossAxisAlignment.end,
                    ),
                    height: 200,
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        if (current < peoples.length - 1) {
                          current++;
                        }
                      });
                    },
                    child: Text('다음'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        if (current > 0) {
                          current--;
                        }
                      });
                    },
                    child: Text('이전'),
                  )
                ],
                mainAxisAlignment: MainAxisAlignment.center,
              ),
            ),
          ),
        );
      }
    }

     

    // people.dart
    class People {
      String name;
      double height;
      double weight;
      double? bmi;
    
      People(this.name, this.height, this.weight) {
        bmi = weight / ((height / 100) * (height / 100));
      }
    }

     

    [그림 1] 그래프 애니메이션

     

    색상 변경 애니메이션

    몸무게에 색상 변경 애니메이션을 적용하였다.

    import 'package:flutter/material.dart';
    import 'people.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: AnimationApp(),
        );
      }
    }
    
    class AnimationApp extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _AnimationApp();
    }
    
    class _AnimationApp extends State<AnimationApp> {
      List<People> peoples = new List.empty(growable: true);
      Color weightColor = Colors.blue;
      int current = 0;
    
      @override
      void initState() {
        peoples.add(People('이일남', 180, 92));
        peoples.add(People('이이남', 170, 75));
        peoples.add(People('이삼남', 165, 59));
        peoples.add(People('이사남', 145, 39));
        peoples.add(People('이오남', 194, 110));
        peoples.add(People('이육남', 155, 60));
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Animation Example'),
          ),
          body: Container(
            child: Center(
              child: Column(
                children: <Widget>[
                  // SizedBox 안에는 Row 이용해 텍스트 위젯 애니메이션을 구현하는 AnimatedContainer를 만든다.
                  SizedBox(
                    child: Row(
                      children: <Widget>[
                        SizedBox(
                            width: 100,
                            child: Text('이름 : ${peoples[current].name}')),
                        AnimatedContainer(
                          duration: Duration(seconds: 2), // 재생 시간 설정
                          curve: Curves.bounceIn, // 애니메이션 모양 설정
                          color: Colors.amber,
                          child: Text(
                            '키 ${peoples[current].height}',
                            textAlign: TextAlign.center,
                          ),
                          width: 50,
                          height: peoples[current].height,
                        ),
                        AnimatedContainer(
                          duration: Duration(seconds: 2),
                          curve: Curves.easeInCubic,
                          color: weightColor,
                          child: Text(
                            '몸무게 ${peoples[current].weight}',
                            textAlign: TextAlign.center,
                          ),
                          width: 50,
                          height: peoples[current].weight,
                        ),
                        AnimatedContainer(
                          duration: Duration(seconds: 2),
                          curve: Curves.linear,
                          color: Colors.pinkAccent,
                          child: Text(
                            'bmi ${peoples[current].bmi.toString().substring(0, 2)}',
                            textAlign: TextAlign.center,
                          ),
                          width: 50,
                          height: peoples[current].bmi,
                        ),
                      ],
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      crossAxisAlignment: CrossAxisAlignment.end,
                    ),
                    height: 200,
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        if (current < peoples.length - 1) {
                          current++;
                        }
                        _changeWeightColor(peoples[current].weight); // 몸무게에 따른 색상 변경 함수 적용
                      });
                    },
                    child: Text('다음'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        if (current > 0) {
                          current--;
                        }
                        _changeWeightColor(peoples[current].weight); // 몸무게에 따른 색상 변경 함수 적용
                      });
                    },
                    child: Text('이전'),
                  )
                ],
                mainAxisAlignment: MainAxisAlignment.center,
              ),
            ),
          ),
        );
      }
    
      // 몸무게에 따른 색상 변경 함수
      void _changeWeightColor(double weight) {
        if (weight < 40) {
          weightColor = Colors.blueAccent;
        } else if (weight < 60) {
          weightColor = Colors.indigo;
        } else if (weight < 80) {
          weightColor = Colors.orange;
        } else {
          weightColor = Colors.red;
        }
      }
    }

     

    [그림 2] 몸무게에 색상 변경 애니메이션 적용

     

    불투명도 애니메이션

    AnimatedOpacity 위젯을 이용해 서서히 사라지는 효과를 구현할 수 있다.

    import 'package:flutter/material.dart';
    import 'people.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: AnimationApp(),
        );
      }
    }
    
    class AnimationApp extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _AnimationApp();
    }
    
    class _AnimationApp extends State<AnimationApp> {
      double _opacity = 1;
      List<People> peoples = new List.empty(growable: true);
      Color weightColor = Colors.blue;
      int current = 0;
    
      @override
      void initState() {
        peoples.add(People('이일남', 180, 92));
        peoples.add(People('이이남', 170, 75));
        peoples.add(People('이삼남', 165, 59));
        peoples.add(People('이사남', 145, 39));
        peoples.add(People('이오남', 194, 110));
        peoples.add(People('이육남', 155, 60));
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Animation Example'),
          ),
          body: Container(
            child: Center(
              child: Column(
                children: <Widget>[
                  // 불투명도 애니메이션 적용
                  AnimatedOpacity(
                    opacity: _opacity,
                    duration: Duration(seconds: 1),
                    // SizedBox 안에는 Row 이용해 텍스트 위젯 애니메이션을 구현하는 AnimatedContainer를 만든다.
                    child: SizedBox(
                      child: Row(
                        children: <Widget>[
                          SizedBox(
                              width: 100,
                              child: Text('이름 : ${peoples[current].name}')),
                          AnimatedContainer(
                            duration: Duration(seconds: 2), // 재생 시간 설정
                            curve: Curves.bounceIn, // 애니메이션 모양 설정
                            color: Colors.amber,
                            child: Text(
                              '키 ${peoples[current].height}',
                              textAlign: TextAlign.center,
                            ),
                            width: 50,
                            height: peoples[current].height,
                          ),
                          AnimatedContainer(
                            duration: Duration(seconds: 2),
                            curve: Curves.easeInCubic,
                            color: weightColor,
                            child: Text(
                              '몸무게 ${peoples[current].weight}',
                              textAlign: TextAlign.center,
                            ),
                            width: 50,
                            height: peoples[current].weight,
                          ),
                          AnimatedContainer(
                            duration: Duration(seconds: 2),
                            curve: Curves.linear,
                            color: Colors.pinkAccent,
                            child: Text(
                              'bmi ${peoples[current].bmi.toString().substring(0, 2)}',
                              textAlign: TextAlign.center,
                            ),
                            width: 50,
                            height: peoples[current].bmi,
                          ),
                        ],
                        mainAxisAlignment: MainAxisAlignment.spaceAround,
                        crossAxisAlignment: CrossAxisAlignment.end,
                      ),
                      height: 200,
                    ),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        if (current < peoples.length - 1) {
                          current++;
                        }
                        _changeWeightColor(peoples[current].weight);
                      });
                    },
                    child: Text('다음'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        if (current > 0) {
                          current--;
                        }
                        _changeWeightColor(peoples[current].weight);
                      });
                    },
                    child: Text('이전'),
                  ),
                  // AnimatedOpacity 불투명도를 이용한 사라지기 버튼
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        _opacity == 1 ? _opacity = 0 : _opacity = 1;
                      });
                    },
                    child: Text('사라지기'),
                  )
                ],
                mainAxisAlignment: MainAxisAlignment.center,
              ),
            ),
          ),
        );
      }
    
      // 몸무게에 따라 색상 바꿔주는 함수
      void _changeWeightColor(double weight) {
        if (weight < 40) {
          weightColor = Colors.blueAccent;
        } else if (weight < 60) {
          weightColor = Colors.indigo;
        } else if (weight < 80) {
          weightColor = Colors.orange;
        } else {
          weightColor = Colors.red;
        }
      }
    }

     

    [그림 3] AnimatedOpacity 불투명도 애니메이션 적용

     


     

    나만의 인트로 화면 만들기

    페이지 이동 애니메이션 / 애니메이션 세밀하게 조정

    페이지 이동 애니메이션은 Hero 위젯을 이용한다.

    // main.dart
    import 'package:flutter/material.dart';
    import 'people.dart';
    import 'secondPage.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: AnimationApp(),
        );
      }
    }
    
    class AnimationApp extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _AnimationApp();
    }
    
    class _AnimationApp extends State<AnimationApp> {
      double _opacity = 1;
      List<People> peoples = new List.empty(growable: true);
      Color weightColor = Colors.blue;
      int current = 0;
    
      @override
      void initState() {
        peoples.add(People('이일남', 180, 92));
        peoples.add(People('이이남', 170, 75));
        peoples.add(People('이삼남', 165, 59));
        peoples.add(People('이사남', 145, 39));
        peoples.add(People('이오남', 194, 110));
        peoples.add(People('이육남', 155, 60));
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Animation Example'),
          ),
          body: Container(
            child: Center(
              child: Column(
                children: <Widget>[
                  // 불투명도 애니메이션 적용
                  AnimatedOpacity(
                    opacity: _opacity,
                    duration: Duration(seconds: 1),
                    // SizedBox 안에는 Row 이용해 텍스트 위젯 애니메이션을 구현하는 AnimatedContainer를 만든다.
                    child: SizedBox(
                      child: Row(
                        children: <Widget>[
                          SizedBox(
                              width: 100,
                              child: Text('이름 : ${peoples[current].name}')),
                          AnimatedContainer(
                            duration: Duration(seconds: 2), // 재생 시간 설정
                            curve: Curves.bounceIn, // 애니메이션 모양 설정
                            color: Colors.amber,
                            child: Text(
                              '키 ${peoples[current].height}',
                              textAlign: TextAlign.center,
                            ),
                            width: 50,
                            height: peoples[current].height,
                          ),
                          AnimatedContainer(
                            duration: Duration(seconds: 2),
                            curve: Curves.easeInCubic,
                            color: weightColor,
                            child: Text(
                              '몸무게 ${peoples[current].weight}',
                              textAlign: TextAlign.center,
                            ),
                            width: 50,
                            height: peoples[current].weight,
                          ),
                          AnimatedContainer(
                            duration: Duration(seconds: 2),
                            curve: Curves.linear,
                            color: Colors.pinkAccent,
                            child: Text(
                              'bmi ${peoples[current].bmi.toString().substring(0, 2)}',
                              textAlign: TextAlign.center,
                            ),
                            width: 50,
                            height: peoples[current].bmi,
                          ),
                        ],
                        mainAxisAlignment: MainAxisAlignment.spaceAround,
                        crossAxisAlignment: CrossAxisAlignment.end,
                      ),
                      height: 200,
                    ),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        if (current < peoples.length - 1) {
                          current++;
                        }
                        _changeWeightColor(peoples[current].weight);
                      });
                    },
                    child: Text('다음'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        if (current > 0) {
                          current--;
                        }
                        _changeWeightColor(peoples[current].weight);
                      });
                    },
                    child: Text('이전'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        _opacity == 1 ? _opacity = 0 : _opacity = 1;
                      });
                    },
                    child: Text('사라지기'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      Navigator.of(context).push(
                          MaterialPageRoute(builder: (context) => SecondPage()));
                    },
                    child: SizedBox(
                      width: 200,
                      child: Row(
                        children: <Widget>[
                          Hero(tag: 'detail', child: Icon(Icons.cake)),
                          Text('이동하기')
                        ],
                      ),
                    ),
                  ),
                ],
                mainAxisAlignment: MainAxisAlignment.center,
              ),
            ),
          ),
        );
      }
    
      // 몸무게에 따라 색상 바꿔주는 함수
      void _changeWeightColor(double weight) {
        if (weight < 40) {
          weightColor = Colors.blueAccent;
        } else if (weight < 60) {
          weightColor = Colors.indigo;
        } else if (weight < 80) {
          weightColor = Colors.orange;
        } else {
          weightColor = Colors.red;
        }
      }
    }

     

    // secondPage.dart
    import 'package:flutter/material.dart';
    import 'dart:math';
    
    class SecondPage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _SecondPage();
    }
    
    class _SecondPage extends State<SecondPage>
        with SingleTickerProviderStateMixin {
      AnimationController? _animationController;
      Animation? _rotateAnimation;
      Animation? _scaleAnimation;
      Animation? _transAnimation;
    
      @override
      void initState() {
        super.initState();
        _animationController =
            AnimationController(duration: Duration(seconds: 5), vsync: this);
        // Tween을 사용해 애니메이션 구성
        _rotateAnimation =
            Tween<double>(begin: 0, end: pi * 10).animate(_animationController!);
        _scaleAnimation =
            Tween<double>(begin: 1, end: 0).animate(_animationController!);
        _transAnimation = Tween<Offset>(begin: Offset(0, 0), end: Offset(200, 200))
            .animate(_animationController!);
      }
    
      @override
      // 애니메이션을 종료해 주어야 한다. 그렇지 않으면 화면을 그리려고 하는 대상이 없어서 오류가 발생한다.
      void dispose() {
        _animationController!.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Animation Example2'),
          ),
          body: Container(
            child: Center(
              child: Column(
                children: <Widget>[
                  // AnimatedBuilder는 애니메이션을 정의한 대로 화면에 그려준다.
                  AnimatedBuilder(
                    animation: _rotateAnimation!,
                    builder: (context, widget) {
                      return Transform.translate( // 위젯의 방향
                        offset: _transAnimation!.value,
                        child: Transform.rotate( // 회전
                            angle: _rotateAnimation!.value,
                            child: Transform.scale( // 크기
                              scale: _scaleAnimation!.value,
                              child: widget,
                            )),
                      );
                    },
                    child: Hero(
                        tag: 'detail',
                        child: Icon(
                          Icons.cake,
                          size: 300,
                        )),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      _animationController!.forward();
                    },
                    child: Text('로테이션 시작하기'),
                  ),
                ],
                mainAxisAlignment: MainAxisAlignment.center,
              ),
            ),
          ),
        );
      }
    }

     

    [그림 4] 로테이션 / 페이지 이동 애니메이션

     

    나만의 인트로 화면 만들기

    인트로 화면에 필요한 이미지를 repo/images 폴더에 넣고 pubspec.yaml을 수정한다.

    // pubspec.yaml
    (...생략...)
    flutter:
      uses-material-design: true
      assets:
        - repo/images/

     

    // main.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_animation_example/intro.dart';
    import 'people.dart';
    import 'secondPage.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: IntroPage(), // 인트로 페이지 열기
        );
      }
    }
    
    class AnimationApp extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _AnimationApp();
    }
    
    class _AnimationApp extends State<AnimationApp> {
      double _opacity = 1;
      List<People> peoples = new List.empty(growable: true);
      Color weightColor = Colors.blue;
      int current = 0;
    
      @override
      void initState() {
        peoples.add(People('이일남', 180, 92));
        peoples.add(People('이이남', 170, 75));
        peoples.add(People('이삼남', 165, 59));
        peoples.add(People('이사남', 145, 39));
        peoples.add(People('이오남', 194, 110));
        peoples.add(People('이육남', 155, 60));
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Animation Example'),
          ),
          body: Container(
            child: Center(
              child: Column(
                children: <Widget>[
                  // 불투명도 애니메이션 적용
                  AnimatedOpacity(
                    opacity: _opacity,
                    duration: Duration(seconds: 1),
                    // SizedBox 안에는 Row 이용해 텍스트 위젯 애니메이션을 구현하는 AnimatedContainer를 만든다.
                    child: SizedBox(
                      child: Row(
                        children: <Widget>[
                          SizedBox(
                              width: 100,
                              child: Text('이름 : ${peoples[current].name}')),
                          AnimatedContainer(
                            duration: Duration(seconds: 2), // 재생 시간 설정
                            curve: Curves.bounceIn, // 애니메이션 모양 설정
                            color: Colors.amber,
                            child: Text(
                              '키 ${peoples[current].height}',
                              textAlign: TextAlign.center,
                            ),
                            width: 50,
                            height: peoples[current].height,
                          ),
                          AnimatedContainer(
                            duration: Duration(seconds: 2),
                            curve: Curves.easeInCubic,
                            color: weightColor,
                            child: Text(
                              '몸무게 ${peoples[current].weight}',
                              textAlign: TextAlign.center,
                            ),
                            width: 50,
                            height: peoples[current].weight,
                          ),
                          AnimatedContainer(
                            duration: Duration(seconds: 2),
                            curve: Curves.linear,
                            color: Colors.pinkAccent,
                            child: Text(
                              'bmi ${peoples[current].bmi.toString().substring(0, 2)}',
                              textAlign: TextAlign.center,
                            ),
                            width: 50,
                            height: peoples[current].bmi,
                          ),
                        ],
                        mainAxisAlignment: MainAxisAlignment.spaceAround,
                        crossAxisAlignment: CrossAxisAlignment.end,
                      ),
                      height: 200,
                    ),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        if (current < peoples.length - 1) {
                          current++;
                        }
                        _changeWeightColor(peoples[current].weight);
                      });
                    },
                    child: Text('다음'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        if (current > 0) {
                          current--;
                        }
                        _changeWeightColor(peoples[current].weight);
                      });
                    },
                    child: Text('이전'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        _opacity == 1 ? _opacity = 0 : _opacity = 1;
                      });
                    },
                    child: Text('사라지기'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      Navigator.of(context).push(
                          MaterialPageRoute(builder: (context) => SecondPage()));
                    },
                    child: SizedBox(
                      width: 200,
                      child: Row(
                        children: <Widget>[
                          Hero(tag: 'detail', child: Icon(Icons.cake)),
                          Text('이동하기')
                        ],
                      ),
                    ),
                  ),
                ],
                mainAxisAlignment: MainAxisAlignment.center,
              ),
            ),
          ),
        );
      }
    
      // 몸무게에 따라 색상 바꿔주는 함수
      void _changeWeightColor(double weight) {
        if (weight < 40) {
          weightColor = Colors.blueAccent;
        } else if (weight < 60) {
          weightColor = Colors.indigo;
        } else if (weight < 80) {
          weightColor = Colors.orange;
        } else {
          weightColor = Colors.red;
        }
      }
    }

     

    // intro.dart
    import 'package:flutter/material.dart';
    import 'saturnLoading.dart';
    import 'dart:async';
    import 'main.dart';
    
    class IntroPage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _IntroPage();
    }
    
    class _IntroPage extends State<IntroPage> {
      @override
      void initState() {
        super.initState();
        loadData();
      }
    
      Future<Timer> loadData() async {
        return Timer(Duration(seconds: 5), onDoneLoading);
      }
    
      onDoneLoading() async {
        Navigator.of(context).pushReplacement(
            MaterialPageRoute(builder: (context) => AnimationApp()));
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Container(
            child: Center(
              child: Column(
                children: <Widget>[
                  Text('애니메이션 앱'),
                  SizedBox(
                    height: 20,
                  ),
                  SaturnLoading() // 애니메이션 불러오기
                ],
                mainAxisAlignment: MainAxisAlignment.center,
              ),
            ),
          ),
        );
      }
    }

     

    // saturnLoading.dart
    import 'package:flutter/material.dart';
    import 'dart:math';
    
    class SaturnLoading extends StatefulWidget {
      _SaturnLoading _saturnLoading = _SaturnLoading();
    
      @override
      State<StatefulWidget> createState() => _SaturnLoading();
    
      void start() {
        _saturnLoading.start();
      }
    
      void stop() {
        _saturnLoading.stop();
      }
    }
    
    class _SaturnLoading extends State<SaturnLoading>
        with SingleTickerProviderStateMixin {
      AnimationController? _animationController;
      Animation? _animation;
    
      @override
      void initState() {
        super.initState();
        _animationController = AnimationController(
            vsync: this,
            duration: Duration(seconds: 3)); // 3초 동안 동작하는 애니메이션 컨트롤러 정의
        _animation = Tween<double>(begin: 0, end: pi * 2)
            .animate(_animationController!); // 애니메이션 시작점과 끝점 정의
        _animationController!.repeat();
      }
    
      @override
      void dispose() {
        _animationController!.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return AnimatedBuilder(
          animation: _animationController!,
          builder: (context, child) {
            return SizedBox(
              width: 100,
              height: 100,
              // 스택 자료구조의 특성상 먼저 들어가면 밑에 쌓이기 때문에 원, 태양, 토성 순으로 배치한다.
              child: Stack(
                children: <Widget>[
                  Image.asset(
                    'repo/images/circle.png',
                    width: 100,
                    height: 100,
                  ),
                  Center(
                    child:
                        Image.asset('repo/images/sunny.png', width: 30, height: 30),
                  ),
                  Padding(
                    padding: EdgeInsets.all(5),
                    child: Transform.rotate(
                      angle: _animation!.value,
                      origin: Offset(35, 35), // 회전의 기준점 지정하기
                      child: Image.asset(
                        'repo/images/saturn.png',
                        width: 20,
                        height: 20,
                      ),
                    ),
                  )
                ],
              ),
            );
          },
        );
      }
      // 애니메이션을 시작하고 멈출 수 있게 한다.
      void stop() {
        _animationController!.stop(canceled: true);
      }
    
      void start() {
        _animationController!.repeat();
      }
    }

     

    [그림 5] 인트로 화면 만들기

     


     

    스크롤 시 역동적인 앱바 만들기

    화면 스크롤할 때 앱바의 크기를 변경하는 애니메이션을 구현한다.

    // main.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_animation_example/intro.dart';
    import 'people.dart';
    import 'secondPage.dart';
    import 'sliverPage.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: IntroPage(), // 인트로 페이지 열기
        );
      }
    }
    
    class AnimationApp extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _AnimationApp();
    }
    
    class _AnimationApp extends State<AnimationApp> {
      double _opacity = 1;
      List<People> peoples = new List.empty(growable: true);
      Color weightColor = Colors.blue;
      int current = 0;
    
      @override
      void initState() {
        peoples.add(People('이일남', 180, 92));
        peoples.add(People('이이남', 170, 75));
        peoples.add(People('이삼남', 165, 59));
        peoples.add(People('이사남', 145, 39));
        peoples.add(People('이오남', 194, 110));
        peoples.add(People('이육남', 155, 60));
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Animation Example'),
          ),
          body: Container(
            child: Center(
              child: Column(
                children: <Widget>[
                  // 불투명도 애니메이션 적용
                  AnimatedOpacity(
                    opacity: _opacity,
                    duration: Duration(seconds: 1),
                    // SizedBox 안에는 Row 이용해 텍스트 위젯 애니메이션을 구현하는 AnimatedContainer를 만든다.
                    child: SizedBox(
                      child: Row(
                        children: <Widget>[
                          SizedBox(
                              width: 100,
                              child: Text('이름 : ${peoples[current].name}')),
                          AnimatedContainer(
                            duration: Duration(seconds: 2), // 재생 시간 설정
                            curve: Curves.bounceIn, // 애니메이션 모양 설정
                            color: Colors.amber,
                            child: Text(
                              '키 ${peoples[current].height}',
                              textAlign: TextAlign.center,
                            ),
                            width: 50,
                            height: peoples[current].height,
                          ),
                          AnimatedContainer(
                            duration: Duration(seconds: 2),
                            curve: Curves.easeInCubic,
                            color: weightColor,
                            child: Text(
                              '몸무게 ${peoples[current].weight}',
                              textAlign: TextAlign.center,
                            ),
                            width: 50,
                            height: peoples[current].weight,
                          ),
                          AnimatedContainer(
                            duration: Duration(seconds: 2),
                            curve: Curves.linear,
                            color: Colors.pinkAccent,
                            child: Text(
                              'bmi ${peoples[current].bmi.toString().substring(0, 2)}',
                              textAlign: TextAlign.center,
                            ),
                            width: 50,
                            height: peoples[current].bmi,
                          ),
                        ],
                        mainAxisAlignment: MainAxisAlignment.spaceAround,
                        crossAxisAlignment: CrossAxisAlignment.end,
                      ),
                      height: 200,
                    ),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        if (current < peoples.length - 1) {
                          current++;
                        }
                        _changeWeightColor(peoples[current].weight);
                      });
                    },
                    child: Text('다음'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        if (current > 0) {
                          current--;
                        }
                        _changeWeightColor(peoples[current].weight);
                      });
                    },
                    child: Text('이전'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        _opacity == 1 ? _opacity = 0 : _opacity = 1;
                      });
                    },
                    child: Text('사라지기'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      Navigator.of(context).push(
                          MaterialPageRoute(builder: (context) => SecondPage()));
                    },
                    child: SizedBox(
                      width: 200,
                      child: Row(
                        children: <Widget>[
                          Hero(tag: 'detail', child: Icon(Icons.cake)),
                          Text('이동하기')
                        ],
                      ),
                    ),
                  ),
                  // SliverPage 이동 버튼
                  ElevatedButton(
                    onPressed: () {
                      Navigator.of(context).push(
                          MaterialPageRoute(builder: (context) => SliverPage()));
                    },
                    child: Text('페이지 이동'),
                  ),
                ],
                mainAxisAlignment: MainAxisAlignment.center,
              ),
            ),
          ),
        );
      }
    
      // 몸무게에 따라 색상 바꿔주는 함수
      void _changeWeightColor(double weight) {
        if (weight < 40) {
          weightColor = Colors.blueAccent;
        } else if (weight < 60) {
          weightColor = Colors.indigo;
        } else if (weight < 80) {
          weightColor = Colors.orange;
        } else {
          weightColor = Colors.red;
        }
      }
    }

     

    // silverPage.dart
    import 'package:flutter/material.dart';
    import 'dart:math' as math;
    
    class SliverPage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => _SliverPage();
    }
    
    class _SliverPage extends State<SliverPage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          // CustomScrollView는 슬리버를 사용해 사용자 정의 스크롤 효과를 만드는 위젯
          body: CustomScrollView(
            // 슬리버가 들어가는 위젯을 이용할 때는 컨테이너나 텍스트 등 기본적인 위젯을 바로 사용할 수 없고 slivers 인자로 위젯을 묶어 주어야 한다.
            slivers: <Widget>[
              // SliverAppBar는 CustomScrollView 위젯 안에서만 사용할 수 있다.
              SliverAppBar(
                expandedHeight: 150.0, // 앱바의 최대 높이 설정
                //유연하게 조절되는 공간
                flexibleSpace: FlexibleSpaceBar(
                  title: Text('Sliver Example'),
                  background: Image.asset('repo/images/sunny.png'),
                ),
                backgroundColor: Colors.deepOrangeAccent,
                pinned: true,
              ),
              SliverPersistentHeader(
                delegate: _HeaderDelegate(
                    minHeight: 50,
                    maxHeight: 150,
                    child: Container(
                      color: Colors.blue,
                      child: Center(
                        child: Column(
                          children: <Widget>[
                            Text(
                              'list 숫자',
                              style: TextStyle(fontSize: 30),
                            ),
                          ],
                          mainAxisAlignment: MainAxisAlignment.center,
                        ),
                      ),
                    )),
                pinned: true, // 앱바가 사라지지 않고 최소 크기로 고정되게 한다.
              ),
    
              SliverList(
                  //     delegate: SliverChildListDelegate([
                  //   customCard('1'),
                  //   customCard('2'),
                  //   customCard('3'),
                  //   customCard('4'),
                  // ])),
                  // 리스트를 빌더 형태로 생성
                  delegate: SliverChildBuilderDelegate((context, index) {
                return Container(
                  child: customCard('list count : $index'),
                );
              }, childCount: 10)),
    
              SliverPersistentHeader(
                delegate: _HeaderDelegate(
                    minHeight: 50,
                    maxHeight: 150,
                    child: Container(
                      color: Colors.blue,
                      child: Center(
                        child: Column(
                          children: <Widget>[
                            Text(
                              '그리드 숫자',
                              style: TextStyle(fontSize: 30),
                            ),
                          ],
                          mainAxisAlignment: MainAxisAlignment.center,
                        ),
                      ),
                    )),
                pinned: true,
              ),
              SliverGrid(
                  // delegate: SliverChildListDelegate([
                  //   customCard('1'),
                  //   customCard('2'),
                  //   customCard('3'),
                  //   customCard('4'),
                  // ]),
                  // 리스트를 빌더 형태로 생성
                  delegate: SliverChildBuilderDelegate((context, index) {
                    return Container(
                      child: customCard('list count : $index'),
                    );
                  }, childCount: 10),
                  gridDelegate:
                      SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2)),
            ],
          ),
        );
      }
    
      // 스크롤되는 위젯 넣기
      Widget customCard(String text) {
        return Card(
          child: Container(
            height: 120,
            child: Center(
                child: Text(
              text,
              style: TextStyle(fontSize: 40),
            )),
          ),
        );
      }
    }
    
    // 4개의 함수를 재정의 함
    class _HeaderDelegate extends SliverPersistentHeaderDelegate {
      final double minHeight;
      final double maxHeight;
      final Widget child;
    
      _HeaderDelegate({
        required this.minHeight,
        required this.maxHeight,
        required this.child,
      });
    
      // 머리말을 만들 때 사용할 위젯을 배치
      @override
      Widget build(
          BuildContext context, double shrinkOffset, bool overlapsContent) {
        return SizedBox.expand(child: child);
      }
    
      // 해당 위젯의 최대 높이 설정
      @override
      double get maxExtent => math.max(maxHeight, minHeight);
    
      // 해당 위젯의 최소 높이 설정
      @override
      double get minExtent => minHeight;
    
      // 위젯을 계속 그릴 것인지 정함. 만약 maxHeight나 minHeight, child가 달라진다면 true를 반환해 계속 다시 그릴수 있게 설정한다.
      @override
      bool shouldRebuild(_HeaderDelegate oldDelegate) {
        return maxHeight != oldDelegate.maxHeight ||
            minHeight != oldDelegate.minHeight ||
            child != oldDelegate.child;
      }
    }

     

    [그림 6] 스크롤시 역동적인 앱바

    728x90
    반응형
    댓글