import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'dart:math' as math; import '../api/api_music_rank.dart'; import '../common_widget/app_data.dart'; import '../models/getRank_bean.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 { 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 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: 500), // 增加动画时长 vsync: this, ); _expandAnimation = CurvedAnimation( parent: _expandController, curve: Curves.easeOutQuart, // 使用更适合扩展的动画曲线 ); } @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/6759856d288fd.jpg' : musicDetail.coverPath!, title: musicDetail.name ?? '', artist: musicDetail.singerName ?? '', musicurl: musicDetail.musicPath ?? '', pic: (musicDetail.coverPath == null || musicDetail.coverPath == '') ? 'https://api.aspark.cc/image/1/6759856d288fd.jpg' : musicDetail.coverPath!, id: musicDetail.id ?? 0, likes: null, collection: null, 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(reverse: true); _dotAnimationControllers.add(dotController); } _animationController.forward(from: 0); } double _getCircleSize(Size screenSize) { return screenSize.width * 0.18; } Map _getCirclePosition(int index, int totalItems, Size screenSize, Offset refreshButtonPosition) { 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; return { 'left': centerX + radius * math.cos(angle) - (circleSize / 2) + (screenSize.width * circleHorizontalOffset), 'top': centerY + radius * math.sin(angle) - (circleSize / 2) + (screenSize.height * circleVerticalOffset), }; } Color _getCircleColor(double relevance) { return Color(0xFFB2FF59).withOpacity(0.3); } Offset _getEffectPosition(Offset basePosition, double progress, double angle, double radius, Size size) { return Offset( basePosition.dx - progress * (radius - 60) * math.cos(angle) + (size.width * effectHorizontalOffset), basePosition.dy - progress * (radius - 60) * math.sin(angle) + (size.height * effectVerticalOffset), ); } // 处理圆圈点击 void _handleCircleTap(int index) { setState(() { if (selectedIndex == index) { selectedIndex = null; _expandController.reverse(); } else { selectedIndex = index; _expandController.forward(from: 0.0); } }); } @override Widget build(BuildContext 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: 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: [ 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(6, (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 * 5.0), screenSize.height * 0.7, // 限制最大高度为屏幕高度的70% ) : baseSize; return AnimatedPositioned( duration: Duration(milliseconds: 300), left: selectedIndex == index ? (screenSize.width - expandedSize) / 2 : position['left']!, top: selectedIndex == index ? math.max( screenSize.height * 0.15, (screenSize.height - expandedSize) / 2.5 + (screenSize.height * expandedCircleVerticalOffset) ) : position['top']!, child: AnimatedOpacity( duration: Duration(milliseconds: 300), 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: () => _handleCircleTap(index), onDoubleTap: () { if (selectedIndex != null && selectedIndex != index) { return; } Navigator.push( context, MaterialPageRoute( builder: (context) => MusicView( songList: songs, initialSongIndex: selectedIndex ?? index, onSongStatusChanged: (index, isCollected, isLiked) { setState(() { songs[index].collection = isCollected; songs[index].likes = isLiked; downloadManager.updateSongInfo(songs[index].id, isCollected, isLiked); }); }, ), ), ); }, behavior: selectedIndex != null && selectedIndex != index ? HitTestBehavior.translucent : HitTestBehavior.opaque, child: SongCircleButton( size: expandedSize, songTitle: song.title ?? '', artistName: song.artist ?? '', songIndex: index, songList: songs, 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.32; final angle = (index * 2 * math.pi / songs.length) - math.pi / 2; final dotPosition = _getEffectPosition( Offset(position['left']!, position['top']!), progress, angle, radius, screenSize ); return Positioned( left: dotPosition.dx - 4, top: dotPosition.dy - 4, child: Container( width: 8, height: 8, decoration: BoxDecoration( color: Colors.white.withOpacity(0.6), shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.white.withOpacity(0.4), blurRadius: 8, spreadRadius: 3, ), ], ), ), ); }, ); }).toList(), if (selectedIndex == null) Transform.translate( offset: Offset( screenSize.width * refreshButtonHorizontalOffset, screenSize.height * refreshButtonVerticalOffset, ), child: Container( width: screenSize.width * 0.15, height: screenSize.width * 0.15, 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, ), 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) { final centerX = size.width / 2 + (size.width * horizontalOffset); final centerY = size.height / 2 + (size.height * verticalOffset); 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 circleCenter = Offset( centerX + radius * math.cos(angle), centerY + radius * math.sin(angle) ); final paint = Paint() ..color = Colors.grey.withOpacity(0.2) ..strokeWidth = 1.0; canvas.drawLine(center, circleCenter, paint); } } @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; const SongCircleButton({ required this.size, required this.songTitle, required this.artistName, required this.songIndex, required this.songList, this.isExpanded = false, this.expandProgress = 0.0, }); @override Widget build(BuildContext context) { return AnimatedContainer( duration: Duration(milliseconds: 300), width: size, height: size, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.7, // 限制最大高度 ), decoration: BoxDecoration( color: Color(0xFFFFC1E3).withOpacity(isExpanded ? 1.0 : 0.95), borderRadius: BorderRadius.circular(size / 2), boxShadow: [ BoxShadow( color: Color(0xFFFFC1E3).withOpacity(isExpanded ? 0.5 : 0.25), blurRadius: isExpanded ? 40 : 12, spreadRadius: isExpanded ? 20 : 4, ), ], ), child: Center( child: SingleChildScrollView( // 添加滚动支持 physics: NeverScrollableScrollPhysics(), child: AnimatedPadding( duration: Duration(milliseconds: 300), padding: EdgeInsets.all(size * (isExpanded ? 0.1 : 0.12)), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (isExpanded) ...[ Text( '推荐歌曲', style: TextStyle( color: Colors.white.withOpacity(0.8), fontSize: size * 0.08, height: 1.2, ), ), SizedBox(height: size * 0.05), ], Text( songTitle, textAlign: TextAlign.center, maxLines: isExpanded ? null : 2, overflow: isExpanded ? null : TextOverflow.ellipsis, style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: isExpanded ? size * 0.12 // 增大展开时的字体 : size * 0.15, height: 1.2, ), ), SizedBox(height: size * (isExpanded ? 0.05 : 0.02)), Text( artistName, textAlign: TextAlign.center, maxLines: isExpanded ? null : 1, overflow: isExpanded ? null : TextOverflow.ellipsis, style: TextStyle( color: Colors.white.withOpacity(isExpanded ? 1.0 : 0.8), fontSize: isExpanded ? size * 0.09 // 增大展开时的字体 : size * 0.12, height: 1.2, ), ), if (isExpanded) ...[ SizedBox(height: size * 0.08), Text( '双击播放', style: TextStyle( color: Colors.white.withOpacity(0.7), fontSize: size * 0.08, height: 1.2, ), ), SizedBox(height: size * 0.05), Icon( Icons.play_circle_outline, color: Colors.white.withOpacity(0.8), size: size * 0.15, ), ], ], ), ), ), ), ); } }