VibePlayer
源代码地址:
VibePlayer: VibePlayer是一款功能强大的Android音乐播放器应用,专为音乐爱好者设计,提供了丰富的音乐播放和管理功能。
用户需求
VibePlayer是一款功能强大的Android音乐播放器应用,专为音乐爱好者设计,提供了丰富的音乐播放和管理功能。
功能特点
音乐播放
- 支持多种播放模式:顺序播放、单曲循环、列表循环、随机播放
- 前台服务保证后台播放稳定运行
音乐库管理
- 自动扫描设备中的音乐文件
- 支持文件浏览器手动导入音乐
- 播放列表创建和管理
音频处理
- 内置均衡器,支持多种预设
- 低音增强和虚拟环绕声效果
- 支持音频可视化
歌词功能
- 歌词编辑器,支持添加时间戳
- 歌词实时同步显示
- 歌词预览功能
用户界面
- 现代化UI设计
- 支持深色/浅色主题切换
- 响应式布局,适配不同尺寸设备
- 直观的播放控制界面
权限说明
应用需要以下权限才能正常工作:
- 读取媒体音频(Android 13及以上)
- 读取外部存储(Android 13以下)
- 前台服务
- 通知权限(可选)
- 媒体内容控制
系统要求
- Android 5.0 (API级别21)或更高版本
- 建议安装在Android 8.0或更高版本上获得最佳体验
使用指南
首次使用
- 启动应用后,系统会请求必要的权限
- 授予权限后,应用会自动扫描设备中的音乐文件
- 若未找到音乐文件,可使用内置的文件浏览器导入音乐
播放控制
- 底部控制栏提供基本播放控制
- 点击正在播放的歌曲可进入全屏播放界面
- 左右滑动可切换歌曲
- 长按歌曲可查看更多选项
播放列表管理
- 点击"+"按钮创建新播放列表
- 长按歌曲可添加到播放列表
- 在播放列表详情页可管理列表内歌曲
均衡器设置
- 在均衡器页面可启用/禁用音效处理
- 选择预设或自定义均衡器设置
- 调整低音增强和虚拟环绕声效果
歌词编辑
- 在全屏播放界面点击歌词编辑按钮
- 输入歌词内容,每行一句
- 播放歌曲,在适当的时间点点击"添加时间戳"
- 保存歌词后可在播放界面同步显示
技术特点
- 使用MediaPlayer和MediaSession管理音乐播放
- 支持MediaBrowserService实现跨组件媒体控制
- 采用Room数据库存储播放列表和歌曲信息
- 利用Fragment和ViewPager实现多页面导航
- 前台服务确保后台播放稳定性
- 适配Android不同版本的权限处理
MainActivity.java:
package com.vibeplayer.app;import android.Manifest;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.viewpager.widget.ViewPager;import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.vibeplayer.app.fragment.NowPlayingFragment;
import com.vibeplayer.app.fragment.PlaylistsFragment;
import com.vibeplayer.app.fragment.SettingsFragment;
import com.vibeplayer.app.fragment.SongsFragment;
import com.vibeplayer.app.fragment.EqualizerFragment;
import com.vibeplayer.app.model.Song;
import com.vibeplayer.app.service.MusicPlayerService;
import com.vibeplayer.app.util.MediaScanner;import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;public class MainActivity extends AppCompatActivity {private static final int PERMISSION_REQUEST_STORAGE = 1;private static final int PERMISSION_REQUEST_NOTIFICATION = 2;private ViewPager viewPager;private BottomNavigationView bottomNav;// 底部播放控制栏组件private View playerControlLayout;private ImageView btnPlayPause;private ImageView btnNext;private ImageView btnPrevious;private TextView txtSongTitle;private TextView txtArtist;private SeekBar seekBar;private TextView txtCurrentTime;private TextView txtTotalTime;private MusicPlayerService musicService;private boolean isBound = false;private MediaScanner mediaScanner;private Timer timer;// 服务连接private ServiceConnection serviceConnection = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {MusicPlayerService.MusicBinder binder = (MusicPlayerService.MusicBinder) service;musicService = binder.getService();isBound = true;// 服务连接后更新UIupdatePlayerControls();startProgressTimer();}@Overridepublic void onServiceDisconnected(ComponentName name) {isBound = false;stopProgressTimer();}};@Overrideprotected void onNewIntent(Intent intent) {super.onNewIntent(intent);handleMusicControlIntent(intent);}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 处理可能的音乐控制动作handleMusicControlIntent(getIntent());// 初始化媒体扫描器mediaScanner = new MediaScanner(this);// 设置扫描完成监听器mediaScanner.setScanCompletedListener(songs -> {Log.d("MainActivity", "Auto scan completed, found " + songs.size() + " songs");if (isBound && musicService != null) {musicService.setSongs(songs);runOnUiThread(() -> {if (!songs.isEmpty()) {// 显示播放控制栏playerControlLayout.setVisibility(View.VISIBLE);} else {// 没有找到音乐文件Toast.makeText(this, "未找到音乐文件", Toast.LENGTH_SHORT).show();}});}});// 检查权限checkPermissions();// 初始化视图initializeViews();// 设置ViewPager适配器setupViewPager();// 设置底部导航setupBottomNavigation();// 设置播放控制栏setupPlayerControls();// 绑定音乐服务bindMusicService();}@Overrideprotected void onStart() {super.onStart();if (!isBound) {bindMusicService();}// 注册媒体观察者mediaScanner.registerMediaObserver();}@Overrideprotected void onStop() {super.onStop();if (isBound) {unbindService(serviceConnection);isBound = false;}stopProgressTimer();// 注销媒体观察者mediaScanner.unregisterMediaObserver();}@Overrideprotected void onDestroy() {super.onDestroy();stopProgressTimer();}@Overrideprotected void onResume() {super.onResume();// 检查权限状态boolean hasPermission = false;if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {hasPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_AUDIO)== PackageManager.PERMISSION_GRANTED;} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {hasPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)== PackageManager.PERMISSION_GRANTED;} else {hasPermission = true;}// 如果已经有权限,直接加载音乐if (hasPermission) {Log.d("MainActivity", "Permission already granted in onResume, loading songs...");loadSongs();}// 如果没有权限且还没有检查过权限,则进行权限检查else if (!hasCheckedPermissions) {Log.d("MainActivity", "No permission in onResume, checking permissions...");checkPermissions();}}private void initializeViews() {viewPager = findViewById(R.id.viewPager);bottomNav = findViewById(R.id.bottomNav);playerControlLayout = findViewById(R.id.playerControlLayout);btnPlayPause = findViewById(R.id.btnPlayPause);btnNext = findViewById(R.id.btnNext);btnPrevious = findViewById(R.id.btnPrevious);txtSongTitle = findViewById(R.id.txtSongTitle);txtArtist = findViewById(R.id.txtArtist);seekBar = findViewById(R.id.seekBar);txtCurrentTime = findViewById(R.id.txtCurrentTime);txtTotalTime = findViewById(R.id.txtTotalTime);}private void setupViewPager() {ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager());adapter.addFragment(new SongsFragment(), "歌曲");adapter.addFragment(new PlaylistsFragment(), "播放列表");adapter.addFragment(new EqualizerFragment(), "均衡器");adapter.addFragment(new SettingsFragment(), "设置");viewPager.setAdapter(adapter);viewPager.setCurrentItem(0); // 默认显示歌曲页面// 设置页面切换监听viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {@Overridepublic void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}@Overridepublic void onPageSelected(int position) {bottomNav.getMenu().getItem(position).setChecked(true);}@Overridepublic void onPageScrollStateChanged(int state) {}});}private void setupBottomNavigation() {bottomNav.setOnNavigationItemSelectedListener(item -> {int itemId = item.getItemId();if (itemId == R.id.nav_songs) {viewPager.setCurrentItem(0);return true;} else if (itemId == R.id.nav_playlists) {viewPager.setCurrentItem(1);return true;} else if (itemId == R.id.nav_equalizer) {viewPager.setCurrentItem(2);return true;} else if (itemId == R.id.nav_settings) {viewPager.setCurrentItem(3);return true;}return false;});}private void setupPlayerControls() {// 播放/暂停按钮点击事件btnPlayPause.setOnClickListener(v -> {if (isBound && musicService != null) {musicService.playPause();updatePlayPauseButton();}});// 下一曲按钮点击事件btnNext.setOnClickListener(v -> {if (isBound && musicService != null) {musicService.playNext();}});// 上一曲按钮点击事件btnPrevious.setOnClickListener(v -> {if (isBound && musicService != null) {musicService.playPrevious();}});// 进度条拖动事件seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {if (fromUser && isBound && musicService != null) {musicService.seekTo(progress);updateCurrentTimeText(progress);}}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {}});// 点击播放控制栏打开全屏播放界面playerControlLayout.setOnClickListener(v -> {if (isBound && musicService != null && musicService.getCurrentSong() != null) {NowPlayingFragment nowPlayingFragment = NowPlayingFragment.newInstance();nowPlayingFragment.show(getSupportFragmentManager(), "now_playing");}});}private void bindMusicService() {Intent intent = new Intent(this, MusicPlayerService.class);startService(intent);bindService(intent, serviceConnection, BIND_AUTO_CREATE);}private void checkPermissions() {hasCheckedPermissions = true; // 标记已经检查过权限// Android 6.0 (API 23)以下版本不需要动态请求权限if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {loadSongs();return;}// 检查是否已经有权限boolean hasPermission = false;if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {// Android 13及以上版本检查READ_MEDIA_AUDIO权限hasPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_AUDIO)== PackageManager.PERMISSION_GRANTED;if (!hasPermission) {// 请求音频权限ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_MEDIA_AUDIO},PERMISSION_REQUEST_STORAGE);}} else {// Android 13以下版本检查READ_EXTERNAL_STORAGE权限hasPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)== PackageManager.PERMISSION_GRANTED;if (!hasPermission) {// 请求存储权限ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},PERMISSION_REQUEST_STORAGE);}}// 如果已经有权限,直接加载音乐if (hasPermission) {Log.d("MainActivity", "Permission already granted in checkPermissions, loading songs...");loadSongs();} else {Log.d("MainActivity", "Requesting permissions...");}}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);if (grantResults.length == 0) {Log.d("MainActivity", "Permission request cancelled");return;}switch (requestCode) {case PERMISSION_REQUEST_STORAGE:if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {Log.d("MainActivity", "Permission granted in onRequestPermissionsResult, loading songs...");loadSongs();} else {Log.d("MainActivity", "Permission denied");// 用户拒绝了权限if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&!shouldShowRequestPermissionRationale(permissions[0])) {// 用户选择了"不再询问"showPermissionSettingsDialog();} else {showRetryDialog();}}break;case PERMISSION_REQUEST_NOTIFICATION:if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {// 通知权限已授权,继续检查其他权限checkPermissions();} else {// 通知权限被拒绝,但这不是必需的,所以继续加载音乐loadSongs();}break;}}private void showRetryDialog() {new AlertDialog.Builder(this).setTitle("权限请求").setMessage("没有存储权限,应用将无法访问音乐文件。是否重新请求权限?").setPositiveButton("重试", (dialog, which) -> checkPermissions()).setNegativeButton("退出", (dialog, which) -> finish()).setCancelable(false).show();}private void showPermissionSettingsDialog() {String message;if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {message = "应用需要访问音频文件的权限才能播放音乐。\n\n" +"操作步骤:\n" +"1. 点击\"立即开启\"\n" +"2. 找到\"音频文件访问权限\"\n" +"3. 点击开关开启权限";} else {message = "应用需要存储权限才能播放音乐。\n\n" +"操作步骤:\n" +"1. 点击\"立即开启\"\n" +"2. 找到\"存储空间\"\n" +"3. 点击开关开启权限";}new AlertDialog.Builder(this).setTitle("需要开启权限").setMessage(message).setPositiveButton("立即开启", (dialog, which) -> {try {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {// Android 13及以上,尝试直接跳转到媒体权限设置Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);Uri uri = Uri.fromParts("package", getPackageName(), null);intent.setData(uri);// 尝试直接打开权限页面intent.putExtra(":settings:fragment_args_key", "permission");intent.putExtra(":settings:show_fragment_args", true);Bundle bundle = new Bundle();bundle.putString(":settings:fragment_args_key", "permission");intent.putExtra("android.provider.extra.APP_PACKAGE", getPackageName());startActivity(intent);} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {// Android 11及以上,使用MANAGE_EXTERNAL_STORAGEIntent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);intent.setData(Uri.parse("package:" + getPackageName()));startActivity(intent);} else {// Android 10及以下,使用APPLICATION_DETAILS_SETTINGSIntent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);intent.setData(Uri.parse("package:" + getPackageName()));intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);intent.addCategory(Intent.CATEGORY_DEFAULT);intent.putExtra("android.provider.extra.APP_PACKAGE", getPackageName());startActivity(intent);}} catch (Exception e) {// 如果特定跳转失败,回退到通用设置页面try {Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);Uri uri = Uri.fromParts("package", getPackageName(), null);intent.setData(uri);startActivity(intent);} catch (Exception e2) {// 如果还是失败,使用最基本的设置页面Intent intent = new Intent(Settings.ACTION_SETTINGS);startActivity(intent);Toast.makeText(this, "请在设置中找到本应用并开启所需权限", Toast.LENGTH_LONG).show();}}}).setNegativeButton("退出应用", (dialog, which) -> finish()).setCancelable(false).show();}private void loadSongs() {Log.d("MainActivity", "Starting to load songs...");// 再次确认权限boolean hasPermission = false;if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {hasPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_AUDIO)== PackageManager.PERMISSION_GRANTED;} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {hasPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)== PackageManager.PERMISSION_GRANTED;} else {hasPermission = true; // Android 6.0以下版本}if (!hasPermission) {Log.d("MainActivity", "Permission not granted when trying to load songs");if (!hasCheckedPermissions) {checkPermissions();}return;}// 使用增强版的异步扫描方法mediaScanner.scanMediaAsync();}private void updatePlayerControls() {if (!isBound || musicService == null) {playerControlLayout.setVisibility(View.GONE);return;}Song currentSong = musicService.getCurrentSong();if (currentSong != null) {playerControlLayout.setVisibility(View.VISIBLE);txtSongTitle.setText(currentSong.getTitle());txtArtist.setText(currentSong.getArtist());updatePlayPauseButton();int duration = musicService.getDuration();seekBar.setMax(duration);txtTotalTime.setText(formatTime(duration));updateSeekBar();} else {playerControlLayout.setVisibility(View.GONE);}}private void updatePlayPauseButton() {if (isBound && musicService != null && musicService.isPlaying()) {btnPlayPause.setImageResource(R.drawable.ic_pause);} else {btnPlayPause.setImageResource(R.drawable.ic_play);}}private void updateSeekBar() {if (isBound && musicService != null && musicService.isPrepared()) {int currentPosition = musicService.getCurrentPosition();seekBar.setProgress(currentPosition);updateCurrentTimeText(currentPosition);}}private void updateCurrentTimeText(int currentPosition) {txtCurrentTime.setText(formatTime(currentPosition));}private void startProgressTimer() {stopProgressTimer();timer = new Timer();timer.scheduleAtFixedRate(new TimerTask() {@Overridepublic void run() {runOnUiThread(() -> {if (isBound && musicService != null && musicService.isPlaying()) {updateSeekBar();updatePlayPauseButton();}});}}, 0, 1000);}private void stopProgressTimer() {if (timer != null) {timer.cancel();timer = null;}}private String formatTime(int milliseconds) {int seconds = (milliseconds / 1000) % 60;int minutes = (milliseconds / (1000 * 60)) % 60;return String.format("%02d:%02d", minutes, seconds);}// ViewPager适配器private static class ViewPagerAdapter extends FragmentPagerAdapter {private final List<Fragment> fragmentList = new ArrayList<>();private final List<String> fragmentTitleList = new ArrayList<>();public ViewPagerAdapter(FragmentManager manager) {super(manager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);}@NonNull@Overridepublic Fragment getItem(int position) {return fragmentList.get(position);}@Overridepublic int getCount() {return fragmentList.size();}public void addFragment(Fragment fragment, String title) {fragmentList.add(fragment);fragmentTitleList.add(title);}@Overridepublic CharSequence getPageTitle(int position) {return fragmentTitleList.get(position);}}// 公开方法,供Fragment调用public void playSong(int position) {if (isBound && musicService != null) {musicService.playSong(position);updatePlayerControls();}}public MusicPlayerService getMusicService() {return musicService;}public boolean isServiceBound() {return isBound;}private void handleMusicControlIntent(Intent intent) {if (intent == null || intent.getAction() == null) {return;}String action = intent.getAction();Intent broadcastIntent = new Intent(action);LocalBroadcastManager.getInstance(this).sendBroadcast(broadcastIntent);}@Overridepublic void onBackPressed() {View viewPager = findViewById(R.id.viewPager);View fragmentContainer = findViewById(R.id.fragmentContainer);if (fragmentContainer.getVisibility() == View.VISIBLE) {// 如果Fragment容器可见,先处理Fragment的返回栈if (getSupportFragmentManager().getBackStackEntryCount() > 0) {getSupportFragmentManager().popBackStack();// 检查返回栈是否为空if (getSupportFragmentManager().getBackStackEntryCount() == 1) {// 如果返回栈即将清空,显示ViewPagerviewPager.setVisibility(View.VISIBLE);fragmentContainer.setVisibility(View.GONE);}}} else {super.onBackPressed();}}// 添加标志位,记录是否已经检查过权限private boolean hasCheckedPermissions = false;
}
其余部分代码已经全部开源。
这是我开源的第一个小项目,同时也是接单的第一单。
开发工具:Android Studio、Navicat Premium 17、IntelliJ IDEA、Cursor