
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:tiktok_clone/constants/sizes.dart';
import 'package:video_player/video_player.dart';
import 'package:visibility_detector/visibility_detector.dart';
class VideoPost extends StatefulWidget {
final Function onVideoFinished;
final int index;
const VideoPost({
super.key,
required this.onVideoFinished,
required this.index,
});
State<VideoPost> createState() => _VideoPostState();
}
class _VideoPostState extends State<VideoPost>
with SingleTickerProviderStateMixin {
final VideoPlayerController _videoPlayerController =
VideoPlayerController.asset("assets/videos/video.mp4");
final Duration _animationDuration = const Duration(milliseconds: 200);
late final AnimationController _animationController;
bool _isPaused = false;
void _onVideoChange() {
if (_videoPlayerController.value.isInitialized) {
if (_videoPlayerController.value.duration ==
_videoPlayerController.value.position) {
widget.onVideoFinished();
}
}
}
void _initVideoPlayer() async {
await _videoPlayerController.initialize();
_videoPlayerController.addListener(_onVideoChange);
setState(() {});
}
void initState() {
super.initState();
_initVideoPlayer();
_animationController = AnimationController(
vsync: this,
lowerBound: 1.0,
upperBound: 1.5,
value: 1.5,
duration: _animationDuration,
);
}
void dispose() {
_videoPlayerController.dispose();
super.dispose();
}
void _onVisibilityChanged(VisibilityInfo info) {
if (info.visibleFraction == 1 && !_videoPlayerController.value.isPlaying) {
_videoPlayerController.play();
}
}
void _onTogglePause() {
if (_videoPlayerController.value.isPlaying) {
_videoPlayerController.pause();
_animationController.reverse();
} else {
_videoPlayerController.play();
_animationController.forward();
}
setState(() {
_isPaused = !_isPaused;
});
}
Widget build(BuildContext context) {
return VisibilityDetector(
key: Key("${widget.index}"),
onVisibilityChanged: _onVisibilityChanged,
child: Stack(
children: [
Positioned.fill(
child: _videoPlayerController.value.isInitialized
? VideoPlayer(_videoPlayerController)
: Container(
color: Colors.black,
),
),
Positioned.fill(
child: GestureDetector(
onTap: _onTogglePause,
),
),
Positioned.fill(
child: IgnorePointer(
child: Center(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
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.size52,
),
),
),
),
),
)
],
),
);
}
}
AnimatedBuilder는 setState와 addListener의 조합보다 몇 가지 이유로 애니메이션을 다룰 때 더 우수한 선택입니다:
성능:
AnimatedBuilder는 오직 자식 위젯의 부분적인 트리만 다시 빌드합니다. 즉, 애니메이션 값이 변경될 때마다 위젯 트리의 해당 부분만 업데이트됩니다.addListener와 setState를 사용하면 매번 전체 위젯이 다시 빌드됩니다. 이는 특히 위젯 트리가 크고 복잡할 때 비효율적입니다.코드의 명확성:
AnimatedBuilder는 애니메이션과 관련된 로직을 분리하여 코드를 더 명확하고 관리하기 쉽게 만듭니다. 애니메이션 관련 로직이 특정 위젯에 중점적으로 모여 있기 때문에 디버깅도 더 쉽습니다.addListener와 setState를 사용하면 이들 로직이 전체 위젯 코드와 더 밀접하게 결합될 가능성이 있습니다.재사용성:
AnimatedBuilder는 재사용 가능한 애니메이션 로직을 작성하는 데 유용합니다. 동일한 애니메이션 로직을 여러 위치에서 사용해야 하는 경우, AnimatedBuilder를 사용하면 이를 쉽게 재사용할 수 있습니다.addListener와 setState를 사용하면 애니메이션 로직이 특정 위젯에 더 강하게 결합됩니다.생명주기 관리:
AnimatedBuilder를 사용하면 addListener와 removeListener를 수동으로 관리할 필요가 없습니다. AnimatedBuilder는 내부적으로 이러한 등록 및 해제를 처리합니다.addListener를 직접 사용하면 위젯이 소멸될 때 리스너를 수동으로 제거해야 하므로, 리스너 제거를 잊어버리면 메모리 누수의 원인이 될 수 있습니다.결론적으로, AnimatedBuilder는 코드의 명확성, 재사용성, 성능 향상 및 생명주기 관리의 이점 때문에 addListener와 setState의 조합보다 애니메이션을 처리하는 데 더 효과적인 방법입니다.
다음은 addListener와 setState의 조합으로 애니메이션을 처리하는 코드 사례입니다.
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:tiktok_clone/constants/sizes.dart';
import 'package:video_player/video_player.dart';
import 'package:visibility_detector/visibility_detector.dart';
class VideoPost extends StatefulWidget {
final Function onVideoFinished;
final int index;
const VideoPost({
super.key,
required this.onVideoFinished,
required this.index,
});
State<VideoPost> createState() => _VideoPostState();
}
class _VideoPostState extends State<VideoPost>
with SingleTickerProviderStateMixin {
final VideoPlayerController _videoPlayerController =
VideoPlayerController.asset("assets/videos/video.mp4");
final Duration _animationDuration = const Duration(milliseconds: 200);
late final AnimationController _animationController;
bool _isPaused = false;
void _onVideoChange() {
if (_videoPlayerController.value.isInitialized) {
if (_videoPlayerController.value.duration ==
_videoPlayerController.value.position) {
widget.onVideoFinished();
}
}
}
void _initVideoPlayer() async {
await _videoPlayerController.initialize();
_videoPlayerController.addListener(_onVideoChange);
setState(() {});
}
void initState() {
super.initState();
_initVideoPlayer();
_animationController = AnimationController(
vsync: this,
lowerBound: 1.0,
upperBound: 1.5,
value: 1.5,
duration: _animationDuration,
);
_animationController.addListener(() {
setState(() {});
});
}
void dispose() {
_videoPlayerController.dispose();
super.dispose();
}
void _onVisibilityChanged(VisibilityInfo info) {
if (info.visibleFraction == 1 && !_videoPlayerController.value.isPlaying) {
_videoPlayerController.play();
}
}
void _onTogglePause() {
if (_videoPlayerController.value.isPlaying) {
_videoPlayerController.pause();
_animationController.reverse();
} else {
_videoPlayerController.play();
_animationController.forward();
}
setState(() {
_isPaused = !_isPaused;
});
}
Widget build(BuildContext context) {
return VisibilityDetector(
key: Key("${widget.index}"),
onVisibilityChanged: _onVisibilityChanged,
child: Stack(
children: [
Positioned.fill(
child: _videoPlayerController.value.isInitialized
? VideoPlayer(_videoPlayerController)
: Container(
color: Colors.black,
),
),
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.size52,
),
),
),
),
),
)
],
),
);
}
}