源代码地址:
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() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
MusicPlayerService.MusicBinder binder = (MusicPlayerService.MusicBinder) service;
musicService = binder.getService();
isBound = true;
// 服务连接后更新UI
updatePlayerControls();
startProgressTimer();
}
@Override
public void onServiceDisconnected(ComponentName name) {
isBound = false;
stopProgressTimer();
}
};
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleMusicControlIntent(intent);
}
@Override
protected 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();
}
@Override
protected void onStart() {
super.onStart();
if (!isBound) {
bindMusicService();
}
// 注册媒体观察者
mediaScanner.registerMediaObserver();
}
@Override
protected void onStop() {
super.onStop();
if (isBound) {
unbindService(serviceConnection);
isBound = false;
}
stopProgressTimer();
// 注销媒体观察者
mediaScanner.unregisterMediaObserver();
}
@Override
protected void onDestroy() {
super.onDestroy();
stopProgressTimer();
}
@Override
protected 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() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
bottomNav.getMenu().getItem(position).setChecked(true);
}
@Override
public 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() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser && isBound && musicService != null) {
musicService.seekTo(progress);
updateCurrentTimeText(progress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public 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...");
}
}
@Override
public 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_STORAGE
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
} else {
// Android 10及以下,使用APPLICATION_DETAILS_SETTINGS
Intent 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() {
@Override
public 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
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
@Override
public int getCount() {
return fragmentList.size();
}
public void addFragment(Fragment fragment, String title) {
fragmentList.add(fragment);
fragmentTitleList.add(title);
}
@Override
public 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);
}
@Override
public 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) {
// 如果返回栈即将清空,显示ViewPager
viewPager.setVisibility(View.VISIBLE);
fragmentContainer.setVisibility(View.GONE);
}
}
} else {
super.onBackPressed();
}
}
// 添加标志位,记录是否已经检查过权限
private boolean hasCheckedPermissions = false;
}
其余部分代码已经全部开源。
这是我开源的第一个小项目,同时也是接单的第一单。
开发工具:Android Studio、Navicat Premium 17、IntelliJ IDEA、Cursor