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/main_tab_view/main_tab_view.dart

532 lines
19 KiB

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import 'package:music_player_miao/view/rank_view.dart';
import 'package:music_player_miao/view/user/user_view.dart';
import '../../common/audio_player_controller.dart';
import '../home_view.dart';
import '../release_view.dart';
import '../song_recommendation_view.dart';
// 迷你播放器组件
class MiniPlayer extends StatelessWidget {
MiniPlayer({super.key}) : audioController = Get.find<AudioPlayerController>();
final AudioPlayerController audioController;
void _showPlaylist(BuildContext context) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(18))),
builder: (BuildContext context) {
return Container(
padding: const EdgeInsets.only(top: 15),
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.45),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
color: Colors.white,
padding: const EdgeInsets.only(bottom: 10),
child: const Center(
child: Text(
"播放列表",
style: TextStyle(fontSize: 20),
),
),
),
Expanded(
child: ClipRRect(
child: Obx(() => ListView.builder(
padding: EdgeInsets.zero,
itemCount: audioController.musicNames.length,
itemBuilder: (BuildContext context, int index) {
final isCurrentlyPlaying =
audioController.currentSongIndex.value == index;
return Container(
decoration: BoxDecoration(
color: isCurrentlyPlaying
? const Color(0xffE3F0ED)
: Colors.white,
),
child: ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 20),
title: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
audioController.musicNames[index],
style: const TextStyle(fontSize: 18),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (isCurrentlyPlaying)
Image.asset(
"assets/img/songs_run.png",
width: 25,
),
],
),
onTap: () {
audioController.changeSong(index);
Navigator.pop(context);
},
),
);
},
)),
),
),
],
),
);
},
);
}
Widget _buildPlayButton() {
return SizedBox(
width: 52,
height: 52,
child: Center(
child: Obx(() {
// 先检查歌单是否为空
if (audioController.songList.isEmpty) {
return IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 42,
minHeight: 42,
maxWidth: 42,
maxHeight: 42,
),
onPressed: null, // 禁用按钮
icon: Image.asset(
"assets/img/music_pause.png",
width: 25,
height: 25,
color: Colors.grey, // 使用灰色表示禁用状态
),
);
}
if (audioController.isLoading.value) {
return const SizedBox(
width: 25,
height: 25,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xff429482)),
strokeWidth: 1.0,
),
);
}
return IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 42,
minHeight: 42,
maxWidth: 42,
maxHeight: 42,
),
onPressed: audioController.playOrPause,
icon: Obx(() => Image.asset(
audioController.isPlaying.value
? "assets/img/music_play.png"
: "assets/img/music_pause.png",
width: 25,
height: 25,
)),
);
}),
),
);
}
@override
Widget build(BuildContext context) {
return Obx(() {
final bool hasPlaylist = audioController.songList.isNotEmpty;
return GestureDetector(
onHorizontalDragEnd: hasPlaylist
? (DragEndDetails details) {
if (audioController.songList.isEmpty) return;
final velocity = details.velocity.pixelsPerSecond.dx;
const threshold = 300.0;
if (velocity > threshold) {
audioController.playPrevious();
HapticFeedback.mediumImpact();
} else if (velocity < -threshold) {
audioController.playNext();
HapticFeedback.mediumImpact();
}
}
: null,
child: Container(
height: 64, // 增加高度使布局更加宽敞
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 8,
spreadRadius: 1,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
// 歌曲封面
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: Hero(
tag: 'album_cover',
flightShuttleBuilder: (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
if (!hasPlaylist) {
return const Icon(Icons.music_note, size: 30);
}
return ClipRRect(
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.hardEdge,
child: Container(
width: 48,
height: 48,
child: Image.network(
audioController
.songList[
audioController.currentSongIndex.value]
.pic,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
const Icon(Icons.music_note, size: 30),
),
),
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.hardEdge,
child: Obx(() {
final currentSong = audioController.songList.isEmpty
? null
: audioController.songList[
audioController.currentSongIndex.value];
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: currentSong != null
? Image.network(
currentSong.pic,
key: ValueKey(currentSong.pic),
width: 48,
height: 48,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
const Icon(Icons.music_note, size: 30),
)
: const Icon(Icons.music_note, size: 30),
);
}),
),
),
),
),
// 歌曲信息
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
Expanded(
child: Obx(() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
audioController.musicName.value == ''
? '喵听音乐'
: audioController.musicName.value,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
letterSpacing: -0.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 3),
Text(
audioController.artistName.value == ''
? '听你想听'
: audioController.artistName.value,
style: TextStyle(
fontSize: 13,
color: Colors.grey[600],
letterSpacing: -0.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
)),
),
// 播放控制
Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildPlayButton(),
const SizedBox(width: 12),
Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => _showPlaylist(context),
child: Container(
width: 42,
height: 42,
padding: const EdgeInsets.all(8),
child: ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.grey[700]!,
BlendMode.srcIn,
),
child: Image.asset(
"assets/img/music_list.png",
width: 22,
height: 22,
),
),
),
),
),
const SizedBox(width: 8),
],
),
],
),
),
),
],
),
),
);
});
}
}
class MainTabView extends StatefulWidget {
const MainTabView({super.key});
@override
State<MainTabView> createState() => _MainTabViewState();
}
class _MainTabViewState extends State<MainTabView>
with SingleTickerProviderStateMixin {
TabController? controller;
int selectTab = 0;
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
DateTime? _lastPressedAt; // 记录上次点击返回时间
@override
void initState() {
super.initState();
controller = TabController(length: 5, vsync: this);
controller?.addListener(() {
selectTab = controller?.index ?? 0;
setState(() {});
});
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (selectTab != 0) {
controller?.animateTo(0);
return false;
}
if (_lastPressedAt == null ||
DateTime.now().difference(_lastPressedAt!) >
const Duration(seconds: 2)) {
_lastPressedAt = DateTime.now();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text(
'再按一次退出程序',
style: TextStyle(color: Colors.black87),
),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.white,
// 改成白色背景
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).size.height * 0.1, // 调整位置
left: 125, // 减小宽度
right: 125, // 减小宽度
),
shape: RoundedRectangleBorder(
// 添加圆角
borderRadius: BorderRadius.circular(20),
),
elevation: 6,
// 增加阴影
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
), // 调整内边距
),
);
return false;
}
SystemNavigator.pop();
return false;
},
child: Scaffold(
resizeToAvoidBottomInset: false,
key: scaffoldKey,
body: Stack(
children: [
Column(
children: [
Expanded(
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),
controller: controller,
children: const [
HomeView(),
RankView(),
SongRecommendationView(),
ReleaseView(),
UserView()
],
),
),
],
),
// 底部迷你播放器和导航栏
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
MiniPlayer(),
Container(
color: Colors.white,
child: TabBar(
controller: controller,
indicatorColor: Colors.transparent,
labelColor: Colors.black,
labelStyle: const TextStyle(fontSize: 12),
unselectedLabelColor: const Color(0xffCDCDCD),
unselectedLabelStyle: const TextStyle(fontSize: 12),
tabs: [
Tab(
height: 60,
icon: Image.asset(
selectTab == 0
? "assets/img/home_tab.png"
: "assets/img/home_tab_un.png",
width: 32,
height: 32,
),
text: "首页",
),
Tab(
height: 60,
icon: Image.asset(
selectTab == 1
? "assets/img/list_tab.png"
: "assets/img/list_tab_un.png",
width: 32,
height: 32,
),
text: "排行榜",
),
Tab(
height: 60,
icon: Image.asset(
selectTab == 2
? "assets/img/star_tab.png"
: "assets/img/star_tab_un.png",
width: 32,
height: 32,
),
text: "知音",
),
Tab(
height: 60,
icon: Image.asset(
selectTab == 3
? "assets/img/music_tab.png"
: "assets/img/music_tab_un.png",
width: 32,
height: 32,
),
text: "发布",
),
Tab(
height: 60,
icon: Image.asset(
selectTab == 4
? "assets/img/user_tab.png"
: "assets/img/user_tab_un.png",
width: 32,
height: 32,
),
text: "我的",
),
],
),
),
],
),
),
],
),
),
);
}
}