티스토리 뷰

11-1

 

애니메이션 

위젯이 처음 위치에서 마지막 위치까지 어떻게 이동할 것인지를 정의하는 것

 

AnimatedContainer 위젯 

기존의 컨테이너 위젯과 똑같지만 애니메이션의 모양을 결정하는 curve와 재생 시간을 결정하는 duration 변수를 추가로 넣을 수 있다. 

플러터 에니메이션은 선형보간법(두개의 점이 주어졌을 때에 그 두점을 지나는 함수를 직선의 방정식으로 나타내는 것)을 활용해 다양한 모양을 만드는데, 이러한 수학적인 부분을 AnimatedContainer가 손쉽게 처리해 준다. 

 

animation example 프로젝트 생성

 

1. 그래프 애니메이션

class _AnimationApp extends State<AnimationApp>{

List<People> peoples = new List.empty(growable: true); // 빈 리스트 생성
int current = 0;
Color weightColor = Colors.blue;
double _opacity = 1;

@override
void initState() { // data 생성은 initsate에서
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('Aimation Example'),
),
body: Container(
child: Center(
child: Column( //Column을 이용해 위젯을 세로로 만듦 -> sizedBox ElevatedButton 생성
children: <Widget> [
SizedBox(
child: Row( // Row을 통해서 가로로 텍스트 위젯과 애니메이션을 구현하는 위젯을 배치시킨다.
children: <Widget>[
SizedBox(width: 100, child: Text('이름: ${peoples[current].name}')),
AnimatedContainer( //current 값이 변경되면 height weight bmi 수치가 변경하는 애니메이션
duration: Duration(seconds: 2), // 애니메이션 모양과 재생 시간설정, 2초 동안 애니메이션 재생
// second 대신에 dyas,hours, milliseconds등 사용 가능
curve: Curves.bounceIn, // Curve 클래스에서 애니메이션의 모양을 지정한다.
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,
),
),
),
);
}
}

 

 

 

2. 색상 변경 애니메이션 

 


class _AnimationApp extends State<AnimationApp>{

Color weightColor = Colors.blue;

 

...

 

AnimatedContainer(
duration: Duration(seconds: 2),
curve: Curves.easeInCubic,
color: weightColor, // 애니메이션 컬러를 weightColor 변수로 변경 
child: Text(
'몸무게 ${peoples[current].weight}',
textAlign: TextAlign.center,
),
width: 50,
height: peoples[current].weight,
),

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('이전'),
),

 

...

}

 

80kg 이상일 경우 그래프 색상 
60키로 미만일 경우의 그래프 색상 변경 애니메이션 

 

 

3. 불투명도 애니메이션 

위젯이 서서히 사라지는 효과로 이러한 애니메이션은 AnimatedOpacity 위젯을 이용해 구현한다. AnimatedOpacity 위젯은 불투명도를 나타내는 Opacity라는 옵션을 조절해 위젯을 투명하게, 또는 불투명하게 표현할 수 있다. 

 

class _AnimationApp extends State<AnimationApp>{

double _opacity = 1; // 불투명도를 나타낼 변수

 

...

 

AnimatedOpacity( //불투명도를 조절하는 위젯 추가
opacity: _opacity, duration: Duration(seconds:1),
child: SizedBox(
child: Row( // Row을 통해서 가로로 텍스트 위젯과 애니메이션을 구현하는 위젯을 배치시킨다.
children: <Widget>[
SizedBox(width: 100, child: Text('이름: ${peoples[current].name}')),
AnimatedContainer()

...

)

 

... 

 

ElevatedButton( // 불투명도 조절하는 버튼 
onPressed: (){
setState((){
_opacity == 1 ? _opacity =0: _opacity =1;

// 불투명도가 0이면 사라지고 1이면 나타난다. 
});
},
child: Text('사라지기'),
),

}

불투명도 0 으로 사라지게 됨 

 

11-2 나만의 인트로 화면 만들기 

1. 페이지 이동 애니메이션 

 

페이지를 이동할 때 애니메이션을 적용하려면 Hero 위젯을 사용한다. 이 위젯은 페이지 간의 이미지를 자연스럽게 애니메이션으로 연결해준다. 

 

...

Widget build(BuildContext context){

... 

ElevatedButton( // 해당 버튼은 아이콘 텍스트로 이루어진다.
onPressed: (){
Navigator.of(context).push(MaterialPageRoute(builder:
(context) => SecondPage()
));
},
child: SizedBox(
width: 200,
child: Row(
children: <Widget>[ //tag detail로 설정한 후에 아이콘을 넣는다.
// 이렇게 하면 같은 태그의 다른 Hero 위젯과 연결된다.
Hero(tag: 'detail', child: Icon(Icons.cake)), //아이콘은 Hero 위젯으로 감싼다.
Text('이동하기')
],
),
),
),

}

 

lib 폴더에 secondPage.dart 파일 새로 생성

 

class _secondPage extends State<SecondPage>{

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Animation Example2'),
),
body: Container(
child: Center(
child: Column(
children: <Widget>[
//build 함수에서 Hero 위젯으로 감싼 아이콘을 화면 가운데에 표시한다.
//이때 tag 옵션을 detail로 지정해서 앞에서 정의한 Hero 위젯과 연결한다.
Hero(tag: 'detail', child: Icon(Icons.cake, size: 150),),
],
mainAxisAlignment: MainAxisAlignment.center,
),
),
)

);
}
}

 

이동하기 버튼 새로 추가 
second 페이지로 이동 

 

 

2. 애니메이션을 세밀하게 조정하기

 

AnimatedBuilder 위젯은 AnimatedContainer 보다 좀 더 세밀하게 애니메이션을 조절할 수 있다. 

 

secondPage.dart 파일에 애니메이션 컨트롤러를 만들어야한다. 

애니메이션 컨트롤러를 사용하려면 SingleTickerProviderStateMixin 클래스를 추가로 상속 받아야 한다. 

with SingleTickerProviderStateMixin => 해당 클래스 추가 상속

 

class _SecondPage extends State<SecondPage> with SingleTickerProviderStateMixin {
AnimationController? _animationController; // 프레임마다 새로운 값을 생성하는 특별한 애니메이션 클래스
Animation? _rotateAnimation;
Animation? _scaleAnimation;
Animation? _transAnimation;

@override
void initState() { // initState 함수에서 애니메이션 컨트롤러 객체와 애니메이션 객체를 초기화 한다. 
super.initState();
_animationController =
AnimationController(duration: Duration(seconds: 5), vsync: this);

//재생시간을 나타내는 duration 과 애니메이션을 표현할 대상을 나타내는 vsync 인수를 전달한다. 위 코드는 5초동안 현재 페이지(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!);

 

// 각 애니메이션을 Tween 클래스를 사용해 초기화 한다. 기본적으로 애니메이션 컨트롤러가 생성해주는 숫자의 범위는 0.0 ~1.0 으로 다른 범위나 데이터의 유형이
//필요하다면 Tween을 사용해 애니메이션을 구성할 수 있다.
//Twween은 시작점을 나타내는 begin 인자와 끝점을 나타내는 end 인자만 있으면 되는 stateless 객체이다. 이 객체를 애니메이션에 사용하려면
//애니메이션 컨트롤러 객체를 전달해 animate() 함수를 호출해야한다.

}

@override
void dispose() {

// 화면이 종료될 때는 호출되는 dispose() 함수에서 _animationController.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(
// AnimateBuilder 를 사용해 앞에서 정의한 애니메이션 3개를 표시
// AnimateVBuilder는 다른 빌더처럼 애니메이션을 정의한 대로 화면에 그린다.
animation: _rotateAnimation!,
builder: (context, widget) {
return Transform.translate( // Transform translate() 함수는 위젯의 방향
offset: _transAnimation!.value, //offset에 지정한 방향으로 이동하면서
child: Transform.rotate( // Transform rotate() 함수는 회전
angle: _rotateAnimation!.value, //회전하고
child: Transform.scale( // Transform scale() 함수는 크기를 조절
scale: _scaleAnimation!.value, //점점 작아지는 애니메이션 구현
child: widget,
)),
);
},

//build 함수에서 Hero 위젯으로 감싼 아이콘을 화면 가운데에 표시한다.
//이때 tag 옵션을 detail로 지정해서 앞에서 정의한 Hero 위젯과 연결한다.
child: Hero(tag: 'detail', child: Icon(Icons.cake, size: 150),),
),
ElevatedButton(
onPressed: (){
_animationController!.forward();
},
child: Text('로테이션 시작하기'),
),
],
mainAxisAlignment: MainAxisAlignment.center,
),
),
)
);
}

}

 

