앱 App/플러터 Flutter

애니메이션 활용하기 - (3) 스크롤 시 역동적인 앱바 만들기

하나비 HANABI 2021. 11. 7. 17:56

애니메이션 활용하기 - (2) 와 이어진다

앱바나 리스트를 역동적으로 만들고 싶을 때는 sliver라는 위젯을 이용

 

실습1. 슬리버를 사용한 스크롤뷰 만들기 (SliverAppBar, SliverList, SliverGride 사용)

 

https://www.youtube.com/watch?v=mSc7qFzxHDw (SliverAppBar)

 

스크롤을 올리면 상단의 앱바가 점점 작아지다가 사라진다

 

 

 

1단계.

Lib폴더에 sliverPage.dart 파일 생성하여 기본골격 잡기

먼저 CustomScrollView의slivers에 SliverAppBar를 사용해 확장 앱바를 만든다.

SliverAppBar는 스크롤에 따라 높이가 달라기거나 다른 위젯 위에 표시되도록 스크롤뷰에 통합된다.

expandheight를 통해 앱바이 최대 높이 설정하고

flexibleSpace를 이용해 flexibleSpaceBar로 제목과 이미지 설정

 

 

2단계.

main.dart 파일에서 _AnimationApp 클래스의 '이동하기'버튼 아래에

방금 만든 SliverPage로 이동하는 버튼 추가

현재까지 만든 어플의 모습

 

 

3단계.

아직은 스크롤 할만한 위젯이 없으므로

텍스트를 전달하면 카드위젯을 만들어주는 customCard() 함수를 _SliverPage 클래스에 추가

 

 

4단계.

CustomScrollView의 slivers에 SliverList와 SliverGrid 위젯 추가.

SliverChildListDelegate를 통해 배열 안에 원하는 위젯을 넣을 수 있도록 한다

 

 

5단계.

위젯을 넣을 곳에 3단계에서 만든 customCard() 호출하여 카드위젯 배치

 

 

 

 

실습2. 위젯을 구분하는 머리말 추가하기

스크롤을 올리면 앱바와 머릿말이 작아진다

 

 

1단계.

sliverPage 파일에 SliverPersistemtHeaderDelegate를 상속받는 _HeaderDelegate 클래스 생성

Required에 오류가 뜰 때는

Pubspec.yaml 파일의 sdk를 2.7.0에서 2.12.0으로 고쳐준다

 

 

 

2단계.SliverList와 SliverGride 위에 SliverPersistemHeader위젯 추가하고슬리버 앱바에 pinned 옵션을 true로 하여 앱바가 사라지지 않고 최소 크기로 고정되도록 설정

 

보너스단계.

리스트를 빌더 형태로 생성하려면

SliverGrid 밑에 SliverChildBuilderDelegate 추가

list count : 0~9까지의 위젯이 추가로 생성됨

 

끝!

 

 

<main.dart>

import 'package:flutter/material.dart';
import 'people.dart';
import 'secondPage.dart';
import 'intro.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('메리', 162, 55));
    peoples.add(People('존', 177, 75));
    peoples.add(People('바트', 130, 40));
    peoples.add(People('콘', 194, 140));
    peoples.add(People('디디', 100, 80));
    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),
                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('이동하기')
                    ],
                  ),),),
              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;
    }
  }
}

 

<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));
  }
}

 

 

<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);
      _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 contexxt) {
      return Scaffold(
        appBar: AppBar(title: Text('Animation Example2'),),
        body: Container(
          child: Center(
            child: Column(
              children: <Widget>[
                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,
            ),),),
      );
    }
}

 

 

<saturnLoading.dart>

import 'package:flutter/material.dart';
import 'dart:math';

class SaturnLoading extends StatefulWidget {
  _SaturnLoading _saturnLoading = _SaturnLoading();

  void start() {
    _saturnLoading.start();
  }

  void stop() {
    _saturnLoading.stop();
  }

  @override
  State<StatefulWidget> createState() => _saturnLoading;
}

class _SaturnLoading extends State<SaturnLoading>
    with SingleTickerProviderStateMixin {
  AnimationController? _animationController;
  Animation? _animation;

  void stop() {
    _animationController!.stop(canceled: true);
  }

  void start() {
    _animationController!.repeat();
  }

  @override
  void initState() {
    super.initState();
    _animationController =
        AnimationController(vsync: this, duration: Duration(seconds: 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), // 태양으로부터 35픽셀 떨어져 회전
                  child: Image.asset(
                    'repo/images/saturn.png',
                    width: 20,
                    height: 20,
                  ),),)
            ],),);
      },);
  }
}

 

 

<intro.dart>

import 'package:flutter/material.dart';
import 'package:animation_example/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,
          ),),),);
  }}

 

<sliverPage.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(
      body: CustomScrollView(
        slivers: <Widget>[
          SliverAppBar(
            pinned: true,
            // 앱바의 높이 설정
            expandedHeight: 150.0,
            // SliverAppBar 공간에 어떤 위젯을 만들지 설정
            flexibleSpace: FlexibleSpaceBar(
              title: Text('Sliver Example'),
              background: Image.asset('repo/images/sunny.png'),
            ),
            backgroundColor: Colors.deepOrangeAccent,
          ),
          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'),
              ])),
          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'),
            ]),
            gridDelegate:
                SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2)),
          SliverList(
            delegate: SliverChildBuilderDelegate((context, index) {
              return Container(
                child: customCard('list count : $index'),
              );
            }, childCount: 10)
          ),
        ],),);
  }

  Widget customCard(String text) {
    return Card(
      child: Container(
        height: 120,
        child: Center(
          child: Text(
            text,
            style: TextStyle(fontSize: 40),
          )),
      ),
    );
  }
}



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;
            // 최소 높이 설정
  @override
  bool shouldRebuild(_HeaderDelegate oldDelegate) {
    return maxHeight != oldDelegate.maxHeight ||
          minHeight != oldDelegate.minHeight ||
          child != oldDelegate.child;
          // 이 셋 중 하나가 달라지면 true를 반환해 계속 다시 그릴 수 있게
  }
}