flutter3-dylive 跨平台仿抖音短视频直播app实战项目。
全新原创基于
flutter3.19.2+dart3.3.0+getx
等技术开发仿抖音app实战项目。实现了类似抖音整屏丝滑式上下滑动视频、左右滑动切换页面模块,直播间进场/礼物动效,聊天等模块。
运用技术
- 编辑器:vscode
- 技术框架:flutter3.19.2+dart3.3.0
- 路由/状态插件:get: ^4.6.6
- 缓存服务:get_storage: ^2.1.1
- 图片预览插件:photo_view: ^0.14.0
- 刷新加载:easy_refresh^3.3.4
- toast轻提示:toast^0.3.0
- 视频套件:media_kit: ^1.1.10+1
Flutter-dyLive实现了类似抖音全屏上下滑动、左右切换页面效果。
左右滑动的同时,顶部状态栏+Tab菜单+底部bottomNavigationBar导航栏三者联动效果。
目录结构
本篇分享主要是短视频和直播模块,至于其它技术知识点,大家可以去看看之前分享的flutter3聊天实例文章。
https://blog.csdn.net/yanxinyun1990/article/details/136051099
https://blog.csdn.net/yanxinyun1990/article/details/136410049
flutter底部导航菜单
使用 bottomNavigationBar
组件实现底部导航页面模块切换。通过getx
状态来联动控制底部导航栏背景颜色。
中间图标/图片按钮,使用了 Positioned
组件定位实现功能。
return Scaffold(backgroundColor: Colors.grey[50],body: pageList[pageCurrent],// 底部导航栏bottomNavigationBar: Theme(// Flutter去掉BottomNavigationBar底部导航栏的水波纹data: ThemeData(splashColor: Colors.transparent,highlightColor: Colors.transparent,hoverColor: Colors.transparent,),child: Obx(() {return Stack(children: [Container(decoration: const BoxDecoration(border: Border(top: BorderSide(color: Colors.black54, width: .1)),),child: BottomNavigationBar(backgroundColor: bottomNavigationBgcolor(),fixedColor: FStyle.primaryColor,unselectedItemColor: bottomNavigationItemcolor(),type: BottomNavigationBarType.fixed,elevation: 1.0,unselectedFontSize: 12.0,selectedFontSize: 12.0,currentIndex: pageCurrent,items: [...pageItems],onTap: (index) {setState(() {pageCurrent = index;});},),),// 自定义底部导航栏中间按钮Positioned(left: MediaQuery.of(context).size.width / 2 - 15,top: 0,bottom: 0,child: InkWell(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [// Icon(Icons.tiktok, color: bottomNavigationItemcolor(centerDocked: true), size: 32.0,),Image.asset('assets/images/applogo.png', width: 32.0, fit: BoxFit.contain,)// Text('直播', style: TextStyle(color: bottomNavigationItemcolor(centerDocked: true), fontSize: 12.0),)],),onTap: () {setState(() {pageCurrent = 2;});},),),],);}),),
);
import 'package:flutter/material.dart';
import 'package:get/get.dart';import '../styles/index.dart';
import '../../controllers/page_video_controller.dart';// 引入pages页面
import '../pages/index/index.dart';
import '../pages/video/index.dart';
import '../pages/live/index.dart';
import '../pages/message/index.dart';
import '../pages/my/index.dart';class Layout extends StatefulWidget {const Layout({super.key});State<Layout> createState() => _LayoutState();
}class _LayoutState extends State<Layout> {PageVideoController pageVideoController = Get.put(PageVideoController());// page索引int pageCurrent = 0;// page页面List pageList = [const Index(), const FVideo(), const FLiveList(), const Message(), const My()];// tabs选项List pageItems = [const BottomNavigationBarItem(icon: Icon(Icons.home_outlined),label: '首页'),const BottomNavigationBarItem(icon: Icon(Icons.play_arrow_outlined),label: '短视频'),const BottomNavigationBarItem(icon: Icon(Icons.live_tv_rounded, color: Colors.transparent,),label: ''),BottomNavigationBarItem(icon: Stack(alignment: const Alignment(4, -2),children: [const Icon(Icons.messenger_outline),FStyle.badge(1)],),label: '消息'),BottomNavigationBarItem(icon: Stack(alignment: const Alignment(1.5, -1),children: [const Icon(Icons.person_outline),FStyle.badge(0, isdot: true)],),label: '我')];// 底部导航栏背景色Color bottomNavigationBgcolor() {int index = pageCurrent;int pageVideoTabIndex = pageVideoController.pageVideoTabIndex.value;Color color = Colors.white;if(index == 1) {if([1, 2, 3].contains(pageVideoTabIndex)) {color = Colors.white;}else {color = Colors.black;}}return color;}// 底部导航栏颜色Color bottomNavigationItemcolor({centerDocked = false}) {int index = pageCurrent;int pageVideoTabIndex = pageVideoController.pageVideoTabIndex.value;Color color = Colors.black54;if(index == 1) {if([1, 2, 3].contains(pageVideoTabIndex)) {color = Colors.black54;}else {color = Colors.white60;}}else if(index == 2 && centerDocked) {color = FStyle.primaryColor;}return color;}// ...
}
flutter3实现抖音沉浸式滑动
使用TabBar
组件和PageView
组件实现顶部菜单和页面联动切换效果。
return Scaffold(extendBodyBehindAppBar: true,appBar: AppBar(forceMaterialTransparency: true,backgroundColor: [1, 2, 3].contains(pageVideoController.pageVideoTabIndex.value) ? null : Colors.transparent,foregroundColor: [1, 2, 3].contains(pageVideoController.pageVideoTabIndex.value) ? Colors.black : Colors.white,titleSpacing: 1.0,leading: Obx(() => IconButton(icon: Icon(Icons.menu, color: tabColor(),), onPressed: () {},),),title: Obx(() {return TabBar(controller: tabController,tabs: pageTabs.map((v) => Tab(text: v)).toList(),isScrollable: true,tabAlignment: TabAlignment.center,overlayColor: MaterialStateProperty.all(Colors.transparent),unselectedLabelColor: unselectedTabColor(),labelColor: tabColor(),indicatorColor: tabColor(),indicatorSize: TabBarIndicatorSize.label,unselectedLabelStyle: const TextStyle(fontSize: 16.0, fontFamily: 'Microsoft YaHei'),labelStyle: const TextStyle(fontSize: 16.0, fontFamily: 'Microsoft YaHei', fontWeight: FontWeight.w600),dividerHeight: 0,labelPadding: const EdgeInsets.symmetric(horizontal: 10.0),indicatorPadding: const EdgeInsets.symmetric(horizontal: 5.0),onTap: (index) {pageVideoController.updatePageVideoTabIndex(index); // 更新索引pageController.jumpToPage(index);},);}),actions: [Obx(() => IconButton(icon: Icon(Icons.search, color: tabColor(),), onPressed: () {},),),],),body: Column(children: [Expanded(child: Stack(children: [/// 水平滚动模块PageView(// 自定义滚动行为(支持桌面端滑动、去掉滚动条槽)scrollBehavior: PageScrollBehavior().copyWith(scrollbars: false),scrollDirection: Axis.horizontal,controller: pageController,onPageChanged: (index) {pageVideoController.updatePageVideoTabIndex(index); // 更新索引setState(() {tabController.animateTo(index);});},children: [...pageModules],),],),),],),
);
PageVideoController pageVideoController = Get.put(PageVideoController());List<String> pageTabs = ['热点', '长视频', '文旅', '商城', '关注', '同城服务', '推荐'];
final pageModules = [const HotModule(),const LongVideoModule(),const TripModule(),const MallModule(),const FavorModule(),const NearModule(),const RecommendModule()
];
late final TabController tabController = TabController(initialIndex: pageVideoController.pageVideoTabIndex.value, length: pageTabs.length, vsync: this);
// 页面controller
late final PageController pageController = PageController(initialPage: pageVideoController.pageVideoTabIndex.value, viewportFraction: 1.0);
void dispose() {tabController.dispose();pageController.dispose();super.dispose();
}
flutter实现短视频底部播放拖拽条
短视频底部又一条mini播放进度条,可实时显示视频播放进度,可拖拽到指定播放时间点。
// flutter滑动短视频模块 Q:282310962return Container(color: Colors.black,child: Column(children: [Expanded(child: Stack(children: [/// 垂直滚动模块PageView.builder(// 自定义滚动行为(支持桌面端滑动、去掉滚动条槽)scrollBehavior: PageScrollBehavior().copyWith(scrollbars: false),scrollDirection: Axis.vertical,controller: pageController,onPageChanged: (index) async {...},itemCount: videoList.length,itemBuilder: (context, index) {return Stack(children: [// 视频区域Positioned(top: 0,left: 0,right: 0,bottom: 0,child: GestureDetector(child: Stack(children: [// 短视频插件Visibility(visible: videoIndex == index,child: Video(controller: videoController,fit: BoxFit.cover,// 无控制条controls: NoVideoControls,),),// 播放/暂停按钮StreamBuilder(stream: player.stream.playing,builder: (context, playing) {return Visibility(visible: playing.data == false,child: Center(child: IconButton(padding: EdgeInsets.zero,onPressed: () {player.playOrPause();},icon: Icon(playing.data == true ? Icons.pause : Icons.play_arrow_rounded,color: Colors.white70,size: 70,),),),);},),],),onTap: () {player.playOrPause();},),),// 右侧操作栏Positioned(bottom: 15.0,right: 10.0,child: Column(...),),// 底部信息区域Positioned(bottom: 15.0,left: 10.0,right: 80.0,child: Column(...),),// 播放mini进度条Positioned(bottom: 0.0,left: 10.0,right: 10.0,child: Visibility(visible: videoIndex == index && position > Duration.zero,child: Listener(child: SliderTheme(data: const SliderThemeData(trackHeight: 2.0,thumbShape: RoundSliderThumbShape(enabledThumbRadius: 4.0), // 调整滑块的大小// trackShape: RectangularSliderTrackShape(), // 使用矩形轨道形状overlayShape: RoundSliderOverlayShape(overlayRadius: 0), // 去掉Slider默认上下边距间隙inactiveTrackColor: Colors.white24, // 设置非活动进度条的颜色activeTrackColor: Colors.white, // 设置活动进度条的颜色thumbColor: Colors.pinkAccent, // 设置滑块的颜色overlayColor: Colors.transparent, // 设置滑块覆盖层的颜色),child: Slider(value: sliderValue,onChanged: (value) async {// debugPrint('当前视频播放时间$value');setState(() {sliderValue = value;});// 跳转播放时间await player.seek(duration * value.clamp(0.0, 1.0));},onChangeEnd: (value) async {setState(() {sliderDraging = false;});// 继续播放if(!player.state.playing) {await player.play();}},),),onPointerMove: (e) {setState(() {sliderDraging = true;});},),),),// 视频播放时间Positioned(bottom: 90.0,left: 10.0,right: 10.0,child: Visibility(visible: sliderDraging,child: Row(mainAxisAlignment: MainAxisAlignment.center,children: [Text(position.label(reference: duration), style: const TextStyle(color: Colors.white, fontSize: 16.0, fontWeight: FontWeight.w600),),Container(margin: const EdgeInsets.symmetric(horizontal: 7.0),child: const Text('/', style: TextStyle(color: Colors.white54, fontSize: 10.0,),),),Text(duration.label(reference: duration), style: const TextStyle(color: Colors.white54, fontSize: 16.0, fontWeight: FontWeight.w600),),],),),),],);},),/// 固定层// 红包Positioned(left: 15.0,top: MediaQuery.of(context).padding.top + 20,child: Container(height: 40.0,width: 40.0,decoration: BoxDecoration(color: Colors.black12,borderRadius: BorderRadius.circular(100.0),),child: UnconstrainedBox(child: Image.asset('assets/images/time-hb.png', width: 30.0, fit: BoxFit.contain,),),),),],),),],),
);
flutter3直播模块
// 商品购买动效
Container(...
),// 加入直播间动效
const AnimationLiveJoin(joinQueryList: [{'avatar': 'assets/images/logo.png', 'name': 'andy'},{'avatar': 'assets/images/logo.png', 'name': 'jack'},{'avatar': 'assets/images/logo.png', 'name': '一条咸鱼'},{'avatar': 'assets/images/logo.png', 'name': '四季平安'},{'avatar': 'assets/images/logo.png', 'name': '叶子'},],
),// 送礼物动效
const AnimationLiveGift(giftQueryList: [{'label': '小心心', 'gift': 'assets/images/gift/gift1.png', 'user': 'Jack', 'avatar': 'assets/images/avatar/uimg2.jpg', 'num': 12},{'label': '棒棒糖', 'gift': 'assets/images/gift/gift2.png', 'user': 'Andy', 'avatar': 'assets/images/avatar/uimg6.jpg', 'num': 36},{'label': '大啤酒', 'gift': 'assets/images/gift/gift3.png', 'user': '一条咸鱼', 'avatar': 'assets/images/avatar/uimg1.jpg', 'num': 162},{'label': '人气票', 'gift': 'assets/images/gift/gift4.png', 'user': 'Flower', 'avatar': 'assets/images/avatar/uimg5.jpg', 'num': 57},{'label': '鲜花', 'gift': 'assets/images/gift/gift5.png', 'user': '四季平安', 'avatar': 'assets/images/avatar/uimg3.jpg', 'num': 6},{'label': '捏捏小脸', 'gift': 'assets/images/gift/gift6.png', 'user': 'Alice', 'avatar': 'assets/images/avatar/uimg4.jpg', 'num': 28},{'label': '你真好看', 'gift': 'assets/images/gift/gift7.png', 'user': '叶子', 'avatar': 'assets/images/avatar/uimg7.jpg', 'num': 95},{'label': '亲吻', 'gift': 'assets/images/gift/gift8.png', 'user': 'YOYO', 'avatar': 'assets/images/avatar/uimg8.jpg', 'num': 11},{'label': '玫瑰', 'gift': 'assets/images/gift/gift12.png', 'user': '宇辉', 'avatar': 'assets/images/avatar/uimg9.jpg', 'num': 3},{'label': '私人飞机', 'gift': 'assets/images/gift/gift16.png', 'user': 'Hison', 'avatar': 'assets/images/avatar/uimg10.jpg', 'num': 273},],
),// 直播弹幕+商品讲解
Container(margin: const EdgeInsets.only(top: 7.0),height: 200.0,child: Row(crossAxisAlignment: CrossAxisAlignment.end,children: [Expanded(child: ListView.builder(padding: EdgeInsets.zero,itemCount: liveJson[index]['message']?.length,itemBuilder: (context, i) => danmuList(liveJson[index]['message'])[i],),),SizedBox(width: isVisibleGoodsTalk ? 7 : 35,),// 商品讲解Visibility(visible: isVisibleGoodsTalk,child: Column(...),),],),
),// 底部工具栏
Container(margin: const EdgeInsets.only(top: 7.0),child: Row(...),
),
直播间从右到左进入直播间动画及礼物左侧滑入效果,通过 SlideTransition 组件实现进场动画。
return SlideTransition(position: animationFirst ? animation : animationMix,child: Container(alignment: Alignment.centerLeft,margin: const EdgeInsets.only(top: 7.0),padding: const EdgeInsets.symmetric(horizontal: 7.0,),height: 23.0,width: 250,decoration: const BoxDecoration(gradient: LinearGradient(begin: Alignment.centerLeft,end: Alignment.centerRight,colors: [Color(0xFF6301FF), Colors.transparent],),borderRadius: BorderRadius.horizontal(left: Radius.circular(10.0)),),child: joinList!.isNotEmpty ? Text('欢迎 ${joinList![0]['name']} 加入直播间', style: const TextStyle(color: Colors.white, fontSize: 14.0,),):Container(),),
);
class _AnimationLiveJoinState extends State<AnimationLiveJoin> with TickerProviderStateMixin {// 动画控制器late AnimationController controller = AnimationController(vsync: this,duration: const Duration(milliseconds: 500), // 第一个动画持续时间);late AnimationController controllerMix = AnimationController(vsync: this,duration: const Duration(milliseconds: 1000), // 第二个动画持续时间);// 动画late Animation<Offset> animation = Tween(begin: const Offset(2.5, 0), end: const Offset(0, 0)).animate(controller);late Animation<Offset> animationMix = Tween(begin: const Offset(0, 0), end: const Offset(-2.5, 0)).animate(controllerMix);Timer? timer;// 是否第一个动画bool animationFirst = true;// 是否空闲bool idle = true;// 加入直播间数据列表List? joinList;void initState() {super.initState();joinList = widget.joinQueryList!.toList();runAnimation();animation.addListener(() {if(animation.status == AnimationStatus.forward) {debugPrint('第一个动画进行中');idle = false;setState(() {});}else if(animation.status == AnimationStatus.completed) {debugPrint('第一个动画结束');animationFirst = false;if(controllerMix.isCompleted || controllerMix.isDismissed) {timer = Timer(const Duration(seconds: 2), () {controllerMix.forward();debugPrint('第二个动画开始');});}setState(() {});}});animationMix.addListener(() {if(animationMix.status == AnimationStatus.forward) {setState(() {});}else if(animationMix.status == AnimationStatus.completed) {animationFirst = true;controller.reset();controllerMix.reset();if(joinList!.isNotEmpty) {joinList!.removeAt(0);}idle = true;// 执行下一个数据runAnimation();setState(() {});}});}void runAnimation() {if(joinList!.isNotEmpty) {// 空闲状态才能执行,防止添加数据播放状态混淆if(idle == true) {if(controller.isCompleted || controller.isDismissed) {setState(() {});timer = Timer(Duration.zero, () {controller.forward();});}}}}void dispose() {controller.dispose();controllerMix.dispose();timer?.cancel();super.dispose();}}
另外项目中还加入了之前flutter3聊天功能模块。
Ok,综上就是flutter3+dart3仿抖音app实例的一些技术知识分享,希望对大家有所帮助。
最后附上两个实例项目
-
uni-app+vue3+vk-uview多端直播商城项目
https://blog.csdn.net/yanxinyun1990/article/details/135329724 -
vue3+vite4中后台管理系统
https://blog.csdn.net/yanxinyun1990/article/details/130144212