로테이션 버튼 추가 
아이콘이 점점 작아지면서 회전한다. 

즉, Hero를 사용하면 페이지 연결을 자연스럽게 표현할 수 있고, AnimatedBuilder와 Tween을 이용하면 원하는 동작으로 애니메이션을 만들 수 있다.

 

3. 나만의 인트로 화면 만들기 

 

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;

@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(// 해당 함수에서는 origin Offset을 지정하는 것을 볼 수 있다.
angle: _animation!.value,
origin: Offset(35, 35), // origin은 회전의 기준점을 의미하며, offset을 통해서 지정
child: Image.asset(
'repo/images/saturn.png',
width: 20,
height: 20,
// 토성 이미지의 크기가 가로세로 20픽셀이므로, 이미지의 중심은 (10,10)이 된다. 그런데 패딩 값을 5
//설정했으므로 토성의 중심부는 (15,15)가 된다. 여기서 가로세로 35픽셀만큼 오프셋한 곳을 기준으로 삼으면
//x 50, y 50이 되고 토성은 태양으로부터 35픽셀 떨어져서 회전한다.
),),)
],),);
},);
}

// start stop 함수를 만들어서 애니메이션을 시작하고 멈출 수 있게 한다.
void stop() {
_animationController!.stop(canceled: true);
}

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

