当前位置: 首页 > news >正文

VibePlayer

源代码地址:

VibePlayer: VibePlayer是一款功能强大的Android音乐播放器应用,专为音乐爱好者设计,提供了丰富的音乐播放和管理功能。

用户需求 

VibePlayer是一款功能强大的Android音乐播放器应用,专为音乐爱好者设计,提供了丰富的音乐播放和管理功能。

功能特点

音乐播放

  • 支持多种播放模式:顺序播放、单曲循环、列表循环、随机播放
  • 前台服务保证后台播放稳定运行

音乐库管理

  • 自动扫描设备中的音乐文件
  • 支持文件浏览器手动导入音乐
  • 播放列表创建和管理

音频处理

  • 内置均衡器,支持多种预设
  • 低音增强和虚拟环绕声效果
  • 支持音频可视化

歌词功能

  • 歌词编辑器,支持添加时间戳
  • 歌词实时同步显示
  • 歌词预览功能

用户界面

  • 现代化UI设计
  • 支持深色/浅色主题切换
  • 响应式布局,适配不同尺寸设备
  • 直观的播放控制界面

权限说明

应用需要以下权限才能正常工作:

  • 读取媒体音频(Android 13及以上)
  • 读取外部存储(Android 13以下)
  • 前台服务
  • 通知权限(可选)
  • 媒体内容控制

系统要求

  • Android 5.0 (API级别21)或更高版本
  • 建议安装在Android 8.0或更高版本上获得最佳体验

使用指南

首次使用

  1. 启动应用后,系统会请求必要的权限
  2. 授予权限后,应用会自动扫描设备中的音乐文件
  3. 若未找到音乐文件,可使用内置的文件浏览器导入音乐

播放控制

  • 底部控制栏提供基本播放控制
  • 点击正在播放的歌曲可进入全屏播放界面
  • 左右滑动可切换歌曲
  • 长按歌曲可查看更多选项

播放列表管理

  • 点击"+"按钮创建新播放列表
  • 长按歌曲可添加到播放列表
  • 在播放列表详情页可管理列表内歌曲

均衡器设置

  • 在均衡器页面可启用/禁用音效处理
  • 选择预设或自定义均衡器设置
  • 调整低音增强和虚拟环绕声效果

歌词编辑

  1. 在全屏播放界面点击歌词编辑按钮
  2. 输入歌词内容,每行一句
  3. 播放歌曲,在适当的时间点点击"添加时间戳"
  4. 保存歌词后可在播放界面同步显示

技术特点

  • 使用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

相关文章:

  • 分类场景数据集大全「包含数据标注+训练脚本」 (持续原地更新)
  • 隐函数 因变量确定标准
  • Java编程之组合模式
  • 深度学习登上Nature子刊!特征选择创新思路
  • Spring 中的三级缓存机制详解
  • 二叉数-100.相同的树-力扣(LeetCode)
  • 2025年U盘数据恢复软件推荐:找回丢失文件的得力助手
  • 人脸识别技术应用备案办理指南
  • protues仿真+C51+外部中断
  • triton学习笔记7: GEMM相关
  • SDC命令详解:使用set_max_area命令进行约束
  • Linux 环境配置
  • Java后端检查空条件查询
  • linux库(AI回答)
  • 算法打卡第18天
  • Java求职者面试指南:计算机基础与源码原理深度解析
  • 2000-2020年各省第三产业增加值占GDP比重数据
  • ffmpeg(四):滤镜命令
  • VS Code扩展安装后如何管理
  • 循环变量捕获问题​​
  • 乐亭网站建设/雅思培训机构哪家好机构排名
  • 自己做一个网站一年的费用/推广方案如何写
  • mugeda做网站/优就业seo课程学多久
  • 网站建设方式有哪些/今日国内新闻头条
  • 做快递网站制作/网站优化靠谱seo
  • 网站建设嘉兴公司电话/青岛seo建站