GPT-SoVITS游戏配音应用:快速生成角色专属语音
2025/12/25 0:17:09
效果图
原始数据 (carouselItems) ↓ 包装 无限循环数据 (infiniteItems) ↓ 传递 PageView.builder → 渲染图片1. PageController 核心控制器,管理页面滚动 viewportFraction: 1.0:每页占满屏幕 initialPage: 1:从真实第一张开始 2. PageView.builder 懒加载构建页面 支持无限滚动 与PageController配合 3. AnimatedContainer 实现指示器动画效果 自动处理属性变化的动画1.假设原始数据有3张图片:carouselItems = ['A', 'B', 'C'] 2.扩展数据 infiniteItems = [ carouselItems.last, // 'C' ← 最后一张放在最前面 ...carouselItems, // 'A', 'B', 'C' ← 原始数据 carouselItems.first, // 'A' ← 第一张放在最后面 ] 得到 索引: 0 1 2 3 4 数据: [C] [A] [B] [C] [A] 3.初始状态 用户看到的: [A] [B] [C] 实际数据: [C] [A] [B] [C] [A] ↑ ↑ ↑ ↑ ↑ 索引: 0 1 2 3 4 当前显示: 👆 (索引1,显示A) 4.非边界滑动情况 滑动前: [C] [A] [B] [C] [A] 👆 显示A (索引1) 滑动后: [C] [A] [B] [C] [A] 👆 显示B (索引2) 5.滑动到最左边边界:当用户从A(索引1)向左滑到C(索引0)时: 滑动后: [C] [A] [B] [C] [A] 👆 显示C (索引0) 此时程序检测到:索引0 == 0(边界条件) 立即跳转到:索引3(也是C) 跳转后: [C] [A] [B] [C] [A] 👆 显示C (索引3) 6.滑动到最右边边界:当用户从C(索引3)向右滑到A(索引4)时 滑动后: [C] [A] [B] [C] [A] 👆 显示A (索引4) 此时程序检测到:索引4 == 数组长度-1 立即跳转到:索引1(也是A) 跳转后: [C] [A] [B] [C] [A] 👆 显示A (索引1)代码逻辑对应
void _onPageChanged(int index) { if (index == 0) { // 滑到了最左边的"假"C _pageController.jumpToPage(3); // 跳转到最右边的"真"C _currentIndex = 2; // 显示原始数据的最后一项(索引2) } else if (index == 4) { // 滑到了最右边的"假"A _pageController.jumpToPage(1); // 跳转到最左边的"真"A _currentIndex = 0; // 显示原始数据的第一项(索引0) } else { // 正常滑动 _currentIndex = index - 1; // 减去前面的"假"C } }1.准备图片
//轮播图数据 final List<String> carouselItems = [ 'assets/images/apple.png', 'assets/images/banana.png', 'assets/images/cherry.png', ];2.定义一些必要的变量
late PageController _pageController; //核心控制器 int _currentIndex = 1; // 从原始数据的第一个开始(注意:索引1对应原始数据的第一个) Timer? _timer;//定时器3.为了实现无缝循环,扩展图片数据
//实现无缝循环 List<String> get infiniteItems { return [ carouselItems.last, // 最后一项放在最前面 ...carouselItems, // 原始数据 carouselItems.first, // 第一项放在最后面 ]; }4.初始化
@override void initState() { super.initState(); //初始胡控制器 _pageController = PageController( viewportFraction: 1.0, //每个页面占视口的比例(0.0~1.0) initialPage: 1, //初始显示第几页 ); _startAutoPlay(); //开始自动播放 }5.注销控制器
@override void dispose() { _timer?.cancel(); _pageController.dispose(); super.dispose(); }6.开始自动播放的方法
//==============================开始自动播放================================ void _startAutoPlay() { //Timer.periodic:创建一个周期性定时器 _timer = Timer.periodic(const Duration(seconds: 3), (timer) { //每3秒执行一次回调函数 //页面安全判断 if (!mounted) return; //页面切换 _pageController.nextPage( //切换下一页的方法 duration: const Duration(milliseconds: 800), //动画持续时间 curve: Curves.fastOutSlowIn,//动画曲线为 先加速后减速 ); }); }7.页面切换的回调函数
//==============================页面切换的回调函数================================ void _onPageChanged(int index) { // 处理边界情况,实现无缝循环 if (index == 0) { // 如果滚动到虚拟的第一页(实际是原始数据的最后一页) // 无动画跳转到真实数据的最后一页 _pageController.jumpToPage(carouselItems.length); setState(() { _currentIndex = carouselItems.length - 1; // 显示指示器为最后一页 }); } else if (index == infiniteItems.length - 1) { // 如果滚动到虚拟的最后一页(实际是原始数据的第一页) // 无动画跳转到真实数据的第一页 _pageController.jumpToPage(1); setState(() { _currentIndex = 0; // 显示指示器为第一页 }); } else { // 正常页面变化 setState(() { _currentIndex = index - 1; // 转换为原始数据的索引 }); } }8.暂停播放和继续播放
//================================暂停自动播放============================== void _pauseAutoPlay() { _timer?.cancel(); } //=============================继续自动播放=================================== void _resumeAutoPlay() { _timer?.cancel(); _startAutoPlay(); }9.轮播图的核心*******
// 轮播图区域 SizedBox( height: 142, // 固定高度 child: PageView.builder( controller: _pageController, //控制器 onPageChanged: _onPageChanged,//页面切换回调 itemCount: infiniteItems.length, //总页数 physics: const ClampingScrollPhysics(), //滚动物理效果 itemBuilder: (context, index) { return GestureDetector( onTap: () { // 计算真实数据的索引 int realIndex = index - 1; if (realIndex < 0) realIndex = carouselItems.length - 1; if (realIndex >= carouselItems.length) realIndex = 0; _handleCarouselTap(realIndex); }, onTapDown: (_) => _pauseAutoPlay(), //按下时暂停自动播放 onTapCancel: () => _resumeAutoPlay(), //取消点击时恢复 onTapUp: (_) => _resumeAutoPlay(), //抬起时恢复 child: Container( margin: EdgeInsets.symmetric(horizontal: 20), decoration: BoxDecoration( image: DecorationImage( image: AssetImage(infiniteItems[index]), fit: BoxFit.contain, ), ), ), ); }, ), ),10.分页指示器的实现
// 分页指示器 Container( margin: const EdgeInsets.only(top: 10), // 距离上方10像素的外边距 height: 20, // 容器高度20像素 child: Row( // 水平排列子组件 mainAxisAlignment: MainAxisAlignment.center, // 子组件水平居中 children: List.generate( // 动态生成指示点列表 carouselItems.length, // 根据轮播图数量生成 (index) => AnimatedContainer( // 每个指示点是一个动画容器 duration: const Duration(milliseconds: 300), // 动画持续时间300ms width: _currentIndex == index ? 20 : 8, // 当前激活点宽20,其他宽8 height: 8, // 所有点高度固定为8 margin: const EdgeInsets.symmetric(horizontal: 4), // 左右间距4像素 decoration: BoxDecoration( shape: BoxShape.circle, // 圆形形状 color: _currentIndex == index // 颜色:激活点蓝色,其他灰色 ? Colors.blue : Colors.grey.withOpacity(0.3), ), ), ), ), ),11.单击的方法
void _handleCarouselTap(int index) { print('点击了: ${carouselItems[index]}'); }代码实例
import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class DialMain extends StatefulWidget { const DialMain({super.key}); @override State<StatefulWidget> createState() => _DialMainState(); } class _DialMainState extends State<DialMain> with SingleTickerProviderStateMixin { //轮播图数据 final List<String> carouselItems = [ 'assets/images/apple.png', 'assets/images/banana.png', 'assets/images/cherry.png', ]; late PageController _pageController; //核心控制器 int _currentIndex = 1; // 从原始数据的第一个开始(注意:索引1对应原始数据的第一个) Timer? _timer;//定时器 //实现无缝循环 List<String> get infiniteItems { return [ carouselItems.last, // 最后一项放在最前面 ...carouselItems, // 原始数据 carouselItems.first, // 第一项放在最后面 ]; } @override void initState() { super.initState(); //初始胡控制器 _pageController = PageController( viewportFraction: 1.0, //每个页面占视口的比例(0.0~1.0) initialPage: 1, //初始显示第几页 ); _startAutoPlay(); //开始自动播放 } @override void dispose() { _timer?.cancel(); _pageController.dispose(); super.dispose(); } //==============================开始自动播放================================ void _startAutoPlay() { //Timer.periodic:创建一个周期性定时器 _timer = Timer.periodic(const Duration(seconds: 3), (timer) { //每3秒执行一次回调函数 //页面安全判断 if (!mounted) return; //页面切换 _pageController.nextPage( //切换下一页的方法 duration: const Duration(milliseconds: 800), //动画持续时间 curve: Curves.fastOutSlowIn,//动画曲线为 先加速后减速 ); }); } //==============================页面切换的回调函数================================ void _onPageChanged(int index) { // 处理边界情况,实现无缝循环 if (index == 0) { // 如果滚动到虚拟的第一页(实际是原始数据的最后一页) // 无动画跳转到真实数据的最后一页 _pageController.jumpToPage(carouselItems.length); setState(() { _currentIndex = carouselItems.length - 1; // 显示指示器为最后一页 }); } else if (index == infiniteItems.length - 1) { // 如果滚动到虚拟的最后一页(实际是原始数据的第一页) // 无动画跳转到真实数据的第一页 _pageController.jumpToPage(1); setState(() { _currentIndex = 0; // 显示指示器为第一页 }); } else { // 正常页面变化 setState(() { _currentIndex = index - 1; // 转换为原始数据的索引 }); } } //================================暂停自动播放============================== void _pauseAutoPlay() { _timer?.cancel(); } //=============================继续自动播放=================================== void _resumeAutoPlay() { _timer?.cancel(); _startAutoPlay(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF7F7F9), appBar: AppBar( backgroundColor: const Color(0xFFF7F7F9), title: const Text("轮播图"), centerTitle: true, leading: IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.arrow_back_ios, color: Colors.black), ), ), body: Column( children: [ // 轮播图区域 SizedBox( height: 142, // 固定高度 child: PageView.builder( controller: _pageController, //控制器 onPageChanged: _onPageChanged,//页面切换回调 itemCount: infiniteItems.length, //总页数 physics: const ClampingScrollPhysics(), //滚动物理效果 itemBuilder: (context, index) { return GestureDetector( onTap: () { // 计算真实数据的索引 int realIndex = index - 1; if (realIndex < 0) realIndex = carouselItems.length - 1; if (realIndex >= carouselItems.length) realIndex = 0; _handleCarouselTap(realIndex); }, onTapDown: (_) => _pauseAutoPlay(), //按下时暂停自动播放 onTapCancel: () => _resumeAutoPlay(), //取消点击时恢复 onTapUp: (_) => _resumeAutoPlay(), //抬起时恢复 child: Container( margin: EdgeInsets.symmetric(horizontal: 20), decoration: BoxDecoration( image: DecorationImage( image: AssetImage(infiniteItems[index]), fit: BoxFit.contain, ), ), ), ); }, ), ), // 分页指示器 Container( margin: const EdgeInsets.only(top: 10), // 距离上方10像素的外边距 height: 20, // 容器高度20像素 child: Row( // 水平排列子组件 mainAxisAlignment: MainAxisAlignment.center, // 子组件水平居中 children: List.generate( // 动态生成指示点列表 carouselItems.length, // 根据轮播图数量生成 (index) => AnimatedContainer( // 每个指示点是一个动画容器 duration: const Duration(milliseconds: 300), // 动画持续时间300ms width: _currentIndex == index ? 20 : 8, // 当前激活点宽20,其他宽8 height: 8, // 所有点高度固定为8 margin: const EdgeInsets.symmetric(horizontal: 4), // 左右间距4像素 decoration: BoxDecoration( shape: BoxShape.circle, // 圆形形状 color: _currentIndex == index // 颜色:激活点蓝色,其他灰色 ? Colors.blue : Colors.grey.withOpacity(0.3), ), ), ), ), ), const SizedBox(height: 20), ], ), ); } void _handleCarouselTap(int index) { print('点击了: ${carouselItems[index]}'); } }