做網(wǎng)站 做好把我踢開(kāi)長(zhǎng)沙百度網(wǎng)站排名優(yōu)化
flutter3-dylive 跨平臺(tái)仿抖音短視頻直播app實(shí)戰(zhàn)項(xiàng)目。
全新原創(chuàng)基于
flutter3.19.2+dart3.3.0+getx
等技術(shù)開(kāi)發(fā)仿抖音app實(shí)戰(zhàn)項(xiàng)目。實(shí)現(xiàn)了類(lèi)似抖音整屏絲滑式上下滑動(dòng)視頻、左右滑動(dòng)切換頁(yè)面模塊,直播間進(jìn)場(chǎng)/禮物動(dòng)效,聊天等模塊。
運(yùn)用技術(shù)
- 編輯器:vscode
- 技術(shù)框架:flutter3.19.2+dart3.3.0
- 路由/狀態(tài)插件:get: ^4.6.6
- 緩存服務(wù):get_storage: ^2.1.1
- 圖片預(yù)覽插件:photo_view: ^0.14.0
- 刷新加載:easy_refresh^3.3.4
- toast輕提示:toast^0.3.0
- 視頻套件:media_kit: ^1.1.10+1
Flutter-dyLive實(shí)現(xiàn)了類(lèi)似抖音全屏上下滑動(dòng)、左右切換頁(yè)面效果。
左右滑動(dòng)的同時(shí),頂部狀態(tài)欄+Tab菜單+底部bottomNavigationBar導(dǎo)航欄三者聯(lián)動(dòng)效果。
目錄結(jié)構(gòu)
本篇分享主要是短視頻和直播模塊,至于其它技術(shù)知識(shí)點(diǎn),大家可以去看看之前分享的flutter3聊天實(shí)例文章。
https://blog.csdn.net/yanxinyun1990/article/details/136051099
https://blog.csdn.net/yanxinyun1990/article/details/136410049
flutter底部導(dǎo)航菜單
使用 bottomNavigationBar
組件實(shí)現(xiàn)底部導(dǎo)航頁(yè)面模塊切換。通過(guò)getx
狀態(tài)來(lái)聯(lián)動(dòng)控制底部導(dǎo)航欄背景顏色。
中間圖標(biāo)/圖片按鈕,使用了 Positioned
組件定位實(shí)現(xiàn)功能。
return Scaffold(backgroundColor: Colors.grey[50],body: pageList[pageCurrent],// 底部導(dǎo)航欄bottomNavigationBar: Theme(// Flutter去掉BottomNavigationBar底部導(dǎo)航欄的水波紋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;});},),),// 自定義底部導(dǎo)航欄中間按鈕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頁(yè)面
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頁(yè)面List pageList = [const Index(), const FVideo(), const FLiveList(), const Message(), const My()];// tabs選項(xiàng)List pageItems = [const BottomNavigationBarItem(icon: Icon(Icons.home_outlined),label: '首頁(yè)'),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: '我')];// 底部導(dǎo)航欄背景色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;}// 底部導(dǎo)航欄顏色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實(shí)現(xiàn)抖音沉浸式滑動(dòng)
使用TabBar
組件和PageView
組件實(shí)現(xiàn)頂部菜單和頁(yè)面聯(lián)動(dòng)切換效果。
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: [/// 水平滾動(dòng)模塊PageView(// 自定義滾動(dòng)行為(支持桌面端滑動(dòng)、去掉滾動(dòng)條槽)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 = ['熱點(diǎn)', '長(zhǎng)視頻', '文旅', '商城', '關(guān)注', '同城服務(wù)', '推薦'];
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);
// 頁(yè)面controller
late final PageController pageController = PageController(initialPage: pageVideoController.pageVideoTabIndex.value, viewportFraction: 1.0);
void dispose() {tabController.dispose();pageController.dispose();super.dispose();
}
flutter實(shí)現(xiàn)短視頻底部播放拖拽條
短視頻底部又一條mini播放進(jìn)度條,可實(shí)時(shí)顯示視頻播放進(jìn)度,可拖拽到指定播放時(shí)間點(diǎn)。
// flutter滑動(dòng)短視頻模塊 Q:282310962return Container(color: Colors.black,child: Column(children: [Expanded(child: Stack(children: [/// 垂直滾動(dòng)模塊PageView.builder(// 自定義滾動(dòng)行為(支持桌面端滑動(dòng)、去掉滾動(dòng)條槽)scrollBehavior: PageScrollBehavior().copyWith(scrollbars: false),scrollDirection: Axis.vertical,controller: pageController,onPageChanged: (index) async {...},itemCount: videoList.length,itemBuilder: (context, index) {return Stack(children: [// 視頻區(qū)域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,// 無(wú)控制條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();},),),// 右側(cè)操作欄Positioned(bottom: 15.0,right: 10.0,child: Column(...),),// 底部信息區(qū)域Positioned(bottom: 15.0,left: 10.0,right: 80.0,child: Column(...),),// 播放mini進(jìn)度條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), // 調(diào)整滑塊的大小// trackShape: RectangularSliderTrackShape(), // 使用矩形軌道形狀overlayShape: RoundSliderOverlayShape(overlayRadius: 0), // 去掉Slider默認(rèn)上下邊距間隙inactiveTrackColor: Colors.white24, // 設(shè)置非活動(dòng)進(jìn)度條的顏色activeTrackColor: Colors.white, // 設(shè)置活動(dòng)進(jìn)度條的顏色thumbColor: Colors.pinkAccent, // 設(shè)置滑塊的顏色overlayColor: Colors.transparent, // 設(shè)置滑塊覆蓋層的顏色),child: Slider(value: sliderValue,onChanged: (value) async {// debugPrint('當(dāng)前視頻播放時(shí)間$value');setState(() {sliderValue = value;});// 跳轉(zhuǎn)播放時(shí)間await player.seek(duration * value.clamp(0.0, 1.0));},onChangeEnd: (value) async {setState(() {sliderDraging = false;});// 繼續(xù)播放if(!player.state.playing) {await player.play();}},),),onPointerMove: (e) {setState(() {sliderDraging = true;});},),),),// 視頻播放時(shí)間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直播模塊
// 商品購(gòu)買(mǎi)動(dòng)效
Container(...
),// 加入直播間動(dòng)效
const AnimationLiveJoin(joinQueryList: [{'avatar': 'assets/images/logo.png', 'name': 'andy'},{'avatar': 'assets/images/logo.png', 'name': 'jack'},{'avatar': 'assets/images/logo.png', 'name': '一條咸魚(yú)'},{'avatar': 'assets/images/logo.png', 'name': '四季平安'},{'avatar': 'assets/images/logo.png', 'name': '葉子'},],
),// 送禮物動(dòng)效
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': '一條咸魚(yú)', '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': '私人飛機(jī)', '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(...),
),
直播間從右到左進(jìn)入直播間動(dòng)畫(huà)及禮物左側(cè)滑入效果,通過(guò) SlideTransition 組件實(shí)現(xiàn)進(jìn)場(chǎng)動(dòng)畫(huà)。
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 {// 動(dòng)畫(huà)控制器late AnimationController controller = AnimationController(vsync: this,duration: const Duration(milliseconds: 500), // 第一個(gè)動(dòng)畫(huà)持續(xù)時(shí)間);late AnimationController controllerMix = AnimationController(vsync: this,duration: const Duration(milliseconds: 1000), // 第二個(gè)動(dòng)畫(huà)持續(xù)時(shí)間);// 動(dòng)畫(huà)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;// 是否第一個(gè)動(dòng)畫(huà)bool animationFirst = true;// 是否空閑bool idle = true;// 加入直播間數(shù)據(jù)列表List? joinList;void initState() {super.initState();joinList = widget.joinQueryList!.toList();runAnimation();animation.addListener(() {if(animation.status == AnimationStatus.forward) {debugPrint('第一個(gè)動(dòng)畫(huà)進(jìn)行中');idle = false;setState(() {});}else if(animation.status == AnimationStatus.completed) {debugPrint('第一個(gè)動(dòng)畫(huà)結(jié)束');animationFirst = false;if(controllerMix.isCompleted || controllerMix.isDismissed) {timer = Timer(const Duration(seconds: 2), () {controllerMix.forward();debugPrint('第二個(gè)動(dòng)畫(huà)開(kāi)始');});}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;// 執(zhí)行下一個(gè)數(shù)據(jù)runAnimation();setState(() {});}});}void runAnimation() {if(joinList!.isNotEmpty) {// 空閑狀態(tài)才能執(zhí)行,防止添加數(shù)據(jù)播放狀態(tài)混淆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();}}
另外項(xiàng)目中還加入了之前flutter3聊天功能模塊。
Ok,綜上就是flutter3+dart3仿抖音app實(shí)例的一些技術(shù)知識(shí)分享,希望對(duì)大家有所幫助。
最后附上兩個(gè)實(shí)例項(xiàng)目
-
uni-app+vue3+vk-uview多端直播商城項(xiàng)目
https://blog.csdn.net/yanxinyun1990/article/details/135329724 -
vue3+vite4中后臺(tái)管理系統(tǒng)
https://blog.csdn.net/yanxinyun1990/article/details/130144212