import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'dart:math' as math; import '../common_widget/app_data.dart'; import '../common_widget/Song_widegt.dart'; import 'music_view.dart'; // 导入MusicView import '../common/download_manager.dart'; import '../api/api_recommend.dart'; import '../api/api_music_list.dart'; class SongRecommendationView extends StatefulWidget { const SongRecommendationView({super.key}); @override State createState() => _SongRecommendationViewState(); } class _SongRecommendationViewState extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { bool isLoading = false; List rankNames = []; List rankSingerName = []; List rankCoverPath = []; List rankMusicPath = []; List rankMid = []; List songs = []; final downloadManager = Get.put(DownloadManager()); late AnimationController _animationController; late List> _circleFadeInAnimations; late List _dotAnimationControllers; // 圆圈的偏移量 final circleVerticalOffset = -0.08; // 向上偏移屏幕高度的8% final circleHorizontalOffset = 0.0; // 连接线的偏移量 final lineVerticalOffset = -0.06; // 向上偏移屏幕高度的6% final lineHorizontalOffset = 0.0; // 刷新按钮的偏移量 final refreshButtonVerticalOffset = -0.06; final refreshButtonHorizontalOffset = 0.0; // 特效的偏移量 final effectVerticalOffset = 0.035; final effectHorizontalOffset = 0.09; // 添加新的状态控制 int? selectedIndex; // 当前选中的圆圈索引 late AnimationController _expandController; late Animation _expandAnimation; // 在类开始处添加一个新的偏移量常量 final expandedCircleVerticalOffset = -0.08; // 向上偏移屏幕高度的8% @override bool get wantKeepAlive => true; @override void initState() { super.initState(); _loadRecommendedSongs(); _animationController = AnimationController( duration: const Duration(seconds: 2), vsync: this, )..repeat(reverse: true); _circleFadeInAnimations = []; _dotAnimationControllers = []; // 修改展开动画控制器 _expandController = AnimationController( duration: const Duration(milliseconds: 50), // 增加动画时长 vsync: this, ); _expandAnimation = CurvedAnimation( parent: _expandController, curve: Curves.easeOutBack.flipped, // 使用 flipped 让动画更有弹性 ); } @override void dispose() { _expandController.dispose(); super.dispose(); } Future _loadRecommendedSongs() async { setState(() { isLoading = true; }); try { // 检查用户是否已登录 if (!AppData().isUserLoggedIn()) { print('用户未登录或登录信息不完整'); print('Token: ${AppData().currentToken}'); print('UID: ${AppData().currentUid}'); setState(() { isLoading = false; }); return; } // 1. 首先获取推荐的音乐ID列表 final recommendBean = await GetRecommend().getRecommendList( Authorization: AppData().currentToken, uid: AppData().currentUid, ); if (recommendBean.code != 200 || recommendBean.data?.ids == null || recommendBean.data!.ids!.isEmpty) { print('No recommended songs available'); setState(() { isLoading = false; }); return; } // 2. 获取前6个ID的详细信息 final List recommendIds = recommendBean.data!.ids!.take(6).toList(); if (recommendIds.isEmpty) { print('No songs to display'); setState(() { isLoading = false; }); return; } final getMusic = GetMusic(); // 清空现有数据 songs.clear(); // 3. 依次获取每个音乐的详细信息 for (int id in recommendIds) { print('正在获取歌曲ID: $id 的详细信息'); final musicDetail = await getMusic.getMusicById( id: id, Authorization: AppData().currentToken, ); print('歌曲详情返回状态码: ${musicDetail.code}'); if (musicDetail.code == 200) { print('成功获取歌曲: ${musicDetail.name}'); } else { print('获取歌曲失败,错误信息: ${musicDetail.msg}'); } if (musicDetail.code == 200) { songs.add(Song( artistPic: (musicDetail.coverPath == null || musicDetail.coverPath == '') ? 'https://api.aspark.cc/image/1/6766afbde0707.png' : musicDetail.coverPath!, title: musicDetail.name ?? '', artist: musicDetail.singerName ?? '', musicurl: musicDetail.musicPath ?? '', pic: (musicDetail.coverPath == null || musicDetail.coverPath == '') ? 'https://api.aspark.cc/image/1/6766afbde0707.png' : musicDetail.coverPath!, id: musicDetail.id ?? 0, likes: null, collection: null, mid: musicDetail.mid ?? '', )); } } print('最终加载的歌曲数量: ${songs.length}'); if (songs.isNotEmpty) { print('加载的歌曲列表:'); for (var song in songs) { print('- ${song.title} (ID: ${song.id})'); } } if (songs.isEmpty) { print('Failed to load any songs'); setState(() { isLoading = false; }); return; } setState(() {}); } catch (e) { print('加载推荐歌曲时发生错误:'); print('错误类型: ${e.runtimeType}'); print('Error fetching recommended songs: $e'); setState(() { isLoading = false; }); return; } setState(() { isLoading = false; }); if (songs.isNotEmpty) { _resetCircleAnimations(); } } void _resetCircleAnimations() { if (songs.isEmpty) { return; } _circleFadeInAnimations.clear(); for (final controller in _dotAnimationControllers) { controller.dispose(); } _dotAnimationControllers.clear(); for (int i = 0; i < songs.length; i++) { final fadeIn = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _animationController, curve: Interval(i * 0.1, (i + 1) * 0.1, curve: Curves.easeInOut), ), ); _circleFadeInAnimations.add(fadeIn); final dotController = AnimationController( duration: const Duration(milliseconds: 2000), vsync: this, )..repeat(); _dotAnimationControllers.add(dotController); } _animationController.forward(from: 0); } double _getCircleSize(Size screenSize) { return screenSize.width * 0.18; } // 修改 _getCirclePosition 方法 Map _getCirclePosition(int index, int totalItems, Size screenSize, Offset refreshButtonPosition) { if (totalItems == 0) return {'left': 0, 'top': 0}; // 添加保护 final radius = screenSize.width * 0.28; final angle = (-math.pi / 2) + (index * 2 * math.pi / totalItems); final circleSize = _getCircleSize(screenSize); // 使用屏幕中心作为基准点 final centerX = screenSize.width / 2; final centerY = screenSize.height / 2; // 确保返回的值不是 NaN final left = centerX + radius * math.cos(angle) - (circleSize / 2) + (screenSize.width * circleHorizontalOffset); final top = centerY + radius * math.sin(angle) - (circleSize / 2) + (screenSize.height * circleVerticalOffset); if (left.isNaN || top.isNaN) { return {'left': centerX, 'top': centerY}; } return {'left': left, 'top': top}; } Color _getCircleColor(double relevance) { return Color(0xFFB2FF59).withOpacity(0.3); } Offset _getEffectPosition(Offset basePosition, double progress, double angle, double radius, Size size) { if (size.width <= 0 || size.height <= 0 || !progress.isFinite || !angle.isFinite || !radius.isFinite) { return Offset.zero; } // 使用和_getCirclePosition完全相同的计算方式 final centerX = size.width / 2 + (size.width * circleHorizontalOffset); final centerY = size.height / 2 + (size.height * circleVerticalOffset); // 计算终点位置 final targetX = centerX + radius * math.cos(angle); final targetY = centerY + radius * math.sin(angle); if (!targetX.isFinite || !targetY.isFinite || !centerX.isFinite || !centerY.isFinite) { return Offset.zero; } // 线性插值 return Offset( centerX + (targetX - centerX) * progress, centerY + (targetY - centerY) * progress ); } // 处理圆圈点击 void _handleCircleTap(int index) { // 如果已经有选中的圆圈,并且点击的不是当前选中的圆圈,则忽略点击 if (selectedIndex != null && selectedIndex != index) { return; } setState(() { if (selectedIndex == index) { selectedIndex = null; _expandController.reverse(); } else { selectedIndex = index; _expandController.forward(from: 0.0); } }); } double _getOpacity(double progress) { // 在前半程淡入 if (progress < 0.5) { return progress * 2; // 0到1的渐变 } // 在后半程淡出 return (1 - progress) * 2; // 1到0的渐变 } @override Widget build(BuildContext context) { super.build(context); final screenSize = MediaQuery.of(context).size; final safeAreaPadding = MediaQuery.of(context).padding; final refreshButtonPosition = Offset(screenSize.width / 2, screenSize.height / 2); return Container( decoration: const BoxDecoration( image: DecorationImage( image: AssetImage("assets/img/app_bg.png"), fit: BoxFit.cover, ), ), child: GestureDetector( onTap: () { if (selectedIndex != null) { setState(() { selectedIndex = null; _expandController.reverse(); }); } }, child: Scaffold( backgroundColor: Colors.transparent, body: SafeArea( child: Stack( children: [ LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( physics: NeverScrollableScrollPhysics(), child: Container( height: constraints.maxHeight, child: Stack( fit: StackFit.loose, children: [ Positioned( top: 20, left: 0, right: 0, child: Center( child: Text( '知音推荐', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black.withOpacity(0.8), ), ), ), ), Center( child: Stack( alignment: Alignment.center, children: [ if (songs.isNotEmpty) ...[ CustomPaint( size: Size(screenSize.width, screenSize.height), painter: selectedIndex == null ? LinePainter( recommendedSongs: songs, screenSize: screenSize, lineAnimation: _animationController, refreshButtonPosition: refreshButtonPosition, verticalOffset: lineVerticalOffset, horizontalOffset: lineHorizontalOffset, ) : null, ), if (!isLoading) Stack( children: List.generate(songs.length, (index) { final song = songs[index]; final baseSize = _getCircleSize(screenSize); final position = _getCirclePosition(index, songs.length, screenSize, refreshButtonPosition); return AnimatedBuilder( animation: _expandAnimation, builder: (context, child) { final expandedSize = selectedIndex == index ? math.min( baseSize * (1 + _expandAnimation.value * 2.5), // 减小一点展开倍数 screenSize.height * 0.45, // 稍微减小最大高度 ) : baseSize; return AnimatedPositioned( duration: Duration(milliseconds: 400), curve: Curves.easeOutCubic, // 使位置移动更流畅 left: selectedIndex == index ? (screenSize.width - expandedSize) / 2 : position['left']!, top: selectedIndex == index ? screenSize.height * 0.2 // 固定展开位置,避免计算导致的跳动 : position['top']!, child: AnimatedOpacity( duration: Duration(milliseconds: 300), curve: Curves.easeOut, opacity: selectedIndex == null || selectedIndex == index ? 1.0 : 0.0, child: FadeTransition( opacity: _circleFadeInAnimations.isNotEmpty && index < _circleFadeInAnimations.length ? _circleFadeInAnimations[index] : AlwaysStoppedAnimation(1.0), child: GestureDetector( onTap: () { // 如果卡片已展开,触发播放 if (selectedIndex == index) { Navigator.push( context, MaterialPageRoute( builder: (context) => MusicView( songList: songs, initialSongIndex: index, onSongStatusChanged: (index, isCollected, isLiked) { setState(() { songs[index].collection = isCollected; songs[index].likes = isLiked; downloadManager.updateSongInfo(songs[index].id, isCollected, isLiked); }); }, ), ), ); } else { // 否则展开卡片 _handleCircleTap(index); } }, behavior: HitTestBehavior.translucent, child: SongCircleButton( size: expandedSize, songTitle: song.title ?? '', artistName: song.artist ?? '', songIndex: index, songList: songs, coverPath: song.pic ?? '', // 添加封面路径 isExpanded: selectedIndex == index, expandProgress: _expandAnimation.value, ), ), ), ), ); }, ); }), ), ], if (selectedIndex == null) ..._dotAnimationControllers.asMap().entries.map((entry) { final index = entry.key; final dotController = entry.value; return AnimatedBuilder( animation: dotController, builder: (context, child) { final progress = dotController.value; final position = _getCirclePosition(index, songs.length, screenSize, refreshButtonPosition); final radius = screenSize.width * 0.28; final angle = (index * 2 * math.pi / songs.length) - math.pi / 2; final dotPosition = _getEffectPosition( Offset( position['left']?.isFinite == true ? position['left']! : 0.0, position['top']?.isFinite == true ? position['top']! : 0.0 ), progress.isFinite ? progress : 0.0, angle.isFinite ? angle : 0.0, radius.isFinite ? radius : 0.0, screenSize ); if (dotPosition.dx.isNaN || dotPosition.dy.isNaN) { return Container(); } return Positioned( left: dotPosition.dx - 4, top: dotPosition.dy - 4, child: Container( width: 8, height: 8, decoration: BoxDecoration( color: Colors.white.withOpacity(_getOpacity(progress)), shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.white.withOpacity(_getOpacity(progress) * 0.6), blurRadius: 8, spreadRadius: 3, ), ], ), ), ); }, ); }).toList(), if (selectedIndex == null) // 以下是修改建议 Transform.translate( offset: Offset( // 添加安全检查,确保offset不会出现NaN (screenSize.width * refreshButtonHorizontalOffset).isFinite ? screenSize.width * refreshButtonHorizontalOffset : 0.0, (screenSize.height * refreshButtonVerticalOffset).isFinite ? screenSize.height * refreshButtonVerticalOffset : 0.0, ), child: Container( width: (screenSize.width * 0.15).isFinite ? screenSize.width * 0.15 : 0.0, height: (screenSize.width * 0.15).isFinite ? screenSize.width * 0.15 : 0.0, decoration: BoxDecoration( color: Colors.blue, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.blue.withOpacity(0.2), blurRadius: 10, spreadRadius: 3, ), ], ), child: IconButton( icon: Icon( isLoading ? Icons.hourglass_empty : Icons.refresh, color: Colors.white, size: (screenSize.width * 0.07).isFinite ? screenSize.width * 0.07 : 0.0, ), onPressed: () { if (!isLoading) { _animationController.reverse(from: 1); Future.delayed(const Duration(milliseconds: 500), () { _loadRecommendedSongs(); }); } }, ), ), ) ], ), ), ], ), ), ); }, ), ], ), ), ), ), ); } } class LinePainter extends CustomPainter { final List recommendedSongs; final Size screenSize; final Animation lineAnimation; final Offset refreshButtonPosition; final double verticalOffset; // 添加垂直偏移参数 final double horizontalOffset; // 添加水平偏移参数 LinePainter({ required this.recommendedSongs, required this.screenSize, required this.lineAnimation, required this.refreshButtonPosition, required this.verticalOffset, required this.horizontalOffset, }); @override void paint(Canvas canvas, Size size) { // 添加空值检查 if (recommendedSongs.isEmpty || size.width <= 0 || size.height <= 0) { return; } final centerX = size.width / 2 + (size.width * horizontalOffset); final centerY = size.height / 2 + (size.height * verticalOffset); // 检查中心点坐标 if (centerX.isNaN || centerY.isNaN) { return; } final center = Offset(centerX, centerY); final radius = size.width * 0.28; for (int i = 0; i < recommendedSongs.length; i++) { final angle = (-math.pi / 2) + (i * 2 * math.pi / recommendedSongs.length); final circleX = centerX + radius * math.cos(angle); final circleY = centerY + radius * math.sin(angle); if (circleX.isNaN || circleY.isNaN) { continue; } final circleCenter = Offset( centerX + radius * math.cos(angle), centerY + radius * math.sin(angle) ); // 绘制发光效果 final glowPaint = Paint() ..shader = RadialGradient( colors: [ Color(0xFFFFB74D).withOpacity(0.3), Color(0xFFFFB74D).withOpacity(0.2), Color(0xFFFFB74D).withOpacity(0.1), Color(0xFFFFB74D).withOpacity(0.0), ], stops: [0.0, 0.3, 0.6, 1.0], ).createShader( Rect.fromCircle( center: circleCenter, radius: size.width * 0.15, ), ); // 绘制光晕 canvas.drawCircle( circleCenter, size.width * 0.15, glowPaint, ); // 绘制连接线 final linePaint = Paint() ..color = Colors.grey.withOpacity(0.2) ..strokeWidth = 1.0; canvas.drawLine(center, circleCenter, linePaint); } } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } } class SongCircleButton extends StatelessWidget { final double size; final String songTitle; final String artistName; final int songIndex; final List songList; final bool isExpanded; final double expandProgress; final String coverPath; // 添加封面路径参数 const SongCircleButton({ required this.size, required this.songTitle, required this.artistName, required this.songIndex, required this.songList, required this.coverPath, // 添加到构造函数 this.isExpanded = false, this.expandProgress = 0.0, }); @override Widget build(BuildContext context) { final validSize = math.max(0.0, size); return AnimatedContainer( duration: Duration(milliseconds: 400), curve: Curves.easeOutCubic, width: size, height: size, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.45, minHeight: 0, // 添加最小高度约束 minWidth: 0, // 添加最小宽度约束 ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(isExpanded ? 24 : size / 2), boxShadow: [ BoxShadow( color: Color(0xFFFFB74D).withOpacity(isExpanded ? 0.4 : 0.3), blurRadius: isExpanded ? 20 : 10, spreadRadius: isExpanded ? 1 : 0, offset: Offset(0, isExpanded ? 4 : 2), ), BoxShadow( color: Color(0xFFFFB74D).withOpacity(isExpanded ? 0.2 : 0.15), blurRadius: isExpanded ? 40 : 20, spreadRadius: isExpanded ? 2 : 1, ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(isExpanded ? 24 : size / 2), child: Stack( children: [ // 背景图片层 Positioned.fill( child: Image.network( coverPath.isEmpty ? 'https://api.aspark.cc/image/1/6759856d288fd.jpg' : coverPath, fit: BoxFit.cover, ), ), // 模糊和暗化层 Positioned.fill( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 1, sigmaY: 1), child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withOpacity(0.3), Colors.black.withOpacity(0.3), ], ), ), ), ), ), // 内容层 Center( child: SingleChildScrollView( physics: NeverScrollableScrollPhysics(), child: AnimatedPadding( duration: Duration(milliseconds: 400), curve: Curves.easeOutCubic, padding: EdgeInsets.all(size * (isExpanded ? 0.1 : 0.12)), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 修改内容层的文字部分 Text( songTitle, textAlign: TextAlign.center, maxLines: 1, // 固定为1行 overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: isExpanded ? size * 0.12 : size * 0.18, height: 1.2, shadows: [ Shadow( color: Colors.black.withOpacity(0.3), blurRadius: 2, offset: Offset(1, 1), ), ], ), ), SizedBox(height: size * (isExpanded ? 0.05 : 0.02)), Text( artistName, textAlign: TextAlign.center, maxLines: 1, // 固定为1行 overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white.withOpacity(0.9), fontSize: isExpanded ? size * 0.09 : size * 0.14, height: 1.2, shadows: [ Shadow( color: Colors.black.withOpacity(0.3), blurRadius: 2, offset: Offset(1, 1), ), ], ), ), if (isExpanded) ...[ SizedBox(height: size * 0.08), Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), shape: BoxShape.circle, ), child: Icon( Icons.play_arrow, color: Colors.white, size: size * 0.15, ), ), ], ], ), ), ), ), ], ), ), ); } }