@override
void initState() { // 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();
}
}

 

intro.dart 파일


class _IntroPage extends State<IntroPage> {
@override
void initState() {
super.initState();
loadData();
}

// 5초후 메인 화면으로 이동하는 함수 정의
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,
),),),);
}
}

 

 

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

1. 슬리버를 사용한 스크롤 뷰 만들기 

 

1. SliverAppBar

2. SliverList

3. SliverGrid 

세 가지 슬리버를 사용해 확장 앱바와 리스트, 그리드가 차례로 포함된 스크롤뷰 

 

sliverPage.dart 파일

 

class _SliverPage extends State<SliverPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView( // tkdydwk wjddml tmzmfhf gyrhk
slivers: <Widget>[
// slivers를 사용해 확장 앱바, 리스트, 그리드와 같은 다양한 스크롤 효과를 만들 수 있다.
// 슬리버가들어가는위젯을 사용할때는 컨테이너나 텍스트 등 기본적인 위젯을 바로 사용할 수 없고 slivers 인자로 위젯을 묶어야함
SliverAppBar( // 특수 위젯으로 일반 칼럼이나 리스트뷰에서는 사용할 수 없음. 오직 CustomScrollView에서만 사용 가능
// 스크롤에 따라 높이가 달라지거나 다른 위젯 위에 표시되도록 스크롤뷰에 통합 된다.
expandedHeight: 150.0,
flexibleSpace: FlexibleSpaceBar( // 머티리얼 디자인 앱바를 확장, 축소, 스트레칭 해 준다.
title: Text('Slvier Example'),
background: Image.asset('repo/images/sunny.png'),
),
backgroundColor: Colors.deepOrangeAccent,
),
]
),
);
}
}

 

