Flutter 仿抖音 TikTok 上下滑动 播放视频UI框架,视频播放使用 video_player
github:GitHub - PangHaHa12138/TiktokVideo: Flutter 仿抖音 TikTok 上下滑动 播放视频UI框架
实现功能:
1.上下滑动自动播放切换视频,loading 封面图占位
2.全屏播放横竖屏切换
3.播放进度条显示
4.仿抖音评论弹窗
效果图:
上代码:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:video_player/video_player.dart';
class VideoPage extends StatefulWidget {
const VideoPage({Key? key}) : super(key: key);
@override
State createState() => _VideoPageState();
}
class _VideoPageState extends State<VideoPage> {
late PageController _pageController;
int currentPageIndex = 0; //当前播放索引
int currentIndex = 0; //当前播放索引
List<VideoData> videoDataList = []; //视频数据列表
List<VideoType> videoTypeList = []; //视频分类数据列表
@override
void initState() {
loadData(false);
loadVideoType();
_pageController = PageController(initialPage: currentIndex);
_pageController.addListener(_onPageScroll);
super.initState();
}
void _onPageScroll() {
final pageIndex = _pageController.page?.round();
if (pageIndex != null && pageIndex != currentPageIndex) {
currentPageIndex = pageIndex;
print('=========> currentPageIndex: ${currentPageIndex}');
if (currentPageIndex == videoDataList.length - 2) {
loadData(true);
}
}
}
/// 视频数据 API请求
Future<void> loadData(bool isLoadMore) async {
// 延迟200ms 模拟网络请求
await Future.delayed(const Duration(milliseconds: 200));
if (isLoadMore) {
print('=========> loadData');
List<VideoData> newVideoDataList = [];
newVideoDataList.clear();
newVideoDataList.addAll(videoDataList);
newVideoDataList.addAll(testVideoData);
setState(() {
videoDataList = newVideoDataList;
});
} else {
setState(() {
videoDataList = testVideoData;
});
}
}
/// 视频类型 API请求
Future<void> loadVideoType() async {
// 延迟200ms 模拟网络请求
await Future.delayed(const Duration(milliseconds: 200));
videoTypeList = testVideoType;
setState(() {});
}
@override
void dispose() {
_pageController.removeListener(_onPageScroll);
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return Scaffold(
resizeToAvoidBottomInset: false, //很重要,不加键盘弹出视频会被挤压
body: Stack(
children: [
PageView.builder(
scrollDirection: Axis.vertical,
itemCount: videoDataList.length,
controller: _pageController,
onPageChanged: (currentPage) {
//页面发生改变的回调
},
itemBuilder: (context, index) {
return VideoPlayerFullPage(
size: size,
videoData: videoDataList[index],
videoTypes: videoTypeList,
);
},
),
header(
context,
videoTypeList,
),
],
));
}
Widget header(BuildContext context, List<VideoType> videoTypes) {
var size = MediaQuery.of(context).size;
return Padding(
padding: const EdgeInsets.only(left: 15, top: 10, bottom: 10),
child: SafeArea(
child: Column(
children: [
Row(
children: [
IconButton(
icon: const Icon(
Icons.arrow_back_ios_new,
color: Colors.white,
),
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
}),
GestureDetector(
onTap: () {
onSearchClick();
},
child: Container(
width: size.width - 100,
padding: const EdgeInsets.only(
left: 15, right: 15, top: 5, bottom: 5),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
color: const Color(0x80444444),
),
child: Row(
children: const [
Icon(
Icons.search,
color: Colors.white,
),
SizedBox(
width: 5,
),
Text(
'搜索社群',
style: TextStyle(
color: Colors.white,
fontSize: 15,
),
),
],
),
),
),
],
),
const SizedBox(height: 10),
Wrap(
spacing: 8.0, // 主轴(水平)方向间距
runSpacing: 2.0, // 纵轴(垂直)方向间距
children: videoTypes.map((item) {
return GestureDetector(
onTap: () {
onVideoTypesClick(item);
},
child: Container(
padding: const EdgeInsets.only(
left: 12, right: 12, top: 4, bottom: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0), // 设置圆角
color: const Color(0xFF69DCE5), // 设置背景颜色
),
child: Text(
item.typeName,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
textAlign: TextAlign.center,
),
),
);
}).toList(),
),
],
),
),
);
}
/// 顶部视频类型 点击
Future<void> onVideoTypesClick(VideoType videoType) async {
print('=====> 点击了视频类型');
}
/// 搜索点击
Future<void> onSearchClick() async {
print('=====> 点击了搜索');
}
}
class VideoPlayerFullPage extends StatefulWidget {
final List<VideoType> videoTypes; //视频顶部分类
final VideoData? videoData;
const VideoPlayerFullPage({
Key? key,
required this.size,
required this.videoTypes,
required this.videoData,
}) : super(key: key);
final Size size;
@override
State createState() => _VideoPlayerFullPageState();
}
class _VideoPlayerFullPageState extends State<VideoPlayerFullPage> {
late VideoPlayerController videoController;
bool isInitPlaying = false;
bool isBuffering = false;
List<CommentData> comments = []; //评论数据列表
double videoWidth = 0;
double videoHeight = 0;
double _currentSliderValue = 0.0;
@override
void initState() {
videoController = VideoPlayerController.network(widget.videoData!.videoUrl)
..initialize().then((value) {
videoController.play();
videoController.setLooping(true);
setState(() {
_currentSliderValue = 0.0;
isInitPlaying = true;
videoWidth = videoController.value.size.width;
videoHeight = videoController.value.size.height;
});
});
videoController.addListener(videoListener);
super.initState();
}
void videoListener() {
setState(() {
isBuffering = videoController.value.isBuffering;
_currentSliderValue = videoController.value.position.inSeconds.toDouble();
});
}
@override
void dispose() {
videoController.removeListener(videoListener);
videoController.dispose();
super.dispose();
}
/// 底部视频话题 点击
Future<void> onVideoTagsClick(VideoTag videoTag) async {
print('=====> 点击了视频话题');
}
///点赞
Future<void> onLikeClick(VideoData videoData) async {
print('=====> 点击了点赞');
}
///评论
Future<void> onCommentClick(BuildContext context, VideoData videoData) async {
print('=====> 点击了评论');
// 延迟200ms 模拟网络请求
await Future.delayed(const Duration(milliseconds: 200));
comments = testCommentData;
showCommentBottomSheet(context, comments, videoData);
}
///观看人数
Future<void> onWatchClick(VideoData videoData) async {
print('=====> 点击了观看人数');
}
///分享
Future<void> onShareClick(VideoData videoData) async {
print('=====> 点击了分享');
}
///加好友
Future<void> onAddFriendClick(VideoData videoData) async {
print('=====> 点击了加好友');
}
///发布人名称点击
Future<void> onUserNameClick(VideoData videoData) async {
print('=====> 点击了发布人名称');
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.grey,
height: widget.size.height,
width: widget.size.width,
child: widget.videoData == null
? Center(
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
color: const Color(0x80444444),
),
child: Column(
children: const [
SizedBox(
height: 20,
),
Icon(
Icons.error_outline,
size: 50,
),
SizedBox(
height: 70,
),
Text(
'无数据',
style: TextStyle(fontSize: 20, color: Colors.white),
),
],
),
),
)
: GestureDetector(
onTap: () {
print('============>视频点击 ');
setState(() {
videoController.value.isPlaying
? videoController.pause()
: videoController.play();
});
},
child: Container(
height: widget.size.height,
width: widget.size.width,
decoration: const BoxDecoration(color: Colors.black),
child: Stack(
children: <Widget>[
videoWidth > videoHeight
? Center(
child: AspectRatio(
aspectRatio: videoController.value.aspectRatio,
child: VideoPlayer(videoController),
),
)
: AspectRatio(
aspectRatio: videoController.value.aspectRatio,
child: VideoPlayer(videoController),
),
Center(
child: !videoController.value.isPlaying && !isInitPlaying
? Image.network(
widget.videoData!.albumImg,
width: widget.size.width,
height: widget.size.height,
fit: BoxFit.cover,
)
: const SizedBox(),
),
Center(
child: Container(
decoration: const BoxDecoration(),
child: isPlaying(),
),
),
isBuffering || !videoController.value.isInitialized
? const Center(
child: SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
color: Color(0xFF69DCE5),
),
),
)
: const SizedBox(),
Padding(
padding:
const EdgeInsets.only(left: 0, top: 10, bottom: 10),
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Row(