您的当前位置:首页正文

flutter3-dylive仿抖音App实例|Flutter3+Getx实战短视频直播应用

2024-10-17 来源:个人技术集锦

原创研发 flutter3+getX+mediaKit 仿抖音app 短视频直播实战 Flutter-DouYin

flutter3_dylive 使用最新跨平台技术 flutter3.x+dart3+getx+get_storage+media_kit 开发手机端仿抖音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_douyin实现了类似抖音全屏沉浸式滑动效果(上下滑动视频、左右切换页面模块)。

如下图:实现了左右滑动的同时,顶部 状态栏+Tab菜单+底部bottomNavigationBar导航栏 三者联动效果。

项目结构目录

在项目创建前期,需要配置好Flutter SDK和Dart SDK开发环境。

如果是使用Vscode编辑器开发flutter项目,大家需自行配置flutter/dart扩展语法插件。

项目中的一些技术知识点,有些是在之前的项目flutter3聊天实例中有介绍过,感兴趣的话可以去看看。

Flutter入口配置main.dart

import 'dart:io';import 'package:flutter/material.dart';import 'package:get/get.dart';import 'package:get_storage/get_storage.dart';import 'package:media_kit/media_kit.dart';import 'utils/index.dart';// 引入布局模板import 'layouts/index.dart';import 'binding/binding.dart';// 引入路由管理import 'router/index.dart';void main() async {  // 初始化get_storage  await GetStorage.init();  // 初始化media_kit  WidgetsFlutterBinding.ensureInitialized();  MediaKit.ensureInitialized();  runApp(const MyApp());}class MyApp extends StatelessWidget {  const MyApp({super.key});  @override  Widget build(BuildContext context) {    return GetMaterialApp(      title: 'FLUTTER3 DYLIVE',      debugShowCheckedModeBanner: false,      theme: ThemeData(        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFFFE2C55)),        useMaterial3: true,        // 修正windows端字体粗细不一致        fontFamily: Platform.isWindows ? 'Microsoft YaHei' : null,      ),      home: const Layout(),      // 全局绑定GetXController      initialBinding: GlobalBindingController(),      // 初始路由      initialRoute: Utils.isLogin() ? '/' : '/login',      // 路由页面      getPages: routePages,      // 错误路由      // unknownRoute: GetPage(name: '/404', page: Error),    );  }}

Flutter3底部导航栏

底部导航栏使用 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;  }  // ...}

Flutter实现抖音全屏式滑动

如下图:实现类似抖音左右滑动全屏切换页面模块、上下滑动丝滑切换短视频。

采用 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);// 页面controllerlate final PageController pageController = PageController(initialPage: pageVideoController.pageVideoTabIndex.value, viewportFraction: 1.0);@overridevoid dispose() {  tabController.dispose();  pageController.dispose();  super.dispose();}

flutter短视频mini进度条

底部mini播放进度条一开始是采用自定义组件实现效果,不过不能拖动滚动条进度,后来就采用了 Slider 组件来实现。

// 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直播模块

flutter直播模块包含了 顶部信息、底部进场提示+商品层+弹幕层+操作栏 等几个模块。

// 商品购买动效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(    ...  ),),

直播聊天弹幕通过Flexible配合Text.rich / TextSpan实现消息自适应布局界面。

通过 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;  @override  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();          });        }      }    }  }  @override  void dispose() {    controller.dispose();    controllerMix.dispose();    timer?.cancel();    super.dispose();  }}

综上就是flutter3实战开发仿抖音的一些知识分享,由于涉及的技术点还是蛮多的,就没有非常详细的展开介绍了。希望以上的一些分享能给大家有些帮助~

目前flutter3-dylive项目已经同步到我的原创作品集。

最后附上两个跨端项目

uniapp+vue3短视频直播商城:

tauri+vite4中后台管理系统:

显示全文