import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:tiktok_clone/constants/gaps.dart';
import 'package:tiktok_clone/constants/sizes.dart';
import 'package:tiktok_clone/features/videos/widgets/video_button.dart';
import 'package:tiktok_clone/features/videos/widgets/video_comments.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 {
late final VideoPlayerController _videoPlayerController;
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 {
_videoPlayerController =
VideoPlayerController.asset("assets/videos/video.mp4");
await _videoPlayerController.initialize();
await _videoPlayerController.setLooping(true);
_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 &&
!_isPaused &&
!_videoPlayerController.value.isPlaying) {
_videoPlayerController.play();
}
}
void _onTogglePause() {
if (_videoPlayerController.value.isPlaying) {
_videoPlayerController.pause();
_animationController.reverse();
} else {
_videoPlayerController.play();
_animationController.forward();
}
setState(() {
_isPaused = !_isPaused;
});
}
void _onCommentsTap(BuildContext context) async {
if (_videoPlayerController.value.isPlaying) {
_onTogglePause();
}
await showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => const VideoComments(),
);
_onTogglePause();
}
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,
),
),
),
),
),
),
Positioned(
bottom: 20,
left: 10,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
"@니꼬",
style: TextStyle(
fontSize: Sizes.size20,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Gaps.v10,
Text(
"This is my house in Thailand!!!",
style: TextStyle(
fontSize: Sizes.size16,
color: Colors.white,
),
)
],
),
),
Positioned(
bottom: 20,
right: 10,
child: Column(
children: [
const CircleAvatar(
radius: 25,
backgroundColor: Colors.black,
foregroundColor: Colors.white,
foregroundImage: NetworkImage(
"https://avatars.githubusercontent.com/u/3612017",
),
child: Text("니꼬"),
),
Gaps.v24,
const VideoButton(
icon: FontAwesomeIcons.solidHeart,
text: "2.9M",
),
Gaps.v24,
GestureDetector(
onTap: () => _onCommentsTap(context),
child: const VideoButton(
icon: FontAwesomeIcons.solidComment,
text: "33K",
),
),
Gaps.v24,
const VideoButton(
icon: FontAwesomeIcons.share,
text: "Share",
)
],
),
),
],
),
);
}
}
이 코드 스니펫에서는 showModalBottomSheet
메서드를 사용하여 모달 스타일의 하단 시트를 표시하는 방법을 보여줍니다. showModalBottomSheet
는 Flutter의 material
라이브러리에서 제공하는 함수로, 화면 하단에서 올라오는 패널을 생성합니다. 이 패널은 사용자가 다른 화면 영역을 탭할 때까지 화면에 남아있으며, 주로 추가 정보를 표시하거나 사용자 입력을 받는 데 사용됩니다.
모달 동작: 하단 시트는 모달이므로, 사용자가 시트 외부를 탭하면 시트가 닫힙니다.
커스터마이징 가능: showModalBottomSheet
함수는 builder
인자를 통해 커스텀 위젯을 하단 시트로 표시할 수 있게 해줍니다. 이를 통해 다양한 레이아웃과 스타일의 하단 시트를 생성할 수 있습니다.
배경색과 레이아웃: backgroundColor
를 통해 시트의 배경색을 지정할 수 있습니다. 코드에서는 Colors.transparent
로 설정되어 있어 배경이 투명하게 보입니다.
void _onCommentsTap(BuildContext context) async {
if (_videoPlayerController.value.isPlaying) {
_onTogglePause();
}
await showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => const VideoComments(),
);
_onTogglePause();
}
이 함수는 사용자가 댓글 버튼을 탭할 때 호출됩니다. 먼저 비디오가 재생 중이면 일시 중지하고, showModalBottomSheet
를 호출하여 VideoComments
위젯을 하단 시트로 표시합니다.
VideoComments
는 사용자 댓글을 보여주는 커스텀 위젯으로 추정됩니다.
시트가 닫히면 _onTogglePause
가 다시 호출되어 비디오 재생을 재개합니다.
이렇게 showModalBottomSheet
는 Flutter 앱에서 추가 정보를 제공하거나 사용자 입력을 받는 등의 상호작용을 구현할 때 유용하게 사용될 수 있습니다.