main.dart 페이지 

class _AnimationApp extends State<AnimationApp>{

...

@override
Widget build(BuildContext context) {

...

ElevatedButton(
onPressed: (){
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => SliverPage()));
},
child: Text('페이지 이동'),
),

...

}

 

 

이제 스크롤 할 위젯을 추가한다. 

return Scaffold(
body: CustomScrollView( // tkdydwk wjddml tmzmfhf gyrhk
slivers: <Widget>[
// slivers를 사용해 확장 앱바, 리스트, 그리드와 같은 다양한 스크롤 효과를 만들 수 있다.
// 슬리버가들어가는위젯을 사용할때는 컨테이너나 텍스트 등 기본적인 위젯을 바로 사용할 수 없고 slivers 인자로 위젯을 묶어야함
SliverAppBar( // 특수 위젯으로 일반 칼럼이나 리스트뷰에서는 사용할 수 없음. 오직 CustomScrollView에서만 사용 가능
// 스크롤에 따라 높이가 달라지거나 다른 위젯 위에 표시되도록 스크롤뷰에 통합 된다.
expandedHeight: 150.0,
flexibleSpace: FlexibleSpaceBar( // 머티리얼 디자인 앱바를 확장, 축소, 스트레칭 해 준다.
title: Text('Slvier Example'),
background: Image.asset('repo/images/sunny.png'),
),
backgroundColor: Colors.deepOrangeAccent,
),
SliverList(
delegate: SliverChildListDelegate([
customCard('1'),
customCard('2'),
customCard('3'),
customCard('4'),
])),
SliverGrid(
delegate: SliverChildListDelegate([
customCard('1'),
customCard('2'),
customCard('3'),
customCard('4'),
]),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2)),
],
),
);

 

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

 

 

 

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

 

앞에서 만든 리스트와 그리드를 구분하는 머리말을 추가한다. 

 

SliverPersistentHeader를 이용하면 스리버 위젯별로 헤더를 지정할 수 있다. 

그런데 이걸 사용하려면 SliverPersistentHeaderDelegate라는 딜리게이트가 필요하다. 

 

class _SliverPage extends State<SliverPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView( // tkdydwk wjddml tmzmfhf gyrhk
slivers: <Widget>[
// slivers를 사용해 확장 앱바, 리스트, 그리드와 같은 다양한 스크롤 효과를 만들 수 있다.
// 슬리버가들어가는위젯을 사용할때는 컨테이너나 텍스트 등 기본적인 위젯을 바로 사용할 수 없고 slivers 인자로 위젯을 묶어야함
SliverAppBar( // 특수 위젯으로 일반 칼럼이나 리스트뷰에서는 사용할 수 없음. 오직 CustomScrollView에서만 사용 가능
// 스크롤에 따라 높이가 달라지거나 다른 위젯 위에 표시되도록 스크롤뷰에 통합 된다.
expandedHeight: 150.0,
flexibleSpace: FlexibleSpaceBar( // 머티리얼 디자인 앱바를 확장, 축소, 스트레칭 해 준다.
title: Text('Slvier 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'),
])),
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)),
],
),
);
}

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

class _HeaderDelegate extends SliverPersistentHeaderDelegate { //4개의 함수 정의
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) { //위젯을 계속 그릴것인지를 결정하는 함수
//3개의 값 이 달라지면 true를 반환해 계속 다시 그릴 수 있게 한다.
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}

 

lsit 숫자 영역 

 

그리스 영역 

'플러터 앱 스터디 일지' 카테고리의 다른 글

플러터 스터디 chap 12  (0) 2021.11.08
플러터 스터디 chap 10  (0) 2021.10.13
플러터 스터디 - chap 9  (0) 2021.10.11
플러터 스터디 chap 8  (0) 2021.10.06
플러터 스터디 chap 7  (0) 2021.10.06
댓글
공지사항