You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MTMusic/lib/view/song_recommendation_view.dart

827 lines
32 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<SongRecommendationView> createState() => _SongRecommendationViewState();
}
class _SongRecommendationViewState extends State<SongRecommendationView>
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
bool isLoading = false;
List rankNames = [];
List rankSingerName = [];
List rankCoverPath = [];
List rankMusicPath = [];
List rankMid = [];
List<Song> songs = [];
final downloadManager = Get.put(DownloadManager());
late AnimationController _animationController;
late List<Animation<double>> _circleFadeInAnimations;
late List<AnimationController> _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<double> _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<void> _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<int> 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<double>(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<String, double> _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<double> 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<Song> 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,
),
),
],
],
),
),
),
),
],
),
),
);
}
}