티스토리 뷰
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('이전'),
),
...
}
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('사라지기'),
),
}
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,
),
),
)
);
}
}
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;
}
}
'플러터 앱 스터디 일지' 카테고리의 다른 글
플러터 스터디 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 |