使用Flutter 3.x构建跨平台仿抖音应用开发教程
发表时间: 2024-03-24 23:43
#精品长文创作季#
又经过了半个多月的爆肝输出,全新原创的flutter3_douyin短视频直播项目完结了。
目前已经实现好的功能有首页、短视频、直播、聊天、我的等几个页面模块。
实现了类似抖音全屏沉浸式滑动效果(上下滑动视频、左右切换页面模块)。
实现了左右滑动的同时,顶部状态栏+Tab菜单+底部bottomNavigationBar导航栏三者联动效果。
为了考虑在windows端调试方便,目前视频播放器使用media_kit套件。
在开发的过程中,遇到了不少问题,最后都顺利解决了。
使用 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}); @override 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; } // ...}
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 ], ), ], ), ), ], ),);
短视频底部有一条播放进度条。
return 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),), ], ), ), ), ], ); }, ), ], ), ), ], ),);
Ok,限于篇幅,flutter3仿抖音短视频项目就先分享到这里。