使用Flutter 3.x构建跨平台仿抖音应用开发教程

发表时间: 2024-03-24 23:43

#精品长文创作季#

又经过了半个多月的爆肝输出,全新原创的flutter3_douyin短视频直播项目完结了。

目前已经实现好的功能有首页、短视频、直播、聊天、我的等几个页面模块。

实现了类似抖音全屏沉浸式滑动效果(上下滑动视频、左右切换页面模块)。

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

技术栈

  • 编辑器: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

为了考虑在windows端调试方便,目前视频播放器使用media_kit套件。

在开发的过程中,遇到了不少问题,最后都顺利解决了。

项目结构目录

预览图

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;  }  // ...}

flutter3短视频滑动效果

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仿抖音短视频项目就先分享到这里。