민프
[Flutter] animation 효과를 적용해보자 (animationController, SingleTickerProviderStateMixin, vsync) 본문
[Flutter] animation 효과를 적용해보자 (animationController, SingleTickerProviderStateMixin, vsync)
민프야 2023. 9. 7. 17:50Flutter에서 Animation을 적용해보려고 합니다.
Flutter에서 Animation을 보다 쉽게 적용하는 방법은 크게
1. animation 위젯 사용
2. animationController 사용
이 있습니다.
예제 화면 설명은
영상이 플레이 되고 있는 상태 인 화면을 터치하면 AnimatedOpacity가 적용된 플레이 이모티콘이 나오면서 영상이 정지가 되고,
정지 된 영상 화면을 클릭하면 AnimatedOpacity가 적용된 플레이 이모티콘이 사라지게 되면서 영상이 플레이 되는 화면 입니다.
1. animation 위젯 사용
https://docs.flutter.dev/ui/widgets/animation
애니메이션 위젯들은 위 링크를 통해서 어떤 것들이 있는지 확인하면 되고 이번 포스팅에서는 AnimatedOpacity를 선택해서 적용을 해보겠습니다.
@override
Widget build(BuildContext context) {
return VisibilityDetector(
key: Key("${widget.index}"),
onVisibilityChanged: (VisibilityInfo info) {
_onVisibilityChanged(info);
},
child: Stack(
children: [
Positioned.fill(
child: _videoPlayerController.value.isInitialized
? VideoPlayer(_videoPlayerController)
: Container(
color: Colors.teal,
),
),
Positioned.fill(
child: GestureDetector(
onTap: _onTogglePause,
),
),
Positioned.fill(
child: IgnorePointer(
child: Center(
child: AnimatedOpacity(
opacity: _isPaused ? 1 : 0,
duration: _animationDuration,
child: const FaIcon(
FontAwesomeIcons.play,
color: Colors.white,
size: Sizes.size48,
),
),
),
)),
],
),
);
}
아래 결과 화면을 보면 AnimatedOpacity가 잘 적용된 것을 확인하실 수 있습니다.
2. animationController 사용
그럼 두번째로 animationController를 이용해서 애니메이션을 추가해보겠습니다.
animationController를 이용해서 이벤트를 추가할때
AnimatedOpacity가 부드럽게 변하는 것 과 같은 효과를 주려면
build 메서드에게 소수점 단위로 업데이트해줘서 부드럽게 변경을 해줘야합니다.
예를 들어서)
Opacity가 1 -> 0으로 갈때
0.9 -> 0.8 -> 0.7 ... 0 으로 가야 부드럽게 변경되는 것 처럼 보일 수 있게 됩니다.
근데 만약 1에서 0으로 바로 바뀌게 된다면 부드럽게 보이는 것 보다는 on / off 처럼 켰다 꺼졌다 처럼 보이게 될 것 입니다.
그럼 animationController를 어떻게 사용해야할까요?
개발자 문서에 보면
animationController를 사용할 때 vsync인자를 받게 되는데 여기에서 vsync를 사용할 때 SingleTickerProviderStateMixin를 추가해서 정의하라고 나와있습니다.
그럼 여기에서
vsync,
SingleTickerProviderStateMixin
는 뭘까?
문서에 의하면
vsync는 위젯이 안보일 때는 애니메이션이 작동하지 않도록 하는 것,
SingleTickerProviderStateMixin는 현재 위젯 트리가 활성화된 동안만 Tick 하는 단일 Ticker를 제공하는 것 즉, 위젯이 화면에 보일때만 Ticker를 제공해준 다는 말 입니다.
그럼 실제 animationController를 정의하는 아래 코드를 보면
class _VideoPostState extends State<VideoPost>
with SingleTickerProviderStateMixin {
.
.
.
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
lowerBound: 1.0,
upperBound: 1.5,
value: 1.5,
duration: _animationDuration,
);
}
with SingleTickerProviderStateMixin를 하여서 SingleTickerProviderStateMixin의 클래스 및 메서드들을 가져온 것을 확인할 수 있고,
vsync : this를 하여서 animationController에 필요한 SingleTickerProviderStateMixin 내부에 있는 vsync 메서드를 적용시킨 것을 확인할 수 있습니다.
그럼 위 컨트롤러가 어떻게 동작하는걸까요?
animationController는 부드럽게 처리가 되어야합니다.
그래서 매 초, 매 밀리초도 아니고 애니메이션의 프레임마다 실행됩니다.
예를 들어서)
초당 60프레임에 0부터 1까지 엄청 부드럽게 애니메이션 효과를 줘야한다고 했을 때,
애니메이션에 callback을 제공해주는 게 바로 Ticker 입니다.
실제로 SingleTickerProviderStateMixin의 내부 코드를 보면
.
.
.
@optionalTypeArgs
mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
Ticker? _ticker;
@override
Ticker createTicker(TickerCallback onTick) {
assert(() {
if (_ticker == null) {
return true;
}
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.'),
ErrorDescription('A SingleTickerProviderStateMixin can only be used as a TickerProvider once.'),
ErrorHint(
'If a State is used for multiple AnimationController objects, or if it is passed to other '
'objects and those objects might use it more than one time in total, then instead of '
'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.',
),
]);
}());
_ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null);
_updateTickerModeNotifier();
_updateTicker(); // Sets _ticker.mute correctly.
return _ticker!;
}
.
.
.
이렇게 시작이 되는데 this로 호출이 되면 매 프레임마다 아래 코드가 callback(onTick)되어서 Ticker가 생성 되게 됍니다.
_ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null);
그 이후 애니메이션이 Ticker를 잡을 거고,
애니메이션이 재생되어야 할 때가 오면 Ticker가 그걸 애니메이션에게 알려주게 됩니다.
따라서 animationController에서 업데이트 된 부분을 build 메서드에게 업데이트가 되는 것을 전달해야하는데
전달하는 방법은
1. setState({})
2. AnimatedBuilder
가 있습니다.
결과화면은 아래와 같습니다.
- 1. setState({})
첫번째 방법은 아래 코드와 같이 setState를 호출해서 build메서드에게 전달해주는 방법입니다.
// 변경 된 수치를 setState로 Build에게 업데이트 요청
_animationController.addListener(() {
setState(() {});
});
전체 코드는 아래와 같습니다.
.
.
.
late final AnimationController _animationController;
@override
void initState() {
super.initState();
_initVideoPlayer();
// 애니메이션 컨트롤러 설정
_animationController = AnimationController(
vsync: this,
lowerBound: 1.0,
upperBound: 1.5,
value: 1.5,
duration: _animationDuration,
);
// 변경 된 수치를 setState로 Build에게 업데이트 요청
_animationController.addListener(() {
setState(() {});
});
}
void _onTogglePause() {
if (_videoPlayerController.value.isPlaying) {
_videoPlayerController.pause();
_animationController.reverse();
} else {
_videoPlayerController.play();
_animationController.forward();
}
setState(() {
_isPaused = !_isPaused;
});
}
@override
Widget build(BuildContext context) {
return VisibilityDetector(
key: Key("${widget.index}"),
onVisibilityChanged: (VisibilityInfo info) {
_onVisibilityChanged(info);
},
child: Stack(
children: [
Positioned.fill(
child: _videoPlayerController.value.isInitialized
? VideoPlayer(_videoPlayerController)
: Container(
color: Colors.teal,
),
),
Positioned.fill(
child: GestureDetector(
onTap: _onTogglePause,
),
),
Positioned.fill(
child: IgnorePointer(
child: Center(
child: Transform.scale(
scale: _animationController.value,
child: AnimatedOpacity(
opacity: _isPaused ? 1 : 0,
duration: _animationDuration,
child: const FaIcon(
FontAwesomeIcons.play,
color: Colors.white,
size: Sizes.size48,
),
),
),
),
)),
],
),
);
}
- 2. AnimatedBuilder
아래 코드에서와 같이 AnimatedBuilder를 사용하게 되면
_animationController가 업데이트 될 때 마다 build에게 전달이 되어서 애니메이션이 잘 동작하는 것을 확인할 수 있습니다.
.
.
.
late final AnimationController _animationController;
@override
void initState() {
super.initState();
_initVideoPlayer();
// 애니메이션 컨트롤러 설정
_animationController = AnimationController(
vsync: this,
lowerBound: 1.0,
upperBound: 1.5,
value: 1.5,
duration: _animationDuration,
);
}
void _onTogglePause() {
if (_videoPlayerController.value.isPlaying) {
_videoPlayerController.pause();
_animationController.reverse();
} else {
_videoPlayerController.play();
_animationController.forward();
}
setState(() {
_isPaused = !_isPaused;
});
}
@override
Widget build(BuildContext context) {
return VisibilityDetector(
key: Key("${widget.index}"),
onVisibilityChanged: (VisibilityInfo info) {
_onVisibilityChanged(info);
},
child: Stack(
children: [
Positioned.fill(
child: _videoPlayerController.value.isInitialized
? VideoPlayer(_videoPlayerController)
: Container(
color: Colors.teal,
),
),
Positioned.fill(
child: GestureDetector(
onTap: _onTogglePause,
),
),
Positioned.fill(
child: IgnorePointer(
child: Center(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
//_animationController 값이 변경 될 때마다 실행됨
return Transform.scale(
scale: _animationController.value,
child: child,
);
},
child: AnimatedOpacity(
opacity: _isPaused ? 1 : 0,
duration: _animationDuration,
child: const FaIcon(
FontAwesomeIcons.play,
color: Colors.white,
size: Sizes.size48,
),
),
),
),
)),
],
),
);
}
참고링크
https://docs.flutter.dev/ui/animations/tutorial
https://api.flutter.dev/flutter/widgets/SingleTickerProviderStateMixin-mixin.html
https://api.flutter.dev/flutter/scheduler/Ticker-class.html
'[Flutter]' 카테고리의 다른 글
[Flutter] TabBar, Tab, TabBarView를 이용해서 탭 메뉴를 만들고, 탭 메뉴에 대한 화면을 만들어보자 (0) | 2023.09.20 |
---|---|
[Flutter] showModalBottomSheet(바텀시트)를 사용해보자 (0) | 2023.09.08 |
[Flutter] ThemeData, TextTheme를 적용해서 앱의 테마를 지정해보자 (0) | 2023.09.06 |
[Flutter] Flutter 아키텍쳐를 알아보자 (0) | 2023.08.29 |
[Flutter] Skia, Impeller 렌더링 엔진에 대해서 알아보자 (0) | 2023.08.29 |