diff --git a/app/src/main/java/com/example/musicplayer/view/MainActivity.java b/app/src/main/java/com/example/musicplayer/view/MainActivity.java index 652a858..6010fa1 100644 --- a/app/src/main/java/com/example/musicplayer/view/MainActivity.java +++ b/app/src/main/java/com/example/musicplayer/view/MainActivity.java @@ -1,6 +1,5 @@ package com.example.musicplayer.view; - import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.app.ActivityOptions; @@ -53,218 +52,287 @@ import org.litepal.LitePal; import butterknife.BindView; import de.hdodenhof.circleimageview.CircleImageView; +// MainActivity类继承自BaseActivity,是音乐播放器应用的主界面类,负责处理界面展示、用户交互以及与音乐播放相关服务的交互等功能 public class MainActivity extends BaseActivity { private static final String TAG = "MainActivity"; + // 通过ButterKnife框架绑定SeekBar控件,用于显示音乐播放进度 @BindView(R.id.sb_progress) SeekBar mSeekBar; + // 通过ButterKnife框架绑定TextView控件,用于显示歌曲名称 @BindView(R.id.tv_song_name) TextView mSongNameTv; + // 通过ButterKnife框架绑定TextView控件,用于显示歌手名称 @BindView(R.id.tv_singer) TextView mSingerTv; + // 通过ButterKnife框架绑定RippleView控件,点击可切换到下一首歌曲 @BindView(R.id.song_next) RippleView mNextIv; + // 通过ButterKnife框架绑定Button控件,用于控制音乐的播放/暂停 @BindView(R.id.btn_player) Button mPlayerBtn; + // 通过ButterKnife框架绑定CircleImageView控件,用于显示歌曲封面图片 @BindView(R.id.circle_img) CircleImageView mCoverIv; + // 通过ButterKnife框架绑定LinearLayout控件,可能是包含播放相关控件的布局容器 @BindView(R.id.linear_player) LinearLayout mLinear; - private boolean isChange; //拖动进度条 - private boolean isSeek;//标记是否在暂停的时候拖动进度条 - private boolean flag; //用做暂停的标记 - private int time; //记录暂停的时间 - - private boolean isExistService;//服务是否存活 - - - private ObjectAnimator mCircleAnimator;//动画 + // 标记是否正在拖动进度条,用于防止进度条更新冲突 + private boolean isChange; + // 标记是否在暂停的时候拖动进度条,用于后续恢复播放时正确设置播放位置 + private boolean isSeek; + // 用作暂停的标记,用于区分不同的播放/暂停操作场景 + private boolean flag; + // 记录暂停的时间,单位可能是毫秒,用于恢复播放时定位播放位置 + private int time; + + // 标记音乐播放相关服务是否存活,用于判断一些操作是否可行 + private boolean isExistService; + + // 属性动画对象,用于实现歌曲封面图片的旋转动画效果 + private ObjectAnimator mCircleAnimator; + // 当前播放的歌曲对象,包含歌曲的相关信息如名称、歌手、时长等 private Song mSong; + // MediaPlayer对象,用于实际的音频播放控制,不过这里看起来主要通过服务中的MediaPlayer来操作 private MediaPlayer mMediaPlayer; + // 线程对象,用于在后台更新SeekBar的进度显示 private Thread mSeekBarThread; + // 用于获取PlayerService中的播放状态相关信息和操作方法的Binder对象 private PlayerService.PlayStatusBinder mPlayStatusBinder; + // 用于获取DownloadService中的下载相关操作方法的Binder对象(这里未看到详细使用) private DownloadService.DownloadBinder mDownloadBinder; + // 服务连接对象,用于与PlayerService建立连接并获取服务端的Binder对象 private ServiceConnection connection = new ServiceConnection() { + // 当与服务成功连接时调用,获取服务端传递过来的Binder对象 @Override public void onServiceConnected(ComponentName name, IBinder service) { mPlayStatusBinder = (PlayerService.PlayStatusBinder) service; } + // 当与服务意外断开连接时调用(比如服务崩溃),这里暂时没做具体处理 @Override public void onServiceDisconnected(ComponentName name) { - - } }; - //绑定下载服务 + // 下载服务的服务连接对象,用于与DownloadService建立连接并获取服务端的Binder对象 private ServiceConnection mDownloadConnection = new ServiceConnection() { + // 当与下载服务成功连接时调用,获取服务端传递过来的Binder对象 @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { mDownloadBinder = (DownloadService.DownloadBinder) iBinder; - if(isExistService) seekBarStart(); + // 如果服务存在,启动SeekBar进度更新 + if (isExistService) seekBarStart(); } + // 当与下载服务意外断开连接时调用(比如服务崩溃),这里暂时没做具体处理 @Override public void onServiceDisconnected(ComponentName componentName) { } }; + // 重写onDestroy方法,在Activity销毁时执行清理和保存相关操作 @Override public void onDestroy() { super.onDestroy(); - //解绑 + // 解除与PlayerService的绑定 unbindService(connection); + // 解除与DownloadService的绑定 unbindService(mDownloadConnection); - //将播放的服务提升至前台服务 + // 创建一个启动PlayerService的Intent,目的是将播放的服务提升至前台服务(针对Android 8.0及以上系统的处理) Intent playIntent = new Intent(MainActivity.this, PlayerService.class); - //Android 8.0以上 + // Android 8.0以上,以前台服务的方式启动服务 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(playIntent); } else { + // Android 8.0以下,普通启动服务 startService(playIntent); } - + // 取消在EventBus上的注册,避免内存泄漏等问题 EventBus.getDefault().unregister(this); - if (mSeekBarThread != null || mSeekBarThread.isAlive()) mSeekBarThread.interrupt(); + // 如果SeekBar线程存在且正在运行,中断该线程 + if (mSeekBarThread!= null || mSeekBarThread.isAlive()) mSeekBarThread.interrupt(); + // 获取当前播放的歌曲对象 Song song = FileUtil.getSong(); + // 将歌曲当前播放时间设置为从服务获取到的当前时间,保存歌曲信息 song.setCurrentTime(mPlayStatusBinder.getCurrentTime()); FileUtil.saveSong(song); - - } - + // 获取Activity对应的布局资源ID,这里返回activity_main布局的ID @Override protected int getLayoutId() { return R.layout.activity_main; } + // 初始化视图相关操作,在Activity创建时调用 @RequiresApi(api = Build.VERSION_CODES.O) @Override protected void initView() { + // 在EventBus上注册,以便接收事件通知 EventBus.getDefault().register(this); + // 初始化LitePal数据库(可能用于数据持久化等操作,比如保存歌曲相关信息) LitePal.getDatabase(); - - //设置属性动画 + // 创建属性动画,设置CircleImageView(歌曲封面图片)的旋转动画,从0度旋转到360度 mCircleAnimator = ObjectAnimator.ofFloat(mCoverIv, "rotation", 0.0f, 360.0f); + // 设置动画持续时间为30秒 mCircleAnimator.setDuration(30000); + // 设置线性插值器,使动画匀速进行 mCircleAnimator.setInterpolator(new LinearInterpolator()); + // 设置动画重复次数为无限次 mCircleAnimator.setRepeatCount(-1); + // 设置动画重复模式为重新开始 mCircleAnimator.setRepeatMode(ValueAnimator.RESTART); - + // 获取当前播放的歌曲对象 mSong = FileUtil.getSong(); - if (mSong.getSongName() != null) { + // 如果歌曲名称不为空,说明有正在播放的歌曲,进行相关界面设置 + if (mSong.getSongName()!= null) { Log.d(TAG, "initView: " + mSong.toString()); + // 显示包含播放相关控件的布局容器 mLinear.setVisibility(View.VISIBLE); + // 设置TextView显示歌曲名称 mSongNameTv.setText(mSong.getSongName()); + // 设置TextView显示歌手名称 mSingerTv.setText(mSong.getSinger()); + // 设置SeekBar的最大进度为歌曲的总时长(单位可能需要转换,这里看起来是以秒为单位存储时长) mSeekBar.setMax((int) mSong.getDuration()); + // 设置SeekBar的当前进度为歌曲的当前播放时间 mSeekBar.setProgress((int) mSong.getCurrentTime()); + // 如果歌曲封面图片的URL为空,通过CommonUtil工具类根据歌手名称设置默认图片 if (mSong.getImgUrl() == null) { CommonUtil.setSingerImg(MainActivity.this, mSong.getSinger(), mCoverIv); } else { + // 使用Glide库加载歌曲封面图片,设置占位图和加载失败时显示的图片 Glide.with(this) - .load(mSong.getImgUrl()) - .apply(RequestOptions.placeholderOf(R.drawable.welcome)) - .apply(RequestOptions.errorOf(R.drawable.welcome)) - .into(mCoverIv); + .load(mSong.getImgUrl()) + .apply(RequestOptions.placeholderOf(R.drawable.welcome)) + .apply(RequestOptions.errorOf(R.drawable.welcome)) + .into(mCoverIv); } } else { + // 如果没有正在播放的歌曲,设置默认的显示文本和图片 mSongNameTv.setText(getString(R.string.app_name)); mSingerTv.setText(getString(R.string.welcome_start)); mCoverIv.setImageResource(R.drawable.jay); } - //如果播放服务还存活 - if(ServiceUtil.isServiceRunning(this,PlayerService.class.getName())){ + // 如果PlayerService正在运行(服务存活) + if (ServiceUtil.isServiceRunning(this, PlayerService.class.getName())) { + // 设置播放按钮为选中状态(表示正在播放) mPlayerBtn.setSelected(true); + // 启动歌曲封面图片的旋转动画 mCircleAnimator.start(); + // 标记服务存活 isExistService = true; } - //处理服务 + // 初始化相关服务(启动并绑定服务) initService(); + // 添加主界面的Fragment到Activity中 addMainFragment(); } - private void initService(){ - //启动服务 + // 启动和绑定音乐播放相关服务(PlayerService和DownloadService) + private void initService() { + // 创建启动PlayerService的Intent Intent playIntent = new Intent(MainActivity.this, PlayerService.class); + // 创建启动DownloadService的Intent Intent downIntent = new Intent(MainActivity.this, DownloadService.class); - //退出程序后依然能播放 + // 如果是Android 8.0及以上系统,以前台服务的方式启动PlayerService if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(playIntent); - }else { + } else { + // Android 8.0以下,普通启动PlayerService startService(playIntent); } + // 绑定PlayerService,建立与服务的连接并获取Binder对象 bindService(playIntent, connection, Context.BIND_AUTO_CREATE); + // 绑定DownloadService,建立与服务的连接并获取Binder对象 bindService(downIntent, mDownloadConnection, Context.BIND_AUTO_CREATE); } + // 接收OnlineSongErrorEvent事件的方法,在主线程中处理,这里只是简单弹出一个版权相关的提示Toast @Subscribe(threadMode = ThreadMode.MAIN) public void onOnlineSongErrorEvent(OnlineSongErrorEvent event) { showToast(getString(R.string.error_out_of_copyright)); } + // 接收SongStatusEvent事件的方法,在主线程中处理,根据歌曲状态进行不同的界面和播放控制操作 @Subscribe(threadMode = ThreadMode.MAIN) public void onSongStatusEvent(SongStatusEvent event) { int status = event.getSongStatus(); + // 如果歌曲状态是恢复播放 if (status == Constant.SONG_RESUME) { + // 设置播放按钮为选中状态(表示正在播放) mPlayerBtn.setSelected(true); + // 恢复歌曲封面图片的旋转动画 mCircleAnimator.resume(); + // 启动SeekBar进度更新 seekBarStart(); } else if (status == Constant.SONG_PAUSE) { + // 设置播放按钮为未选中状态(表示暂停) mPlayerBtn.setSelected(false); + // 暂停歌曲封面图片的旋转动画 mCircleAnimator.pause(); } else if (status == Constant.SONG_CHANGE) { + // 获取当前播放的歌曲对象(可能歌曲切换了) mSong = FileUtil.getSong(); + // 更新TextView显示的歌曲名称 mSongNameTv.setText(mSong.getSongName()); + // 更新TextView显示的歌手名称 mSingerTv.setText(mSong.getSinger()); + // 设置SeekBar的最大进度为歌曲的总时长 mSeekBar.setMax((int) mSong.getDuration()); + // 设置播放按钮为选中状态(表示正在播放) mPlayerBtn.setSelected(true); + // 启动歌曲封面图片的旋转动画 mCircleAnimator.start(); + // 启动SeekBar进度更新 seekBarStart(); + // 如果歌曲不是在线歌曲(可能是本地歌曲),通过CommonUtil工具类根据歌手名称设置默认图片 if (!mSong.isOnline()) { CommonUtil.setSingerImg(MainActivity.this, mSong.getSinger(), mCoverIv); } else { + // 如果是在线歌曲,使用Glide库加载歌曲封面图片,设置占位图和加载失败时显示的图片 Glide.with(MainActivity.this) - .load(mSong.getImgUrl()) - .apply(RequestOptions.placeholderOf(R.drawable.welcome)) - .apply(RequestOptions.errorOf(R.drawable.welcome)) - .into(mCoverIv); + .load(mSong.getImgUrl()) + .apply(RequestOptions.placeholderOf(R.drawable.welcome)) + .apply(RequestOptions.errorOf(R.drawable.welcome)) + .into(mCoverIv); } } - } - + // 初始化数据相关操作,这里暂时没有具体实现内容 @Override protected void initData() { } + // 处理各种点击事件的方法 @Override protected void onClick() { - //进度条的监听事件 + // 为SeekBar设置进度变化监听器,用于处理进度条拖动等操作 mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + // 当进度改变时调用(拖动过程中会多次调用),这里暂时没做具体处理 @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { } + // 当开始拖动进度条时调用,设置标记防止与后台更新进度冲突 @Override public void onStartTrackingTouch(SeekBar seekBar) { - //防止在拖动进度条进行进度设置时与Thread更新播放进度条冲突 isChange = true; } + // 当停止拖动进度条时调用,根据播放状态设置新的播放位置,并重新启动SeekBar进度更新 @Override public void onStopTrackingTouch(SeekBar seekBar) { if (mPlayStatusBinder.isPlaying()) { @@ -278,7 +346,7 @@ public class MainActivity extends BaseActivity { } }); - //控制按钮,播放,暂停 + // 为播放/暂停按钮设置点击监听器,根据当前播放状态进行播放、暂停、恢复播放等操作 mPlayerBtn.setOnClickListener(v -> { mMediaPlayer = mPlayStatusBinder.getMediaPlayer(); if (mPlayStatusBinder.isPlaying()) { @@ -292,7 +360,8 @@ public class MainActivity extends BaseActivity { mMediaPlayer.seekTo(time * 1000); isSeek = false; } - } else {//退出程序后重新打开后的情况 + } else { + // 退出程序后重新打开后的情况,根据歌曲是否在线选择不同的播放方式 if (FileUtil.getSong().isOnline()) { mPlayStatusBinder.playOnline(); } else { @@ -302,71 +371,88 @@ public class MainActivity extends BaseActivity { mMediaPlayer.seekTo((int) mSong.getCurrentTime() * 1000); } }); - //下一首 - mNextIv.setOnClickListener(v -> { - if (FileUtil.getSong().getSongName() != null) mPlayStatusBinder.next(); - if (mPlayStatusBinder.isPlaying()) { - mPlayerBtn.setSelected(true); - } else { - mPlayerBtn.setSelected(false); - } - }); - //点击播放栏,跳转到播放的主界面 - mLinear.setOnClickListener(v -> { - if (FileUtil.getSong().getSongName() != null) { - Intent toPlayActivityIntent = new Intent(MainActivity.this, PlayActivity.class); + // 为下一首歌曲按钮设置点击监听器,点击时切换到下一首歌曲,并根据播放状态更新播放按钮状态 +mNextIv.setOnClickListener(v -> { + // 如果当前有正在播放的歌曲(歌曲名称不为空),则调用PlayerService的next方法切换到下一首歌曲 + if (FileUtil.getSong().getSongName()!= null) mPlayStatusBinder.next(); + // 如果PlayerService正在播放歌曲,设置播放按钮为选中状态(表示正在播放),否则设置为未选中状态(表示暂停) + if (mPlayStatusBinder.isPlaying()) { + mPlayerBtn.setSelected(true); + } else { + mPlayerBtn.setSelected(false); + } + }); - //播放情况 - if (mPlayStatusBinder.isPlaying()) { - Song song = FileUtil.getSong(); - song.setCurrentTime(mPlayStatusBinder.getCurrentTime()); - FileUtil.saveSong(song); - toPlayActivityIntent.putExtra(Constant.PLAYER_STATUS, Constant.SONG_PLAY); - } else { - //暂停情况 - Song song = FileUtil.getSong(); - song.setCurrentTime(mSeekBar.getProgress()); - FileUtil.saveSong(song); - } - if (FileUtil.getSong().getImgUrl() != null) { - toPlayActivityIntent.putExtra(SearchContentFragment.IS_ONLINE, true); - } - //如果版本大于21 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - startActivity(toPlayActivityIntent, - ActivityOptions.makeSceneTransitionAnimation(MainActivity.this).toBundle()); - } else { - startActivity(toPlayActivityIntent); - } + // 为包含播放相关控件的LinearLayout设置点击监听器,点击时跳转到播放的主界面(PlayActivity),并传递相关播放状态等信息 + mLinear.setOnClickListener(v -> { + // 如果当前有正在播放的歌曲(歌曲名称不为空),执行跳转操作 + if (FileUtil.getSong().getSongName()!= null) { + Intent toPlayActivityIntent = new Intent(MainActivity.this, PlayActivity.class); + // 如果PlayerService正在播放歌曲,获取当前歌曲对象,设置当前播放时间,并将播放状态设置为正在播放(Constant.SONG_PLAY)传递给目标Activity + if (mPlayStatusBinder.isPlaying()) { + Song song = FileUtil.getSong(); + song.setCurrentTime(mPlayStatusBinder.getCurrentTime()); + FileUtil.saveSong(song); + toPlayActivityIntent.putExtra(Constant.PLAYER_STATUS, Constant.SONG_PLAY); } else { - showToast(getString(R.string.welcome_start)); + // 如果歌曲处于暂停状态,获取当前歌曲对象,将SeekBar当前进度作为歌曲当前播放时间设置给歌曲对象,并保存 + Song song = FileUtil.getSong(); + song.setCurrentTime(mSeekBar.getProgress()); + FileUtil.saveSong(song); } - }); - } + // 如果歌曲封面图片的URL不为空,说明是在线歌曲,传递一个标识(SearchContentFragment.IS_ONLINE为true)给目标Activity + if (FileUtil.getSong().getImgUrl()!= null) { + toPlayActivityIntent.putExtra(SearchContentFragment.IS_ONLINE, true); + } + // 如果系统版本大于等于LOLLIPOP(Android 5.0),使用转场动画启动目标Activity + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + startActivity(toPlayActivityIntent, + ActivityOptions.makeSceneTransitionAnimation(MainActivity.this).toBundle()); + } else { + // 如果系统版本小于Android 5.0,直接启动目标Activity + startActivity(toPlayActivityIntent); + } + + } else { + // 如果没有正在播放的歌曲,弹出一个提示Toast(显示默认的欢迎开始之类的文本) + showToast(getString(R.string.welcome_start)); + } + }); +} + // 添加主界面的Fragment到Activity中的方法 private void addMainFragment() { + // 创建MainFragment实例 MainFragment mainFragment = new MainFragment(); + // 获取FragmentManager对象,用于管理Fragment事务 FragmentManager fragmentManager = getSupportFragmentManager(); + // 开启Fragment事务 FragmentTransaction transaction = fragmentManager.beginTransaction(); + // 将MainFragment添加到指定的布局容器(R.id.fragment_container)中 transaction.add(R.id.fragment_container, mainFragment); + // 提交Fragment事务,使添加操作生效 transaction.commit(); } - + // 启动SeekBar进度更新的方法,创建并启动一个线程来更新SeekBar的进度显示 private void seekBarStart() { mSeekBarThread = new Thread(new SeekBarThread()); mSeekBarThread.start(); } + // 实现了Runnable接口的内部类,用于在后台线程中更新SeekBar的进度显示 class SeekBarThread implements Runnable { @Override public void run() { - if (mPlayStatusBinder != null) { + // 只要没有正在拖动进度条且PlayerService正在播放歌曲,就持续更新SeekBar的进度 + if (mPlayStatusBinder!= null) { while (!isChange && mPlayStatusBinder.isPlaying()) { + // 设置SeekBar的进度为从PlayerService获取到的当前播放时间(单位可能需要和SeekBar的进度单位一致,这里看起来是以秒为单位) mSeekBar.setProgress((int) mPlayStatusBinder.getCurrentTime()); try { + // 线程休眠1秒,模拟每秒更新一次进度 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); @@ -375,4 +461,4 @@ public class MainActivity extends BaseActivity { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/view/PlayActivity.java b/app/src/main/java/com/example/musicplayer/view/PlayActivity.java index ca272c2..b9e9b6d 100644 --- a/app/src/main/java/com/example/musicplayer/view/PlayActivity.java +++ b/app/src/main/java/com/example/musicplayer/view/PlayActivity.java @@ -75,97 +75,141 @@ import butterknife.BindView; /** * 播放界面 + * 该类是音乐播放器应用中用于展示音乐播放相关界面以及处理各种播放相关操作、用户交互、与服务通信等功能的Activity类, + * 实现了IPlayContract.View接口,遵循MVP架构模式,与PlayPresenter协同工作来实现业务逻辑。 */ public class PlayActivity extends BaseMvpActivity implements IPlayContract.View { private final static String TAG = "PlayActivity"; + // 通过ButterKnife框架绑定TextView控件,用于显示歌曲名称 @BindView(R.id.tv_song) TextView mSongTv; + // 通过ButterKnife框架绑定ImageView控件,用于返回按钮,点击可返回上一界面 @BindView(R.id.iv_back) ImageView mBackIv; + // 通过ButterKnife框架绑定TextView控件,用于显示歌手名称 @BindView(R.id.tv_singer) TextView mSingerTv; + // 通过ButterKnife框架绑定Button控件,用于控制音乐的播放/暂停 @BindView(R.id.btn_player) Button mPlayBtn; + // 通过ButterKnife框架绑定Button控件,用于切换到上一首歌曲 @BindView(R.id.btn_last) Button mLastBtn; + // 通过ButterKnife框架绑定Button控件,用于切换播放模式(顺序、随机、单曲循环等) @BindView(R.id.btn_order) Button mPlayModeBtn; + // 通过ButterKnife框架绑定Button控件,用于切换到下一首歌曲 @BindView(R.id.next) Button mNextBtn; + // 通过ButterKnife框架绑定自定义的BackgroundAnimationRelativeLayout,可能用于背景相关动画效果展示等 @BindView(R.id.relative_root) BackgroundAnimationRelativeLayout mRootLayout; + // 通过ButterKnife框架绑定Button控件,用于标记歌曲是否为喜欢(收藏)状态,点击可切换收藏状态 @BindView(R.id.btn_love) Button mLoveBtn; + // 通过ButterKnife框架绑定SeekBar控件,用于显示音乐播放进度,并可拖动来调整播放位置 @BindView(R.id.seek) SeekBar mSeekBar; + // 通过ButterKnife框架绑定TextView控件,用于显示当前播放时间 @BindView(R.id.tv_current_time) TextView mCurrentTimeTv; + // 通过ButterKnife框架绑定TextView控件,用于显示歌曲总时长 @BindView(R.id.tv_duration_time) TextView mDurationTimeTv; + // 通过ButterKnife框架绑定DiscView自定义视图,用于展示唱片、唱针相关动画效果,模拟唱片播放音乐的视觉效果 @BindView(R.id.disc_view) - DiscView mDisc; //唱碟 + DiscView mDisc; + // 通过ButterKnife框架绑定ImageView控件,用于显示唱碟中的歌手头像(封面图片) @BindView(R.id.iv_disc_background) - ImageView mDiscImg; //唱碟中的歌手头像 + ImageView mDiscImg; + // 通过ButterKnife框架绑定Button控件,用于获取封面图片和歌词(可能针对本地歌曲没有相关资源时使用) @BindView(R.id.btn_get_img_lrc) - Button mGetImgAndLrcBtn;//获取封面和歌词 + Button mGetImgAndLrcBtn; + // 通过ButterKnife框架绑定LrcView自定义视图,用于展示歌词内容,并实现歌词与音乐播放进度同步等功能 @BindView(R.id.lrcView) - LrcView mLrcView; //歌词自定义View + LrcView mLrcView; + // 通过ButterKnife框架绑定ImageView控件,用于显示歌曲下载相关状态图标,点击可进行下载操作 @BindView(R.id.downloadIv) - ImageView mDownLoadIv; //下载 + ImageView mDownLoadIv; private PlayPresenter mPresenter; - - private boolean isOnline; //判断是否为网络歌曲 - private int mListType; //列表类型 + // 判断是否为网络歌曲,用于区分不同的资源获取和操作逻辑 + private boolean isOnline; + // 列表类型,可能用于标识歌曲所在的不同列表(例如本地歌曲列表、在线歌曲列表等不同分类),具体含义需结合业务逻辑确定 + private int mListType; + // 播放状态,通过常量来表示不同的播放情况(例如正在播放、暂停等),取值对应Constant类中的相关定义 private int mPlayStatus; - private int mPlayMode;//播放模式 - - private boolean isChange; //拖动进度条 - private boolean isSeek;//标记是否在暂停的时候拖动进度条 - private boolean flag; //用做暂停的标记 - private int time; //记录暂停的时间 + // 播放模式,同样通过常量来表示不同的播放模式(顺序播放、随机播放、单曲循环等),取值对应Constant类中的相关定义 + private int mPlayMode; + + // 标记是否正在拖动进度条,用于防止进度条更新冲突,避免在拖动时后台线程等同时更新进度导致异常 + private boolean isChange; + // 标记是否在暂停的时候拖动进度条,用于后续恢复播放时正确设置播放位置等操作 + private boolean isSeek; + // 用作暂停的标记,用于区分不同的播放/暂停操作场景,辅助控制播放相关逻辑 + private boolean flag; + // 记录暂停的时间,单位可能是毫秒,用于恢复播放时定位播放位置,使播放能从暂停位置继续 + private int time; + // 标记当前是否正在播放音乐,用于界面展示和一些操作的判断依据 private boolean isPlaying; + // 当前播放的歌曲对象,包含歌曲的各种信息,如歌曲名称、歌手、时长、是否在线、是否下载等属性 private Song mSong; + // MediaPlayer对象,用于实际的音频播放控制,不过这里看起来主要通过服务中的MediaPlayer来操作 private MediaPlayer mMediaPlayer; - + // 用于显示播放相关控件的RelativeLayout布局,可能用于整体布局管理和一些布局相关操作 private RelativeLayout mPlayRelative; + // 存储歌词内容的字符串,用于在获取歌词后展示到界面的LrcView中,并实现歌词同步等功能 private String mLrc = null; - private boolean isLove;//是否已经在我喜欢的列表中 + // 标记歌曲是否已经在“我喜欢”的列表中,用于控制收藏按钮的显示状态以及相关操作逻辑 + private boolean isLove; + // 用于存储歌手头像(封面图片)对应的Bitmap对象,方便在界面展示和进行一些图片相关处理 private Bitmap mImgBmp; - private List mLocalSong;//用来判断是否有本地照片 - //服务 + // 存储本地歌曲列表,可能用于判断歌曲是否有本地照片等相关操作,具体用途需结合业务逻辑确定 + private List mLocalSong; + + // 用于与PlayerService建立连接并获取服务端的Binder对象,从而实现与播放服务的交互,获取播放状态等信息以及调用播放相关方法 private PlayerService.PlayStatusBinder mPlayStatusBinder; + // 用于与DownloadService建立连接并获取服务端的Binder对象,以便调用下载相关的操作方法(如开始下载歌曲等) private DownloadService.DownloadBinder mDownloadBinder; - //播放 + // 播放服务的服务连接对象,实现了ServiceConnection接口,用于处理与PlayerService连接建立、断开时的回调逻辑 private ServiceConnection mPlayConnection = new ServiceConnection() { + // 当与PlayerService成功连接时调用,获取服务端传递过来的Binder对象,并进行一系列初始化和界面设置操作 @Override public void onServiceConnected(ComponentName name, IBinder service) { mPlayStatusBinder = (PlayerService.PlayStatusBinder) service; - //播放模式 - mPlayMode = mPresenter.getPlayMode();//得到播放模式 - mPlayStatusBinder.setPlayMode(mPlayMode);//通知服务播放模式 + // 获取播放模式,通过Presenter获取当前设置的播放模式(例如顺序播放、随机播放等) + mPlayMode = mPresenter.getPlayMode(); + // 将获取到的播放模式通知给PlayerService,使服务按照该模式进行音乐播放 + mPlayStatusBinder.setPlayMode(mPlayMode); + // 判断当前歌曲是否为在线歌曲,通过FileUtil获取歌曲信息并判断其是否在线 isOnline = FileUtil.getSong().isOnline(); if (isOnline) { + // 如果是在线歌曲,隐藏获取封面和歌词的按钮,因为在线歌曲可能已经有相关资源或者通过其他方式获取 mGetImgAndLrcBtn.setVisibility(View.GONE); + // 设置歌手头像(封面图片),通过调用setSingerImg方法传入歌曲的图片URL来加载显示图片 setSingerImg(FileUtil.getSong().getImgUrl()); + // 如果当前播放状态为正在播放(Constant.SONG_PLAY),则启动唱片动画、设置播放按钮为选中状态(表示正在播放),并开始更新SeekBar进度 if (mPlayStatus == Constant.SONG_PLAY) { mDisc.play(); mPlayBtn.setSelected(true); startUpdateSeekBarProgress(); } } else { + // 如果是本地歌曲,设置本地图片(可能是本地存储的封面图片或者根据歌手等信息生成的默认图片),调用setLocalImg方法传入歌手名称来设置图片 setLocalImg(mSong.getSinger()); + // 设置SeekBar的二级进度为歌曲的总时长,二级进度可能用于显示缓冲进度等其他用途(具体需看业务逻辑) mSeekBar.setSecondaryProgress((int) mSong.getDuration()); } + // 设置显示歌曲总时长的TextView文本内容,通过MediaUtil工具类将歌曲时长格式化为合适的时间格式(例如"03:30"这种形式)进行显示 mDurationTimeTv.setText(MediaUtil.formatTime(mSong.getDuration())); - //缓存进度条 + // 为MediaPlayer设置缓冲更新监听器,当缓冲进度更新时,会根据缓冲百分比来更新SeekBar的二级进度,用于展示缓冲情况 mPlayStatusBinder.getMediaPlayer().setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() { @Override public void onBufferingUpdate(MediaPlayer mp, int percent) { @@ -174,577 +218,628 @@ public class PlayActivity extends BaseMvpActivity implements IPla }); } + // 当与PlayerService意外断开连接时调用(比如服务崩溃),这里暂时没做具体处理 @Override public void onServiceDisconnected(ComponentName name) { - - } }; - //绑定下载服务 + + // 下载服务的服务连接对象,实现了ServiceConnection接口,用于处理与DownloadService连接建立、断开时的回调逻辑 private ServiceConnection mDownloadConnection = new ServiceConnection() { + // 当与DownloadService成功连接时调用,获取服务端传递过来的Binder对象,用于后续调用下载相关操作方法 @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { mDownloadBinder = (DownloadService.DownloadBinder) iBinder; } + // 当与DownloadService意外断开连接时调用(比如服务崩溃),这里暂时没做具体处理 @Override public void onServiceDisconnected(ComponentName componentName) { - } }; + // 用于处理音乐播放进度更新相关逻辑的Handler对象,通过定时发送消息来更新SeekBar进度以及当前播放时间的显示等 @SuppressLint("HandlerLeak") private Handler mMusicHandler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); + // 如果当前没有正在拖动进度条,执行以下更新操作,避免拖动进度条时与后台更新冲突 if (!isChange) { + // 设置SeekBar的进度为从PlayerService获取到的当前播放时间(单位可能需要和SeekBar的进度单位一致,这里看起来是以秒为单位) mSeekBar.setProgress((int) mPlayStatusBinder.getCurrentTime()); + // 设置显示当前播放时间的TextView文本内容,通过MediaUtil工具类将当前进度时间格式化为合适的时间格式进行显示 mCurrentTimeTv.setText(MediaUtil.formatTime(mSeekBar.getProgress())); + // 再次启动更新SeekBar进度的操作,形成定时更新的循环,每隔一定时间(下面代码中是1秒)更新一次进度显示 startUpdateSeekBarProgress(); } - } }; - + // 初始化视图相关操作,在Activity创建时调用,进行一系列的初始化设置,如注册EventBus、隐藏状态栏、设置页面过渡动画等 @Override protected void initView() { super.initView(); + // 在EventBus上注册,以便接收事件通知,例如下载成功、歌曲状态改变等相关事件 EventBus.getDefault().register(this); + // 调用工具类方法隐藏状态栏,传入true可能表示完全隐藏状态栏(具体需看CommonUtil中方法的实现逻辑) CommonUtil.hideStatusBar(this, true); - //设置进入退出动画 + // 设置Activity进入时的过渡动画为从侧边滑动进入的效果,通过创建Slide对象来设置 getWindow().setEnterTransition(new Slide()); + // 设置Activity退出时的过渡动画为从侧边滑动退出的效果,同样通过创建Slide对象来设置 getWindow().setExitTransition(new Slide()); - //判断播放状态 + // 获取从Intent传递过来的播放状态参数,通过Constant.PLAYER_STATUS作为键获取对应的值,若没有则默认取值为2(具体含义需看Constant类定义) mPlayStatus = getIntent().getIntExtra(Constant.PLAYER_STATUS, 2); - //绑定服务,播放和下载的服务 + // 创建启动PlayerService的Intent,用于与播放服务建立连接并交互 Intent playIntent = new Intent(PlayActivity.this, PlayerService.class); + // 创建启动DownloadService的Intent,用于与下载服务建立连接并交互 Intent downIntent = new Intent(PlayActivity.this, DownloadService.class); + // 绑定PlayerService,建立与播放服务的连接,传入连接对象、绑定模式(自动创建服务如果不存在的话) bindService(playIntent, mPlayConnection, Context.BIND_AUTO_CREATE); + // 绑定DownloadService,建立与下载服务的连接,传入连接对象、绑定模式(自动创建服务如果不存在的话) bindService(downIntent, mDownloadConnection, Context.BIND_AUTO_CREATE); - //界面填充 + // 获取当前播放的歌曲对象,通过FileUtil工具类获取存储的歌曲信息 mSong = FileUtil.getSong(); + // 获取歌曲的列表类型,可能用于区分歌曲来源或所属分类等,存储到mListType变量中 mListType = mSong.getListType(); + // 设置显示歌手名称的TextView文本内容为歌曲的歌手信息 mSingerTv.setText(mSong.getSinger()); + // 设置显示歌曲名称的TextView文本内容为歌曲的歌曲名称信息 mSongTv.setText(mSong.getSongName()); + // 设置显示当前播放时间的TextView文本内容,通过MediaUtil工具类将歌曲当前时间格式化为合适的时间格式进行显示 mCurrentTimeTv.setText(MediaUtil.formatTime(mSong.getCurrentTime())); + // 设置SeekBar的最大进度为歌曲的总时长(单位可能需要转换,这里看起来是以秒为单位存储时长) mSeekBar.setMax((int) mSong.getDuration()); + // 设置SeekBar的当前进度为歌曲的当前播放时间 mSeekBar.setProgress((int) mSong.getCurrentTime()); - mDownLoadIv.setVisibility(mSong.isOnline() ? View.VISIBLE : View.GONE); //下载按钮是否隐藏 - mDownLoadIv.setImageDrawable(mSong.isDownload() ? getDrawable(R.drawable.downloaded) : getDrawable(R.drawable.download_song)); - - mPlayMode = mPresenter.getPlayMode();//得到播放模式 - if (mPlayMode == Constant.PLAY_ORDER) { - mPlayModeBtn.setBackground(getDrawable(R.drawable.play_mode_order)); - } else if (mPlayMode == Constant.PLAY_RANDOM) { - mPlayModeBtn.setBackground(getDrawable(R.drawable.play_mode_random)); - } else { - mPlayModeBtn.setBackground(getDrawable(R.drawable.play_mode_single)); - } - - - } - - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onDownloadSuccessEvent(DownloadEvent event){ - if(event.getDownloadStatus() == Constant.TYPE_DOWNLOAD_SUCCESS){ - mDownLoadIv.setImageDrawable( - LitePal.where("songId=?", mSong.getSongId()).find(DownloadSong.class).size() != 0 - ? getDrawable(R.drawable.downloaded) - : getDrawable(R.drawable.download_song)); - } - } - - @Override - protected PlayPresenter getPresenter() { - //与Presenter建立关系 - mPresenter = new PlayPresenter(); - return mPresenter; - } - - @Override - protected int getLayoutId() { - return R.layout.activity_play; - } - - @Override - protected void initData() { - mPresenter.queryLove(mSong.getSongId()); //查找歌曲是否为我喜欢的歌曲 + // 根据歌曲是否为在线歌曲来设置下载按钮的显示状态,如果是在线歌曲则显示下载按钮,否则隐藏 + mDownLoadIv.setVisibility(mSong.isOnline()? View.VISIBLE + // 根据歌曲是否为在线歌曲来设置下载按钮的显示状态,如果是在线歌曲则显示下载按钮,否则隐藏 +mDownLoadIv.setVisibility(mSong.isOnline()? View.VISIBLE : View.GONE); +// 根据歌曲是否已经下载来设置下载按钮显示的图标,如果已下载则显示已下载图标,否则显示下载歌曲图标 +mDownLoadIv.setImageDrawable(mSong.isDownload()? getDrawable(R.drawable.downloaded) : getDrawable(R.drawable.download_song)); + +// 获取当前的播放模式,通过Presenter获取当前设置的播放模式(例如顺序播放、随机播放等) +mPlayMode = mPresenter.getPlayMode(); +// 根据获取到的播放模式设置播放模式按钮的背景图片,以展示对应的播放模式图标 +if (mPlayMode == Constant.PLAY_ORDER) { + mPlayModeBtn.setBackground(getDrawable(R.drawable.play_mode_order)); +} else if (mPlayMode == Constant.PLAY_RANDOM) { + mPlayModeBtn.setBackground(getDrawable(R.drawable.play_mode_random)); +} else { + mPlayModeBtn.setBackground(getDrawable(R.drawable.play_mode_single)); +} - if (mPlayStatus == Constant.SONG_PLAY) { - mDisc.play(); - mPlayBtn.setSelected(true); - startUpdateSeekBarProgress(); - } +// 接收DownloadEvent事件的方法,在主线程中处理,当下载成功事件发生且下载状态为成功时,更新下载按钮的图标显示 +@Subscribe(threadMode = ThreadMode.MAIN) +public void onDownloadSuccessEvent(DownloadEvent event) { + if (event.getDownloadStatus() == Constant.TYPE_DOWNLOAD_SUCCESS) { + mDownLoadIv.setImageDrawable( + // 通过LitePal查询数据库中是否存在对应歌曲ID的已下载歌曲记录,如果存在则显示已下载图标,否则显示下载歌曲图标 + LitePal.where("songId=?", mSong.getSongId()).find(DownloadSong.class).size()!= 0 + ? getDrawable(R.drawable.downloaded) + : getDrawable(R.drawable.download_song)); } +} +// 获取与该Activity对应的Presenter对象,创建并返回PlayPresenter实例,用于处理业务逻辑,实现MVP模式中的Presenter层关联 +@Override +protected PlayPresenter getPresenter() { + // 创建PlayPresenter实例,建立与Presenter的关联,以便后续调用Presenter中的方法来处理业务逻辑 + mPresenter = new PlayPresenter(); + return mPresenter; +} - private void try2UpdateMusicPicBackground(final Bitmap bitmap) { - new Thread(() -> { - final Drawable drawable = getForegroundDrawable(bitmap); - runOnUiThread(() -> { - mRootLayout.setForeground(drawable); - mRootLayout.beginAnimation(); - }); - }).start(); - } +// 获取Activity对应的布局资源ID,这里返回activity_play布局的ID,用于设置Activity的界面布局 +@Override +protected int getLayoutId() { + return R.layout.activity_play; +} - private Drawable getForegroundDrawable(Bitmap bitmap) { - /*得到屏幕的宽高比,以便按比例切割图片一部分*/ - final float widthHeightSize = (float) (DisplayUtil.getScreenWidth(PlayActivity.this) - * 1.0 / DisplayUtil.getScreenHeight(this) * 1.0); - - int cropBitmapWidth = (int) (widthHeightSize * bitmap.getHeight()); - int cropBitmapWidthX = (int) ((bitmap.getWidth() - cropBitmapWidth) / 2.0); - - /*切割部分图片*/ - Bitmap cropBitmap = Bitmap.createBitmap(bitmap, cropBitmapWidthX, 0, cropBitmapWidth, - bitmap.getHeight()); - /*缩小图片*/ - Bitmap scaleBitmap = Bitmap.createScaledBitmap(cropBitmap, bitmap.getWidth() / 50, bitmap - .getHeight() / 50, false); - /*模糊化*/ - final Bitmap blurBitmap = FastBlurUtil.doBlur(scaleBitmap, 8, true); - - final Drawable foregroundDrawable = new BitmapDrawable(blurBitmap); - /*加入灰色遮罩层,避免图片过亮影响其他控件*/ - foregroundDrawable.setColorFilter(Color.GRAY, PorterDuff.Mode.MULTIPLY); - return foregroundDrawable; +// 初始化数据相关操作,在Activity创建时调用,进行一些数据查询和初始化播放相关操作 +@Override +protected void initData() { + // 调用Presenter的方法查询歌曲是否为“我喜欢”列表中的歌曲,以便后续根据查询结果更新界面显示等操作 + mPresenter.queryLove(mSong.getSongId()); + // 如果当前播放状态为正在播放(Constant.SONG_PLAY),则启动唱片动画、设置播放按钮为选中状态(表示正在播放),并开始更新SeekBar进度 + if (mPlayStatus == Constant.SONG_PLAY) { + mDisc.play(); + mPlayBtn.setSelected(true); + startUpdateSeekBarProgress(); } +} +// 在新线程中尝试更新音乐图片背景,传入一个Bitmap对象作为背景图片,进行图片处理后设置为界面的背景相关效果 +private void try2UpdateMusicPicBackground(final Bitmap bitmap) { + new Thread(() -> { + // 获取处理后的Drawable对象,用于设置为背景,调用getForegroundDrawable方法进行图片处理等操作 + final Drawable drawable = getForegroundDrawable(bitmap); + // 在UI线程中执行设置背景和启动相关动画的操作,因为涉及到UI更新,必须在UI线程中进行 + runOnUiThread(() -> { + mRootLayout.setForeground(drawable); + mRootLayout.beginAnimation(); + }); + }).start(); +} - @Override - protected void onClick() { - //返回按钮 - mBackIv.setOnClickListener(v -> finish()); - //获取本地音乐的图片和歌词 - mGetImgAndLrcBtn.setOnClickListener(v -> getSingerAndLrc()); - - //进度条的监听事件 - mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - - } +// 对传入的Bitmap图片进行一系列处理,生成用于设置为界面背景相关效果的Drawable对象,包括按比例切割、缩小、模糊以及添加颜色滤镜等操作 +private Drawable getForegroundDrawable(Bitmap bitmap) { + /*得到屏幕的宽高比,以便按比例切割图片一部分,通过获取屏幕宽度和高度并计算比例,用于后续图片裁剪操作,使图片适应界面展示效果*/ + final float widthHeightSize = (float) (DisplayUtil.getScreenWidth(PlayActivity.this) + * 1.0 / DisplayUtil.getScreenHeight(this) * 1.0); + + // 根据屏幕宽高比计算裁剪后的图片宽度,用于从原始图片中裁剪出合适的部分 + int cropBitmapWidth = (int) (widthHeightSize * bitmap.getHeight()); + // 计算裁剪图片时在X轴方向的起始位置,使裁剪出的图片部分在水平方向居中 + int cropBitmapWidthX = (int) ((bitmap.getWidth() - cropBitmapWidth) / 2.0); + + /*切割部分图片,从原始Bitmap对象中按照计算好的位置和尺寸裁剪出一部分图片,用于后续处理和作为界面背景相关效果展示*/ + Bitmap cropBitmap = Bitmap.createBitmap(bitmap, cropBitmapWidthX, 0, cropBitmapWidth, + bitmap.getHeight()); + /*缩小图片,将裁剪后的图片按照一定比例进行缩小,可能是为了优化性能或者适应界面显示需求等原因,生成一个尺寸更小的Bitmap对象*/ + Bitmap scaleBitmap = Bitmap.createScaledBitmap(cropBitmap, bitmap.getWidth() / 50, bitmap + .getHeight() / 50, false); + /*模糊化,对缩小后的图片进行模糊处理,使图片作为背景时呈现出一种模糊的视觉效果,通过调用FastBlurUtil工具类的方法来实现模糊操作*/ + final Bitmap blurBitmap = FastBlurUtil.doBlur(scaleBitmap, 8, true); + + // 创建一个基于模糊处理后的Bitmap对象的BitmapDrawable对象,作为最终要返回的Drawable,用于设置为界面背景相关效果 + final Drawable foregroundDrawable = new BitmapDrawable(blurBitmap); + /*加入灰色遮罩层,避免图片过亮影响其他控件,通过设置颜色滤镜,将图片颜色与灰色进行混合相乘的模式,使图片整体色调变暗,达到不影响其他控件显示的效果*/ + foregroundDrawable.setColorFilter(Color.GRAY, PorterDuff.Mode.MULTIPLY); + return foregroundDrawable; +} - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - //防止在拖动进度条进行进度设置时与Thread更新播放进度条冲突 - isChange = true; - } +// 处理各种点击事件的方法,为各个按钮、视图等设置点击监听器,实现对应的业务逻辑,如返回、获取图片歌词、播放控制、模式切换、收藏等操作 +@Override +protected void onClick() { + // 为返回按钮设置点击监听器,点击时调用finish方法关闭当前Activity,返回上一界面 + mBackIv.setOnClickListener(v -> finish()); + // 为获取本地音乐的图片和歌词按钮设置点击监听器,点击时调用getSingerAndLrc方法获取相关资源 + mGetImgAndLrcBtn.setOnClickListener(v -> getSingerAndLrc()); + + // 为SeekBar设置进度变化监听器,用于处理进度条拖动等操作,实现根据拖动情况调整播放位置以及更新相关界面显示等功能 + mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + // 当进度改变时调用(拖动过程中会多次调用),这里暂时没做具体处理 + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - if (mPlayStatusBinder.isPlaying()) { - mMediaPlayer = mPlayStatusBinder.getMediaPlayer(); - mMediaPlayer.seekTo(seekBar.getProgress() * 1000); - startUpdateSeekBarProgress(); - } else { - time = seekBar.getProgress(); - isSeek = true; - } - isChange = false; - mCurrentTimeTv.setText(MediaUtil.formatTime(seekBar.getProgress())); - } - }); + } - mPlayModeBtn.setOnClickListener(v -> changePlayMode()); + // 当开始拖动进度条时调用,设置标记防止与后台更新进度冲突,标记当前正在拖动进度条 + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + isChange = true; + } - //播放,暂停的实现 - mPlayBtn.setOnClickListener(v -> { - mMediaPlayer = mPlayStatusBinder.getMediaPlayer(); + // 当停止拖动进度条时调用,根据播放状态设置新的播放位置,并更新当前播放时间显示,重新启动SeekBar进度更新等操作 + @Override + public void onStopTrackingTouch(SeekBar seekBar) { if (mPlayStatusBinder.isPlaying()) { - mPlayStatusBinder.pause(); - stopUpdateSeekBarProgress(); - flag = true; - mPlayBtn.setSelected(false); - mDisc.pause(); - } else if (flag) { - mPlayStatusBinder.resume(); - flag = false; - if (isSeek) { - Log.d(TAG, "onClick: " + time); - mMediaPlayer.seekTo(time * 1000); - isSeek = false; - } - mDisc.play(); - mPlayBtn.setSelected(true); + mMediaPlayer = mPlayStatusBinder.getMediaPlayer(); + mMediaPlayer.seekTo(seekBar.getProgress() * 1000); startUpdateSeekBarProgress(); } else { - if (isOnline) { - mPlayStatusBinder.playOnline(); - } else { - mPlayStatusBinder.play(mListType); - } - mMediaPlayer.seekTo((int) mSong.getCurrentTime() * 1000); - mDisc.play(); - mPlayBtn.setSelected(true); - startUpdateSeekBarProgress(); + time = seekBar.getProgress(); + isSeek = true; } - }); - mNextBtn.setOnClickListener(v -> { - mPlayStatusBinder.next(); - if (mPlayStatusBinder.isPlaying()) { - mPlayBtn.setSelected(true); - } else { - mPlayBtn.setSelected(false); + isChange = false; + mCurrentTimeTv.setText(MediaUtil.formatTime(seekBar.getProgress())); + } + }); + + // 为播放模式按钮设置点击监听器,点击时调用changePlayMode方法切换播放模式,弹出选择播放模式的弹窗供用户选择 + mPlayModeBtn.setOnClickListener(v -> changePlayMode()); + + // 为播放/暂停按钮设置点击监听器,根据当前播放状态进行播放、暂停、恢复播放等操作,并相应地更新界面显示和相关动画效果 + mPlayBtn.setOnClickListener(v -> { + mMediaPlayer = mPlayStatusBinder.getMediaPlayer(); + if (mPlayStatusBinder.isPlaying()) { + mPlayStatusBinder.pause(); + stopUpdateSeekBarProgress(); + flag = true; + mPlayBtn.setSelected(false); + mDisc.pause(); + } else if (flag) { + mPlayStatusBinder.resume(); + flag = false; + if (isSeek) { + Log.d(TAG, "onClick: " + time); + mMediaPlayer.seekTo(time * 1000); + isSeek = false; } - mDisc.next(); - }); - mLastBtn.setOnClickListener(v -> { - mPlayStatusBinder.last(); + mDisc.play(); mPlayBtn.setSelected(true); - mDisc.last(); - }); - - mLoveBtn.setOnClickListener(v -> { - showLoveAnim(); - if (isLove) { - mLoveBtn.setSelected(false); - mPresenter.deleteFromLove(FileUtil.getSong().getSongId()); + startUpdateSeekBarProgress(); + } else { + if (isOnline) { + mPlayStatusBinder.playOnline(); } else { - mLoveBtn.setSelected(true); - mPresenter.saveToLove(FileUtil.getSong()); + mPlayStatusBinder.play(mListType); } - isLove = !isLove; - }); + mMediaPlayer.seekTo((int) mSong.getCurrentTime() * 1000); + mDisc.play(); + mPlayBtn.setSelected(true); + startUpdateSeekBarProgress(); + } + }); - //唱碟点击效果 - mDisc.setOnClickListener(v -> { - if (!isOnline) { - String lrc = FileUtil.getLrcFromNative(mSong.getSongName()); - if (null == lrc) { - String qqId = mSong.getQqId(); - if (Constant.SONG_ID_UNFIND.equals(qqId)) {//匹配不到歌词 - getLrcError(null); - } else if (null == qqId) {//歌曲的id还未匹配 - mPresenter.getSongId(mSong.getSongName(), mSong.getDuration()); - } else {//歌词还未匹配 - mPresenter.getLrc(qqId, Constant.SONG_LOCAL); - } + // 为下一首歌曲按钮设置点击监听器,点击时调用PlayerService的next方法切换到下一首歌曲,同时更新播放按钮状态和唱片相关动画效果 + mNextBtn.setOnClickListener(v -> { + mPlayStatusBinder.next(); + if (mPlayStatusBinder.isPlaying()) { + mPlayBtn.setSelected(true); + } else { + mPlayBtn.setSelected(false); + } + mDisc.next(); + }); + + // 为上一首歌曲按钮设置点击监听器,点击时调用PlayerService的last方法切换到上一首歌曲,同时更新唱片相关动画效果 + mLastBtn.setOnClickListener(v -> { + mPlayStatusBinder.last(); + mPlayBtn.setSelected(true); + mDisc.last(); + }); + + // 为收藏按钮设置点击监听器,点击时切换收藏状态,调用相关方法实现添加或移除收藏操作,并更新按钮的选中状态显示以及通过EventBus发送收藏相关事件 + mLoveBtn.setOnClickListener(v -> { + showLoveAnim(); + if (isLove) { + mLoveBtn.setSelected(false); + mPresenter.deleteFromLove(FileUtil.getSong().getSongId()); + } else { + mLoveBtn.setSelected(true); + mPresenter.saveToLove(FileUtil.getSong()); + } + isLove =!isLove; + }); + + // 为唱碟视图设置点击监听器,根据歌曲是否为在线歌曲进行不同的歌词获取操作,如果是本地歌曲还需进一步判断歌词是否存在等情况来决定获取歌词的方式 + mDisc.setOnClickListener(v -> { + if (!isOnline) { + String lrc = FileUtil.getLrcFromNative(mSong.getSongName()); + if (null == lrc) { + String qqId = mSong.getQqId(); + if (Constant.SONG_ID_UNFIND.equals(qqId)) { + // 如果歌曲ID匹配不到歌词(可能是歌词资源不存在等原因),调用getLrcError方法显示获取歌词失败提示 + getLrcError(null); + } else if (null == qqId) { + // 如果歌曲的ID还未匹配(可能是刚添加的本地歌曲等情况),调用Presenter的方法获取歌曲ID + mPresenter.getSongId(mSong.getSongName(), mSong.getDuration()); } else { - showLrc(lrc); + // 如果歌词还未匹配(已有歌曲ID但歌词未获取到),调用Presenter的方法根据歌曲ID获取本地歌词 + mPresenter.getLrc(qqId, Constant.SONG_LOCAL); } } else { - mPresenter.getLrc(mSong.getSongId(), Constant.SONG_ONLINE); + // 如果本地已经存在歌词,调用showLrc方法展示歌词内容到界面的LrcView中 + showLrc(lrc); } + } else { + // 如果是在线歌曲,调用Presenter的方法根据歌曲ID获取在线歌词 + mPresenter.getLrc(mSong.getSongId(), Constant.SONG_ONLINE); } - ); - //歌词点击效果 - mLrcView.setOnClickListener(v -> { - mLrcView.setVisibility(View.GONE); - mDisc.setVisibility(View.VISIBLE); - }); - //歌曲下载 - mDownLoadIv.setOnClickListener(v -> { - if (mSong.isDownload()) { - showToast(getString(R.string.downloded)); - } else { - mDownloadBinder.startDownload(getDownloadInfoFromSong(mSong)); } - }); - - } - - @Override - public String getSingerName() { - Song song = FileUtil.getSong(); - if (song.getSinger().contains("/")) { - String[] s = song.getSinger().split("/"); - return s[0].trim(); + ); + + // 为歌词视图设置点击监听器,点击时隐藏歌词视图,显示唱碟视图,实现歌词视图和唱碟视图的切换显示效果 + mLrcView.setOnClickListener(v -> { + mLrcView.setVisibility(View.GONE); + mDisc.setVisibility(View.VISIBLE); + }); + + // 为歌曲下载按钮设置点击监听器,点击时根据歌曲是否已经下载进行不同操作,如果已下载则显示提示信息,如果未下载则调用下载服务的方法开始下载歌曲 + mDownLoadIv.setOnClickListener(v -> { + if (mSong.isDownload()) { + showToast(getString(R.string.downloded)); } else { - return song.getSinger().trim(); + mDownloadBinder.startDownload(getDownloadInfoFromSong(mSong)); } + }); - } +} - private String getSongName() { - Song song = FileUtil.getSong(); - assert song != null; - return song.getSongName().trim(); +// 获取歌手名称的方法,对获取到的歌手名称进行处理,如果歌手名称包含斜杠(可能是多个歌手等情况),则取第一个歌手名称并去除空格后返回,否则直接去除空格后返回 +@Override +public String getSingerName() { + Song song = FileUtil.getSong(); + if (song.getSinger().contains("/")) { + String[] s = song.getSinger().split("/"); + return s[0].trim(); + } else { + return song.getSinger().trim(); } +} - @Override - public void getSingerAndLrc() { - mGetImgAndLrcBtn.setText("正在获取..."); - mPresenter.getSingerImg(getSingerName(), getSongName(), mSong.getDuration()); - } +// 获取歌曲名称的方法,获取当前播放歌曲的名称,并去除空格后返回 +private String getSongName() { + Song song = FileUtil.getSong(); + assert song!= null; + return song.getSongName().trim(); +} - @Override - public void setSingerImg(String ImgUrl) { - Glide.with(this) - .load(ImgUrl) - .apply(RequestOptions.placeholderOf(R.drawable.welcome)) - .apply(RequestOptions.errorOf(R.drawable.welcome)) - .into(new SimpleTarget() { - @Override - public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { - mImgBmp = ((BitmapDrawable) resource).getBitmap(); - //如果是本地音乐 - if (!isOnline) { - //保存图片到本地 - FileUtil.saveImgToNative(PlayActivity.this, mImgBmp, getSingerName()); - //将封面地址放到数据库中 - LocalSong localSong = new LocalSong(); - localSong.setPic(Api.STORAGE_IMG_FILE + FileUtil.getSong().getSinger() + ".jpg"); - localSong.updateAll("songId=?", FileUtil.getSong().getSongId()); - } +// 获取本地音乐的图片和歌词的方法,设置按钮文本为“正在获取...”,然后调用Presenter的方法根据歌手名称和歌曲名称以及歌曲时长来获取歌手图片和歌词资源 +@Override +public void getSingerAndLrc() { + mGetImgAndLrcBtn.setText("正在获取..."); + mPresenter.getSingerImg(getSingerName(), getSongName(), mSong.getDuration()); +} - try2UpdateMusicPicBackground(mImgBmp); - setDiscImg(mImgBmp); - mGetImgAndLrcBtn.setVisibility(View.GONE); +// 设置歌手头像(封面图片)的方法,使用Glide库加载图片,设置占位图和加载失败时显示的图片,在图片加载成功后进行一系列处理,如保存本地、更新数据库、设置唱碟中的图片等,并更新界面相关显示 +@Override +public void setSingerImg(String ImgUrl) { + Glide.with(this) + .load(ImgUrl) + .apply(RequestOptions.placeholderOf(R.drawable.welcome)) + .apply(RequestOptions.errorOf(R.drawable.welcome)) + .into(new SimpleTarget() { + @Override + public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { + mImgBmp = ((BitmapDrawable) resource).getBitmap(); + //如果是本地音乐 + if (!isOnline) { + //保存图片到本地,调用FileUtil的方法将图片保存到本地存储中,方便后续使用 + FileUtil.saveImgToNative(PlayActivity.this, mImgBmp, getSingerName()); + //将封面地址放到数据库中,创建一个LocalSong对象,设置其图片路径属性,并根据歌曲ID更新数据库中对应记录的图片路径信息 + LocalSong localSong = new LocalSong(); + localSong.setPic(Api.STORAGE_IMG_FILE + FileUtil.getSong().getSinger() + ".jpg"); + localSong.updateAll("songId=?", FileUtil.getSong().getSongId()); } - }); - - } - - - @Override - public void showLove(final boolean love) { - isLove = love; - runOnUiThread(() -> { - if (love) { - mLoveBtn.setSelected(true); - } else { - mLoveBtn.setSelected(false); - } - }); - - } - - @Override - public void showLoveAnim() { - AnimatorSet animatorSet = (AnimatorSet) AnimatorInflater.loadAnimator(PlayActivity.this, R.animator.favorites_anim); - animatorSet.setTarget(mLoveBtn); - animatorSet.start(); - } - - @Override - public void saveToLoveSuccess() { - EventBus.getDefault().post(new SongCollectionEvent(true)); - CommonUtil.showToast(PlayActivity.this, getString(R.string.love_success)); - } - - @Override - public void sendUpdateCollection() { - EventBus.getDefault().post(new SongCollectionEvent(false)); - } - - - //设置唱碟中歌手头像 - private void setDiscImg(Bitmap bitmap) { - mDiscImg.setImageDrawable(mDisc.getDiscDrawable(bitmap)); - int marginTop = (int) (DisplayUtil.SCALE_DISC_MARGIN_TOP * CommonUtil.getScreenHeight(PlayActivity.this)); - RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) mDiscImg - .getLayoutParams(); - layoutParams.setMargins(0, marginTop, 0, 0); - mDiscImg.setLayoutParams(layoutParams); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onSongChanageEvent(SongStatusEvent event) { - if (event.getSongStatus() == Constant.SONG_CHANGE) { - mDisc.setVisibility(View.VISIBLE); - mLrcView.setVisibility(View.GONE); - mSong = FileUtil.getSong(); - mSongTv.setText(mSong.getSongName()); - mSingerTv.setText(mSong.getSinger()); - mDurationTimeTv.setText(MediaUtil.formatTime(mSong.getDuration())); - mPlayBtn.setSelected(true); - mSeekBar.setMax((int) mSong.getDuration()); - startUpdateSeekBarProgress(); - //缓存进度条 - mPlayStatusBinder.getMediaPlayer().setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() { - @Override - public void onBufferingUpdate(MediaPlayer mp, int percent) { - mSeekBar.setSecondaryProgress(percent * mSeekBar.getProgress()); + try2UpdateMusicPicBackground(mImgBmp); + setDiscImg(mImgBmp); + mGetImgAndLrcBtn.setVisibility(View.GONE); } }); - mPresenter.queryLove(mSong.getSongId()); //查找歌曲是否为我喜欢的歌曲 - if (mSong.isOnline()) { - setSingerImg(mSong.getImgUrl()); - } else { - setLocalImg(mSong.getSinger());//显示照片 - } - } - } - - private void startUpdateSeekBarProgress() { - /*避免重复发送Message*/ - stopUpdateSeekBarProgress(); - mMusicHandler.sendEmptyMessageDelayed(0, 1000); - } - - private void stopUpdateSeekBarProgress() { - mMusicHandler.removeMessages(0); - } - - private void setLocalImg(String singer) { - String imgUrl = Api.STORAGE_IMG_FILE + MediaUtil.formatSinger(singer) + ".jpg"; - Glide.with(this) - .load(imgUrl) - .listener(new RequestListener() { - @Override - public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { - mGetImgAndLrcBtn.setVisibility(View.VISIBLE); - mGetImgAndLrcBtn.setText("获取封面和歌词"); - setDiscImg(BitmapFactory.decodeResource(getResources(), R.drawable.default_disc)); - mRootLayout.setBackgroundResource(R.drawable.background); - return true; - } +} - @Override - public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { - return false; - } - }) - .apply(RequestOptions.placeholderOf(R.drawable.background)) - .apply(RequestOptions.errorOf(R.drawable.background)) - .into(new SimpleTarget() { - @Override - public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { - mGetImgAndLrcBtn.setVisibility(View.GONE); - mImgBmp = ((BitmapDrawable) resource).getBitmap(); - try2UpdateMusicPicBackground(mImgBmp); - setDiscImg(mImgBmp); - } - }); - } +// 根据传入的参数在UI线程中更新歌曲是否为“喜欢”(收藏)状态的显示,通过设置收藏按钮的选中状态来展示 +@Override +public void showLove(final boolean love) { + isLove = love; + runOnUiThread(() -> { + if (love) { + mLoveBtn.setSelected(true); + } else { + mLoveBtn.setSelected(false); + } + }); +} +// 展示收藏动画的方法,通过AnimatorInflater加载动画资源,设置动画作用的目标为收藏按钮,然后启动动画,实现收藏按钮的动画效果 +@Override +public void showLoveAnim() { + AnimatorSet animatorSet = (AnimatorSet) AnimatorInflater.loadAnimator(PlayActivity.this, R.animator.favorites_anim); + animatorSet.setTarget(mLoveBtn); + animatorSet.start(); +} - /** - * 展示歌词 - * - * @param lrc - */ - @Override - public void showLrc(final String lrc) { - mDisc.setVisibility(View.GONE); - mLrcView.setVisibility(View.VISIBLE); - Log.d(TAG, "showLrc: "+mPlayStatusBinder.getMediaPlayer().getCurrentPosition()); - mLrcView.setLrc(lrc).setPlayer(mPlayStatusBinder.getMediaPlayer()).draw(); +// 保存到“喜欢”(收藏)列表成功后的处理方法,通过EventBus发送一个SongCollectionEvent事件(传入true表示收藏成功),并显示一个提示收藏成功的Toast信息 +@Override +public void saveToLoveSuccess() { + EventBus.getDefault().post(new SongCollectionEvent(true)); + CommonUtil.showToast(PlayActivity.this, getString(R.string.love_success)); +} - } +// 发送更新收藏相关信息的方法,通过EventBus发送一个SongCollectionEvent事件(传入false表示其他与收藏相关的更新情况),具体用途需结合业务逻辑确定 +@Override +public void sendUpdateCollection() { + EventBus.getDefault().post(new SongCollectionEvent(false)); +} - @Override - public void getLrcError(String content) { - showToast(getString(R.string.get_lrc_fail)); - mSong.setQqId(content); - FileUtil.saveSong(mSong); - } +// 设置唱碟中歌手头像的方法,调用DiscView的方法获取合成后的Drawable对象(包含音乐专辑图片等合成效果),并设置唱碟图片ImageView的显示图片,同时调整其布局参数中的上边距 +private void setDiscImg(Bitmap bitmap) { + mDiscImg.setImageDrawable(mDisc.getDiscDrawable(bitmap)); + int marginTop = (int) (DisplayUtil.SCALE_DISC_MARGIN_TOP * CommonUtil.getScreenHeight(PlayActivity.this)); + RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) mDiscImg + .getLayoutParams(); + layoutParams.setMargins(0, marginTop, 0, 0); - @Override - public void setLocalSongId(String songId) { - mSong.setQqId(songId); - FileUtil.saveSong(mSong); //保存 - } + mDiscImg.setLayoutParams(layoutParams); +} - @Override - public void getSongIdSuccess(String songId) { - setLocalSongId(songId);//保存音乐信息 - mPresenter.getLrc(songId, Constant.SONG_LOCAL);//获取歌词 +// 接收SongStatusEvent事件的方法,在主线程中处理,当歌曲状态改变(Constant.SONG_CHANGE)时,进行一系列界面更新操作,如显示唱碟、隐藏歌词视图、更新歌曲相关信息显示、重新启动SeekBar进度更新等 +@Subscribe(threadMode = ThreadMode.MAIN) +public void onSongChanageEvent(SongStatusEvent event) { + if (event.getSongStatus() == Constant.SONG_CHANGE) { + mDisc.setVisibility(View.VISIBLE); + mLrcView.setVisibility(View.GONE); + mSong = FileUtil.getSong(); + mSongTv.setText(mSong.getSongName()); + mSingerTv.setText(mSong.getSinger()); + mDurationTimeTv.setText(MediaUtil.formatTime(mSong.getDuration())); + mPlayBtn.setSelected(true); + mSeekBar.setMax((int) mSong.getDuration()); + startUpdateSeekBarProgress(); + //缓存进度条 + mPlayStatusBinder.getMediaPlayer().setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() { + @Override + public void onBufferingUpdate(MediaPlayer mp, int percent) { + mSeekBar.setSecondaryProgress(percent * mSeekBar.getProgress()); + } + }); + mPresenter.queryLove(mSong.getSongId()); //查找歌曲是否为我喜欢的歌曲 + if (mSong.isOnline()) { + setSingerImg(mSong.getImgUrl()); + } else { + setLocalImg(mSong.getSinger());//显示照片 + } } +} - @Override - public void saveLrc(String lrc) { - FileUtil.saveLrcToNative(lrc, mSong.getSongName()); - } +// 启动更新SeekBar进度的方法,先停止可能正在进行的更新操作(避免重复更新),然后通过Handler发送一个延迟消息,每隔1秒触发一次进度更新操作(由Handler的handleMessage方法处理) +private void startUpdateSeekBarProgress() { + /*避免重复发送Message*/ + stopUpdateSeekBarProgress(); + mMusicHandler.sendEmptyMessageDelayed(0, 1000); +} +// 停止更新SeekBar进度的方法,通过Handler移除所有待处理的消息,从而停止进度更新操作,常用于暂停播放、退出界面等情况 +private void stopUpdateSeekBarProgress() { + mMusicHandler.removeMessages(0); +} - //改变播放模式 - private void changePlayMode() { - View playModeView = LayoutInflater.from(this).inflate(R.layout.play_mode, null); - ConstraintLayout orderLayout = playModeView.findViewById(R.id.orderLayout); - ConstraintLayout randomLayout = playModeView.findViewById(R.id.randomLayout); - ConstraintLayout singleLayout = playModeView.findViewById(R.id.singleLayout); - TextView orderTv = playModeView.findViewById(R.id.orderTv); - TextView randomTv = playModeView.findViewById(R.id.randomTv); - TextView singleTv = playModeView.findViewById(R.id.singleTv); - - //显示弹窗 - PopupWindow popupWindow = new PopupWindow(playModeView, ScreenUtil.dip2px(this, 130), ScreenUtil.dip2px(this, 150)); - //设置背景色 - popupWindow.setBackgroundDrawable(getDrawable(R.color.transparent)); - //设置焦点 - popupWindow.setFocusable(true); - //设置可以触摸框以外的地方 - popupWindow.setOutsideTouchable(true); - popupWindow.update(); - //设置弹出的位置 - popupWindow.showAsDropDown(mPlayModeBtn, 0, -50); - - - //显示播放模式 - int mode = mPresenter.getPlayMode(); - if (mode == Constant.PLAY_ORDER) { - orderTv.setSelected(true); - randomTv.setSelected(false); - singleTv.setSelected(false); - } else if (mode == Constant.PLAY_RANDOM) { - randomTv.setSelected(true); - orderTv.setSelected(false); - singleTv.setSelected(false); - } else { - singleTv.setSelected(true); - randomTv.setSelected(false); - orderTv.setSelected(false); - } +// 设置本地图片(可能是本地存储的封面图片或者根据歌手等信息生成的默认图片)的方法,使用Glide库加载图片,设置占位图、加载失败时显示的图片以及加载成功和失败的监听器,在加载成功后进行一系列处理,如更新界面显示、设置背景等 +private void setLocalImg(String singer) { + String imgUrl = Api.STORAGE_IMG_FILE + MediaUtil.formatSinger(singer) + ".jpg"; + Glide.with(this) + .load(imgUrl) + .listener(new RequestListener() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + mGetImgAndLrcBtn.setVisibility(View.VISIBLE); + mGetImgAndLrcBtn.setText("获取封面和歌词"); + setDiscImg(BitmapFactory.decodeResource(getResources(), R.drawable.default_disc)); + mRootLayout.setBackgroundResource(R.drawable.background); + return true; + } + @Override + public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { + return false; + } + }) + .apply(RequestOptions.placeholderOf(R.drawable.background)) + .apply(RequestOptions.errorOf(R.drawable.background)) + .into(new SimpleTarget() { + @Override + public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { + mGetImgAndLrcBtn.setVisibility(View.GONE); + mImgBmp = ((BitmapDrawable) resource).getBitmap(); + try2UpdateMusicPicBackground(mImgBmp); + setDiscImg(mImgBmp); + } + }); +} - //顺序播放 - orderLayout.setOnClickListener(view -> { - mPlayStatusBinder.setPlayMode(Constant.PLAY_ORDER); //通知服务 - mPresenter.setPlayMode(Constant.PLAY_ORDER); - popupWindow.dismiss(); - mPlayModeBtn.setBackground(getDrawable(R.drawable.play_mode_order)); +// 展示歌词的方法,隐藏唱碟视图,显示歌词视图,然后调用LrcView的方法设置歌词内容,并传入MediaPlayer对象以实现歌词与音乐播放进度同步,最后调用draw方法进行歌词绘制显示 +@Override +public void showLrc(final String lrc) { + mDisc.setVisibility(View.GONE); + mLrcView.setVisibility(View.VISIBLE); + Log.d(TAG, "showLrc: " + mPlayStatusBinder.getMediaPlayer().getCurrentPosition()); + mLrcView.setLrc(lrc).setPlayer(mPlayStatusBinder.getMediaPlayer()).draw(); +} - }); - //随机播放 - randomLayout.setOnClickListener(view -> { - mPlayStatusBinder.setPlayMode(Constant.PLAY_RANDOM); - mPresenter.setPlayMode(Constant.PLAY_RANDOM); - popupWindow.dismiss(); - mPlayModeBtn.setBackground(getDrawable(R.drawable.play_mode_random)); - }); - //单曲循环 - singleLayout.setOnClickListener(view -> { - mPlayStatusBinder.setPlayMode(Constant.PLAY_SINGLE); - mPresenter.setPlayMode(Constant.PLAY_SINGLE); - popupWindow.dismiss(); - mPlayModeBtn.setBackground(getDrawable(R.drawable.play_mode_single)); - }); +// 获取歌词失败的处理方法,显示一个获取歌词失败的Toast提示信息,同时将相关错误内容(可能是歌曲ID等信息)保存到歌曲对象中,并调用FileUtil的方法保存歌曲信息更新到本地存储 +@Override +public void getLrcError(String content) { + showToast(getString(R.string.get_lrc_fail)); + mSong.setQqId(content); + FileUtil.saveSong(mSong); +} +// 设置本地歌曲ID的方法,将传入的歌曲ID设置到歌曲对象中,并调用FileUtil的方法保存歌曲信息更新到本地存储,用于保存本地歌曲相关的ID信息 +@Override +public void setLocalSongId(String songId) { + mSong.setQqId(songId); + FileUtil.saveSong(mSong); //保存 +} - } +// 获取歌曲ID成功的处理方法,调用setLocalSongId方法保存获取到的歌曲ID信息到本地存储,然后调用Presenter的方法根据歌曲ID获取本地歌词 +@Override +public void getSongIdSuccess(String songId) { + setLocalSongId(songId);//保存音乐信息 + mPresenter.getLrc(songId, Constant.SONG_LOCAL);//获取歌词 +} +// 保存歌词到本地的方法,调用FileUtil的方法将歌词内容保存到本地存储中,方便后续本地歌曲播放时直接使用歌词信息 +@Override +public void saveLrc(String lrc) { + FileUtil.saveLrcToNative(lrc, mSong.getSongName()); +} - @Override - public void onDestroy() { - super.onDestroy(); - unbindService(mPlayConnection); - unbindService(mDownloadConnection); - EventBus.getDefault().unregister(this); - stopUpdateSeekBarProgress(); - - //避免内存泄漏 - mMusicHandler.removeCallbacksAndMessages(null); +// 改变播放模式的方法,创建一个包含播放模式选项的布局视图,设置弹出窗口的相关属性(大小、背景、焦点、可触摸外部等),然后根据当前播放模式设置对应选项的选中状态,最后为各个播放模式选项设置点击监听器,实现切换播放模式的功能 +private void changePlayMode() { + // 从布局资源中加载播放模式选择的布局视图 + View playModeView = LayoutInflater.from(this).inflate(R.layout.play_mode, null); + // 获取顺序播放模式对应的布局容器 + ConstraintLayout orderLayout = playModeView.findViewById(R.id.orderLayout); + // 获取随机播放模式对应的布局容器 + ConstraintLayout randomLayout = playModeView.findViewById(R.id.randomLayout); + // 获取单曲循环播放模式对应的布局容器 + ConstraintLayout singleLayout = playModeView.findViewById(R.id.singleLayout); + // 获取顺序播放模式对应的TextView,用于显示和设置选中状态 + TextView orderTv = playModeView.findViewById(R.id.orderTv); + // 获取随机播放模式对应的TextView,用于显示和设置选中状态 + TextView randomTv = playModeView.findViewById(R.id.randomTv); + // 获取单曲循环播放模式对应的TextView,用于显示和设置选中状态 + TextView singleTv = playModeView.findViewById(R.id.singleTv); + + // 创建一个PopupWindow弹出窗口对象,设置其内容视图、宽度和高度(通过dip单位转换为像素) + PopupWindow popupWindow = new PopupWindow(playModeView, ScreenUtil.dip2px(this, 130), ScreenUtil.dip2px(this, 150)); + // 设置弹出窗口的背景Drawable,传入透明背景的Drawable资源,使其背景透明 + popupWindow.setBackgroundDrawable(getDrawable(R.color.transparent)); + // 设置弹出窗口获取焦点,使其能够接收按键等操作事件 + popupWindow.setFocusable(true); + // 设置弹出窗口可以触摸框以外的地方,点击外部区域可关闭弹出窗口 + popupWindow.setOutsideTouchable(true); + popupWindow.update(); + // 设置弹出窗口显示的位置,使其在播放模式按钮下方弹出(垂直偏移一定距离) + popupWindow.showAsDropDown(mPlayModeBtn, 0, -50); + + // 显示当前播放模式对应的选项为选中状态,根据Presenter获取到的当前播放模式,设置对应TextView的选中状态 + int mode = mPresenter.getPlayMode(); + if (mode == Constant.PLAY_ORDER) { + orderTv.setSelected(true); + randomTv.setSelected(false); + singleTv.setSelected(false); + } else if (mode == Constant.PLAY_RANDOM) { + randomTv.setSelected(true); + orderTv.setSelected(false); + singleTv.setSelected(false); + } else { + singleTv.setSelected(true); + randomTv.setSelected(false); + orderTv.setSelected(false); } - private DownloadInfo getDownloadInfoFromSong(Song song){ - DownloadInfo downloadInfo = new DownloadInfo(); - downloadInfo.setSinger(song.getSinger()); - downloadInfo.setProgress(0); - downloadInfo.setSongId(song.getSongId()); - downloadInfo.setUrl(song.getUrl()); - downloadInfo.setSongName(song.getSongName()); - downloadInfo.setDuration(song.getDuration()); - return downloadInfo; - } + // 顺序播放模式选项的点击监听器,设置播放模式为顺序播放,通知PlayerService和Presenter更新播放模式,关闭弹出窗口,并更新播放模式按钮的背景图片为顺序播放模式图标 + orderLayout.setOnClickListener(view -> { + mPlayStatusBinder.setPlayMode(Constant.PLAY_ORDER); //通知服务 + mPresenter.setPlayMode(Constant.PLAY_ORDER); + popupWindow.dismiss(); + mPlayModeBtn.setBackground(getDrawable(R.drawable.play_mode_order)); + + }); + // 随机播放模式选项的点击监听器,设置播放模式为随机播放,通知PlayerService和Presenter更新播放模式,关闭弹出窗口,并更新播放模式按钮的背景图片为随机播放模式图标 + randomLayout.setOnClickListener(view -> { + mPlayStatusBinder.setPlayMode(Constant.PLAY_RANDOM); + mPresenter.setPlayMode(Constant.PLAY_RANDOM); + popupWindow.dismiss(); + mPlayModeBtn.setBackground(getDrawable(R.drawable.play_mode_random)); + }); + // 单曲循环播放模式选项的点击监听器,设置播放模式为单曲循环,通知PlayerService和Presenter更新播放模式,关闭弹出窗口,并更新播放模式按钮的背景图片为单曲循环模式图标 + singleLayout.setOnClickListener(view -> { + mPlayStatusBinder.setPlayMode(Constant.PLAY_SINGLE); + mPresenter.setPlayMode(Constant.PLAY_SINGLE); + popupWindow.dismiss(); + mPlayModeBtn.setBackground(getDrawable(R.drawable.play_mode_single)); + }); +} +// Activity销毁时的回调方法,进行一些资源释放和清理操作,如解除与服务的绑定、注销EventBus注册、停止SeekBar进度更新、移除Handler中的所有回调和消息,避免内存泄漏 +@Override +public void onDestroy() { + super.onDestroy(); + unbindService(mPlayConnection); + unbindService(mDownloadConnection); + EventBus.getDefault().unregister(this); + stopUpdateSeekBarProgress(); + + //避免内存泄漏 + mMusicHandler.removeCallbacksAndMessages(null); +} +// 根据传入的歌曲对象创建一个DownloadInfo对象,用于传递歌曲下载相关的信息(如歌手、进度、歌曲ID、歌曲名称、时长等),方便后续传递给下载服务来启动下载任务。 +private DownloadInfo getDownloadInfoFromSong(Song song) { + DownloadInfo downloadInfo = new DownloadInfo(); + downloadInfo.setSinger(song.getSinger()); + downloadInfo.setProgress(0); + downloadInfo.setSongId(song.getSongId()); + downloadInfo.setUrl(song.getUrl()); + downloadInfo.setSongName(song.getSongName()); + downloadInfo.setDuration(song.getDuration()); + return downloadInfo; } diff --git a/app/src/main/java/com/example/musicplayer/view/WelcomeActivity.java b/app/src/main/java/com/example/musicplayer/view/WelcomeActivity.java index a1c4c4c..bd4f1c6 100644 --- a/app/src/main/java/com/example/musicplayer/view/WelcomeActivity.java +++ b/app/src/main/java/com/example/musicplayer/view/WelcomeActivity.java @@ -18,46 +18,61 @@ import com.example.musicplayer.R; import com.example.musicplayer.adapter.SongAdapter; import com.example.musicplayer.util.CommonUtil; +// WelcomeActivity类继承自AppCompatActivity,是安卓应用中用于展示欢迎界面的Activity类, +// 在这里主要负责处理欢迎界面相关的初始化操作、权限申请以及跳转到主界面的逻辑。 public class WelcomeActivity extends AppCompatActivity { - + // 重写onCreate方法,该方法在Activity创建时被调用,用于进行各种初始化操作,例如设置界面布局、申请权限等。 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - CommonUtil.hideStatusBar(this,true); + // 调用工具类CommonUtil中的方法隐藏状态栏,传入true表示隐藏状态栏,使欢迎界面能够全屏展示(具体效果取决于CommonUtil中方法的实现逻辑)。 + CommonUtil.hideStatusBar(this, true); + // 将窗口的背景Drawable设置为null,可能是为了实现某种特定的背景效果(比如透明背景等,具体需结合应用整体的UI设计来看)。 getWindow().setBackgroundDrawable(null); + // 设置该Activity的界面布局为activity_welcome,通过加载对应的布局资源文件来定义界面的外观和包含的控件等内容。 setContentView(R.layout.activity_welcome); - //申请权限 + // 申请权限部分,检查应用是否已经获取了写入外部存储的权限(Manifest.permission.WRITE_EXTERNAL_STORAGE)。 + // 如果没有获取到该权限(ContextCompat.checkSelfPermission返回的结果不等于PackageManager.PERMISSION_GRANTED), + // 则通过ActivityCompat.requestPermissions方法向用户请求该权限,传入当前Activity实例、需要请求的权限数组(这里只请求了写入外部存储权限)以及请求码(用于在权限申请结果回调中区分不同的权限申请)。 if (ContextCompat.checkSelfPermission(WelcomeActivity.this, Manifest.permission. - WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(WelcomeActivity.this, new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); } else { + // 如果已经获取了写入外部存储的权限,通过Handler发送一个延迟消息,延迟2000毫秒(2秒)后执行相应操作,这里会调用getHome方法跳转到主界面。 mHandler.sendEmptyMessageDelayed(0, 2000); } } + // 创建一个Handler对象,用于处理消息,在这里主要用于在合适的时机(延迟2秒后或者权限申请成功后)执行跳转到主界面的操作。 private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { + // 调用getHome方法,该方法用于启动主界面Activity并关闭当前的欢迎界面Activity,实现页面跳转。 getHome(); super.handleMessage(msg); } }; + // 定义getHome方法,用于启动主界面Activity(MainActivity)并关闭当前的欢迎界面Activity,实现从欢迎界面跳转到应用主界面的功能。 private void getHome() { startActivity(new Intent(WelcomeActivity.this, MainActivity.class)); finish(); } + // 重写onRequestPermissionsResult方法,该方法用于处理权限申请的结果回调,根据用户是否授予权限来执行不同的逻辑。 @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { switch (requestCode) { case 1: + // 如果请求码为1(对应之前请求写入外部存储权限时设置的请求码),并且权限授予结果数组长度大于0,且第一个元素(对应写入外部存储权限的授予结果)为PackageManager.PERMISSION_GRANTED(表示权限已授予), + // 则通过Handler发送一个延迟消息,延迟2000毫秒(2秒)后执行相应操作,这里同样会调用getHome方法跳转到主界面。 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { mHandler.sendEmptyMessageDelayed(0, 2000); } else { + // 如果权限被拒绝,通过Toast显示一个提示信息,告知用户拒绝该权限将无法使用程序,然后调用finish方法关闭当前的欢迎界面Activity。 Toast.makeText(this, "拒绝该权限无法使用该程序", Toast.LENGTH_SHORT).show(); finish(); } @@ -66,4 +81,4 @@ public class WelcomeActivity extends AppCompatActivity { break; } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/view/main/MainFragment.java b/app/src/main/java/com/example/musicplayer/view/main/MainFragment.java index 7e645f5..d9f1dc9 100644 --- a/app/src/main/java/com/example/musicplayer/view/main/MainFragment.java +++ b/app/src/main/java/com/example/musicplayer/view/main/MainFragment.java @@ -1,6 +1,5 @@ package com.example.musicplayer.view.main; - import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; @@ -39,139 +38,226 @@ import java.util.List; /** * A simple {@link Fragment} subclass. + * 这个类表示主界面的Fragment,作为音乐播放器主界面的主要展示和交互区域, + * 包含了展示歌单列表、各种音乐分类入口(如本地音乐、收藏、下载、历史播放等), + * 能够响应相关事件(如收藏歌单变化、不同类型歌曲数量变化等),并处理各部分的点击交互逻辑。 */ public class MainFragment extends Fragment { private static final String TAG = "MainFragment"; + // 自定义的可展开列表视图组件,用于展示如自建歌单、收藏歌单等相关歌单信息,支持分组展开和收缩查看详情 private MyListView myListView; + // 可展开列表视图的适配器,负责将歌单数据(分组及子项数据)适配到MyListView上进行展示,并处理相关点击等交互逻辑 private ExpandableListViewAdapter mAdapter; - private LinearLayout mLocalMusicLinear, mCollectionLinear, mHistoryMusicLinear,mDownloadLinear; - private TextView mLocalMusicNum, mLoveMusicNum, mHistoryMusicNum,mDownloadMusicNum; + // 用于展示本地音乐入口的线性布局,点击可进入本地音乐相关界面,方便用户操作 + private LinearLayout mLocalMusicLinear, + // 用于展示收藏歌单入口的线性布局,点击可进入收藏歌单相关界面 + mCollectionLinear, + // 用于展示历史播放音乐入口的线性布局,点击可进入历史播放音乐相关界面 + mHistoryMusicLinear, + // 用于展示下载音乐入口的线性布局,点击可进入下载音乐相关界面 + mDownloadLinear; + // 用于显示本地音乐数量的TextView组件,实时展示本地音乐的数量情况 + private TextView mLocalMusicNum, + // 用于显示收藏歌曲(例如“我喜欢”歌单中歌曲数量等)数量的TextView组件 + mLoveMusicNum, + // 用于显示历史播放音乐数量的TextView组件 + mHistoryMusicNum, + // 用于显示下载音乐数量的TextView组件 + mDownloadMusicNum; + // 用于触发搜索操作的按钮(可能是一个TextView,样式上作为按钮使用),点击可进入搜索相关界面 private TextView mSeekBtn; + // 存储歌单收藏信息的二维列表,外层列表按分组(如自建歌单、收藏歌单等)划分,内层列表存储每个分组下具体的歌单信息 private List> mAlbumCollectionList; + // 存储“我喜欢”歌单信息的列表,属于一种特殊的收藏歌单,单独列出方便操作和展示 private List mLoveAlbumList; + // 用于标记歌单展开和收缩状态的布尔变量,用于判断二级歌单展开或收缩的操作逻辑,辅助更新界面展示 private boolean twoExpand; + // 定义可展开列表视图中分组标题的字符串数组,这里包含了"自建歌单"和"收藏歌单"两个分组标题 private String[] mGroupStrings = {"自建歌单", "收藏歌单"}; - + /** + * 创建Fragment的视图,加载对应的布局文件,获取布局中的各个视图组件, + * 同时注册到EventBus用于接收相关事件(如歌单更新、歌曲数量变化等事件)。 + * @param inflater 用于将布局文件实例化为视图对象的布局填充器 + * @param container 父视图容器 + * @param savedInstanceState 保存的实例状态信息(如果有的话) + * @return 返回创建好的视图对象 + */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // 注册当前Fragment到EventBus,用于接收事件(例如歌单收藏变化、不同类型歌曲数量变化等事件,用于更新界面等操作) EventBus.getDefault().register(this); + // 加载fragment_main布局文件并创建视图对象 View view = inflater.inflate(R.layout.fragment_main, container, false); + // 从加载的布局中找到对应的本地音乐入口线性布局组件 mLocalMusicLinear = view.findViewById(R.id.linear_local_music); + // 从加载的布局中找到对应的收藏歌单入口线性布局组件 mCollectionLinear = view.findViewById(R.id.linear_collection); + // 从加载的布局中找到对应的功能区线性布局组件(从代码中看可能用于整体获取焦点等操作,具体作用看业务逻辑) LinearLayout mFunctionLinear = view.findViewById(R.id.linear_function); + // 从加载的布局中找到对应的历史播放音乐入口线性布局组件 mHistoryMusicLinear = view.findViewById(R.id.linear_history); - //获取焦点 + // 设置功能区线性布局可在触摸模式下获取焦点,可能用于处理焦点相关交互逻辑(比如键盘操作等场景下焦点管理) mFunctionLinear.setFocusableInTouchMode(true); + // 从加载的布局中找到对应的可展开列表视图组件 myListView = view.findViewById(R.id.expand_lv_song_list); + // 从加载的布局中找到对应的搜索按钮组件(这里是一个TextView作为按钮使用) mSeekBtn = view.findViewById(R.id.tv_seek); + // 从加载的布局中找到对应的本地音乐数量显示TextView组件 mLocalMusicNum = view.findViewById(R.id.tv_local_music_num); + // 从加载的布局中找到对应的收藏歌曲数量显示TextView组件 mLoveMusicNum = view.findViewById(R.id.tv_love_num); + // 从加载的布局中找到对应的历史播放音乐数量显示TextView组件 mHistoryMusicNum = view.findViewById(R.id.tv_history_num); + // 从加载的布局中找到对应的下载音乐数量显示TextView组件 mDownloadMusicNum = view.findViewById(R.id.tv_download_num); + // 从加载的布局中找到对应的下载音乐入口线性布局组件 mDownloadLinear = view.findViewById(R.id.downloadLinear); return view; } + /** + * 当Fragment所在的Activity创建完成后调用,主要用于调用展示歌单列表的方法以及设置各种点击事件的监听逻辑, + * 完成界面的初始化展示和交互功能配置。 + * @param savedInstanceState 保存的实例状态信息(如果有的话) + */ @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); + // 调用方法展示歌单列表信息,包括创建数据集合、初始化数据、设置适配器等操作 showAlbumList(); + // 调用方法设置各种点击事件的监听逻辑,如不同音乐分类入口点击、歌单展开收缩点击、歌单项点击等操作 onClick(); } + /** + * 展示歌单列表信息的方法,初始化“我喜欢”歌单列表和整体歌单收藏列表,添加默认的“我喜欢”歌单信息, + * 从数据库获取其他收藏歌单信息并整理排序,创建可展开列表视图的适配器并设置给对应的视图组件,用于展示歌单列表。 + */ private void showAlbumList() { + // 创建用于存储“我喜欢”歌单信息的空列表 mLoveAlbumList = new ArrayList<>(); + // 创建用于存储整体歌单收藏信息(按分组划分)的二维空列表 mAlbumCollectionList = new ArrayList<>(); + // 创建一个AlbumCollection对象,用于表示“我喜欢”歌单的基本信息(这里设置了名称和歌手名称示例信息) AlbumCollection albumCollection = new AlbumCollection(); albumCollection.setAlbumName("我喜欢"); albumCollection.setSingerName("残渊"); + // 将“我喜欢”歌单信息对象添加到对应的列表中 mLoveAlbumList.add(albumCollection); - mAlbumCollectionList.add(mLoveAlbumList); + // 从数据库中查询获取所有的AlbumCollection(歌单收藏)数据,调用orderCollection方法对数据进行逆序排列整理, + // 然后添加到整体歌单收藏列表中作为“收藏歌单”分组下的数据 mAlbumCollectionList.add(orderCollection(LitePal.findAll(AlbumCollection.class))); + // 创建可展开列表视图的适配器,传入当前Activity上下文、分组标题数组和整理好的歌单收藏列表数据, + // 用于将数据适配并展示到可展开列表视图上 mAdapter = new ExpandableListViewAdapter(getActivity(), mGroupStrings, mAlbumCollectionList); + // 将创建好的适配器设置给可展开列表视图组件,使其能够展示歌单列表信息 myListView.setAdapter(mAdapter); } - + /** + * 当Fragment被销毁时调用,进行一些清理操作,如注销在EventBus的注册等,避免内存泄漏等问题,释放相关资源。 + */ @Override public void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } + /** + * 当Fragment开始可见时调用,主要用于调用展示不同类型音乐数量的方法,更新界面上各音乐分类的数量显示情况。 + */ @Override public void onStart() { super.onStart(); showMusicNum(); } + /** + * 订阅EventBus的事件,当接收到AlbumCollectionEvent类型的事件时, + * 先清空歌单收藏列表数据,重新添加“我喜欢”歌单信息和整理后的其他收藏歌单信息, + * 根据之前记录的歌单展开收缩状态,通过展开和收缩操作来更新可展开列表视图的展示内容,达到更新歌单列表的功能。 + * @param event 接收到的AlbumCollectionEvent事件对象,可能包含了歌单收藏相关的变化信息(如新增、删除歌单等情况) + */ @Subscribe(threadMode = ThreadMode.MAIN) public void onMessageEvent(AlbumCollectionEvent event) { - mAlbumCollectionList.clear(); - mAlbumCollectionList.add(mLoveAlbumList); - mAlbumCollectionList.add(orderCollection(LitePal.findAll(AlbumCollection.class))); - //根据之前的状态,进行展开和收缩,从而达到更新列表的功能 - if (twoExpand) { - myListView.collapseGroup(1); - myListView.expandGroup(1); - } else { - myListView.expandGroup(1); - myListView.collapseGroup(1); - } + mAlbumCollectionList.clear(); + mAlbumCollectionList.add(mLoveAlbumList); + mAlbumCollectionList.add(orderCollection(LitePal.findAll(AlbumCollection.class))); + // 根据之前的状态,进行展开和收缩,从而达到更新列表的功能 + if (twoExpand) { + myListView.collapseGroup(1); + myListView.expandGroup(1); + } else { + myListView.expandGroup(1); + myListView.collapseGroup(1); + } } + /** + * 订阅EventBus的事件,当接收到SongListNumEvent类型的事件时,根据事件中传递的类型标识, + * 更新对应类型音乐(历史播放、本地音乐、下载音乐)在界面上显示的数量信息,实时展示不同类型音乐的数量变化情况。 + * @param event 接收到的SongListNumEvent事件对象,包含了音乐类型以及对应的数量变化相关信息 + */ @Subscribe(threadMode = ThreadMode.MAIN) - public void onSongListEvent(SongListNumEvent event){ + public void onSongListEvent(SongListNumEvent event) { int type = event.getType(); - if(type == Constant.LIST_TYPE_HISTORY){ + if (type == Constant.LIST_TYPE_HISTORY) { mHistoryMusicNum.setText(String.valueOf(LitePal.findAll(HistorySong.class).size())); - }else if(type == Constant.LIST_TYPE_LOCAL){ + } else if (type == Constant.LIST_TYPE_LOCAL) { mLocalMusicNum.setText(String.valueOf(LitePal.findAll(LocalSong.class).size())); - }else if(type == Constant.LIST_TYPE_DOWNLOAD){ + } else if (type == Constant.LIST_TYPE_DOWNLOAD) { mDownloadMusicNum.setText(String.valueOf(DownloadUtil.getSongFromFile(Api.STORAGE_SONG_FILE).size())); } } - - + /** + * 设置各种点击事件的监听逻辑的方法,包括不同音乐分类入口点击、歌单展开收缩点击、歌单项点击等操作, + * 通过点击相应的布局或列表项实现界面的跳转、数据更新等交互功能。 + */ private void onClick() { - //本地音乐 + // 为本地音乐入口线性布局设置点击事件监听,点击时跳转到本地音乐相关的Fragment(LocalFragment) mLocalMusicLinear.setOnClickListener(v -> replaceFragment(new LocalFragment())); - //搜索 + // 为搜索按钮设置点击事件监听,点击时跳转到搜索相关的Fragment(AlbumContentFragment.SearchFragment) mSeekBtn.setOnClickListener(v -> replaceFragment(new AlbumContentFragment.SearchFragment())); - //我的收藏 + // 为收藏歌单入口线性布局设置点击事件监听,点击时跳转到收藏歌单相关的Fragment(CollectionFragment) mCollectionLinear.setOnClickListener(v -> replaceFragment(new CollectionFragment())); - //下载 + // 为下载音乐入口线性布局设置点击事件监听,点击时跳转到下载音乐相关的Fragment(DownloadFragment) mDownloadLinear.setOnClickListener(view -> replaceFragment(new DownloadFragment())); - //最近播放 + // 为历史播放音乐入口线性布局设置点击事件监听,点击时跳转到历史播放音乐相关的Fragment(HistoryFragment) mHistoryMusicLinear.setOnClickListener(v -> replaceFragment(new HistoryFragment())); - //歌单点击展开 + // 为可展开列表视图设置分组展开监听事件,当某个分组展开时,如果是二级分组(索引为1,对应“收藏歌单”分组)展开, + // 则将twoExpand标记设置为true,表示二级歌单处于展开状态 myListView.setOnGroupExpandListener(groupPosition -> { if (groupPosition == 1) { twoExpand = true; } }); - //歌单点击收缩 + // 为可展开列表视图设置分组收缩监听事件,当某个分组收缩时,如果是二级分组(索引为1,对应“收藏歌单”分组)收缩, + // 则将twoExpand标记设置为false,表示二级歌单处于收缩状态 myListView.setOnGroupCollapseListener(groupPosition -> { if (groupPosition == 1) { twoExpand = false; } }); + // 为可展开列表视图设置分组点击事件监听,这里直接返回false,表示点击分组标题时不进行默认的展开收缩切换操作, + // 可能是为了自定义展开收缩逻辑或者避免与其他操作冲突,具体看业务需求 myListView.setOnGroupClickListener((parent, v, groupPosition, id) -> false); - //二级列表的item点击效果 + + // 为可展开列表视图的适配器设置子项(二级列表项,即具体歌单项)点击事件监听,点击时根据点击的位置进行不同的界面跳转操作 mAdapter.setOnChildItemClickListener((groupPosition, childPosition) -> { - //一级列表的第一个默认为我喜欢的歌单,故点击后跳转到我的收藏界面 + // 如果点击的是一级列表的第一个子项(通常对应“我喜欢”歌单,索引为0),则跳转到收藏歌单相关的Fragment(CollectionFragment) if (groupPosition == 0 && childPosition == 0) { replaceFragment(new CollectionFragment()); } else if (groupPosition == 1) { - //其他的列表都是我的收藏的列表,故跳转到专辑详细fragment + // 如果点击的是二级列表中的其他项(对应“收藏歌单”分组下的具体歌单),则获取对应的歌单信息, + // 跳转到专辑详细信息展示的Fragment(AlbumContentFragment),并传递歌单相关参数(如ID、名称、封面等)用于展示详细内容 AlbumCollection albumCollection = mAlbumCollectionList.get(groupPosition).get(childPosition); replaceFragment(AlbumContentFragment.newInstance( albumCollection.getAlbumId(), @@ -184,34 +270,60 @@ public class MainFragment extends Fragment { }); } - + /** + * 用于替换Fragment的方法,获取Activity的Fragment管理器,开启一个Fragment事务, + * 设置Fragment切换的动画效果,将传入的目标Fragment添加到指定的容器布局(R.id.fragment_container)中, + * 同时隐藏当前Fragment,并将事务添加到返回栈,以便用户可以通过返回键回到当前Fragment,最后提交事务完成切换操作。 + * @param fragment 要替换展示的目标Fragment实例,决定了点击操作后跳转到的具体界面内容 + */ private void replaceFragment(Fragment fragment) { FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); FragmentTransaction transaction = fragmentManager.beginTransaction(); - //进入和退出动画 + // 设置Fragment进入和退出的动画效果,分别对应进入、退出、从右侧进入、从右侧退出的动画资源ID transaction.setCustomAnimations(R.anim.fragment_in, R.anim.fragment_out, R.anim.slide_in_right, R.anim.slide_out_right); transaction.add(R.id.fragment_container, fragment); transaction.hide(this); - //将事务提交到返回栈 + // 将事务添加到返回栈,按下返回键时可以按照添加顺序反向执行事务,实现返回上一个Fragment的效果 transaction.addToBackStack(null); transaction.commit(); } - //显示数目 + /** + * 展示不同类型音乐数量的方法,通过查询LitePal数据库获取本地音乐、收藏歌曲、历史播放音乐的数量, + * 以及通过特定工具方法获取下载音乐的数量,然后将数量信息更新到对应的TextView组件上进行显示, + * 实现实时展示各类型音乐数量情况的功能。 + */ private void showMusicNum() { + // 获取本地歌曲数量,通过LitePal框架查询本地歌曲(LocalSong类对应的数据库表)的所有记录数量, + // 将数量转换为字符串并设置到对应的TextView组件上进行显示 mLocalMusicNum.setText(String.valueOf(LitePal.findAll(LocalSong.class).size())); + // 获取收藏歌曲(这里可能是“我喜欢”等收藏相关歌曲,Love类对应的数据库表)的数量, + // 通过LitePal框架查询所有记录数量,将数量转换为字符串并设置到对应的TextView组件上进行显示 mLoveMusicNum.setText(String.valueOf(LitePal.findAll(Love.class).size())); + // 获取历史播放歌曲数量,通过LitePal框架查询历史播放歌曲(HistorySong类对应的数据库表)的所有记录数量, + // 将数量转换为字符串并设置到对应的TextView组件上进行显示 mHistoryMusicNum.setText(String.valueOf(LitePal.findAll(HistorySong.class).size())); + // 获取下载歌曲数量,通过DownloadUtil工具类的特定方法(getSongFromFile方法,可能是从特定文件或存储位置获取下载歌曲信息) + // 获取下载歌曲列表,然后获取其数量,将数量转换为字符串并设置到对应的TextView组件上进行显示 mDownloadMusicNum.setText(String.valueOf(DownloadUtil.getSongFromFile(Api.STORAGE_SONG_FILE).size())); } - //使数据库中的列表逆序排列 + /** + * 使数据库中的列表逆序排列的方法,接收一个AlbumCollection类型的列表作为参数, + * 创建一个新的空列表,将传入列表中的元素从后往前(逆序)逐个添加到新列表中,最后返回整理后的逆序列表, + * 可用于对从数据库获取的歌单列表等数据按照特定顺序(如时间倒序等需求)进行展示。 + * @param tempList 从数据库获取的原始AlbumCollection列表数据,需要进行逆序排列整理 + * @return 返回逆序排列后的AlbumCollection列表数据 + */ private List orderCollection(List tempList) { + // 创建用于存储逆序排列后数据的空列表 List mAlbumCollectionList = new ArrayList<>(); + // 从传入列表的最后一个元素开始,逆序循环遍历每个元素 for (int i = tempList.size() - 1; i >= 0; i--) { + // 将当前元素添加到新创建的列表中 mAlbumCollectionList.add(tempList.get(i)); } return mAlbumCollectionList; } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/view/main/collection/CollectionFragment.java b/app/src/main/java/com/example/musicplayer/view/main/collection/CollectionFragment.java index beba7bb..79e32ac 100644 --- a/app/src/main/java/com/example/musicplayer/view/main/collection/CollectionFragment.java +++ b/app/src/main/java/com/example/musicplayer/view/main/collection/CollectionFragment.java @@ -1,6 +1,5 @@ package com.example.musicplayer.view.main.collection; - import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -36,108 +35,249 @@ import java.util.Objects; /** * Created by 残渊 on 2018/11/30. + * 这个类表示收藏歌曲列表展示的Fragment,用于展示用户收藏的歌曲相关信息, + * 可以与播放服务进行交互来实现播放收藏歌曲的功能,能响应歌曲收藏相关事件(如新增收藏、取消收藏等事件)以更新界面展示内容, + * 通过RecyclerView展示收藏歌曲的列表,并处理列表项的点击事件(如点击播放收藏歌曲等操作), + * 为用户提供管理和操作收藏歌曲的功能体验,是音乐播放器应用中收藏功能模块的重要部分。 */ - public class CollectionFragment extends Fragment { - private static final String TAG="CollectionFragment"; + private static final String TAG = "CollectionFragment"; + // 用于展示收藏歌曲列表的RecyclerView组件,通过设置适配器等展示收藏歌曲的相关信息, + // 如歌曲名称、歌手等,同时支持列表项的点击等交互操作(如点击播放歌曲等),方便用户操作收藏的歌曲。 private RecyclerView mRecycler; + // 用于返回上一界面的ImageView组件,通常点击该图标可实现返回功能,遵循常见的界面导航逻辑, + // 使得用户可以方便地回到之前浏览的页面,增强应用的操作便利性。 private ImageView mBackIv; + // 用于管理RecyclerView布局的线性布局管理器,设置收藏歌曲列表的布局方式(如垂直排列等), + // 决定了列表项在RecyclerView中的排列顺序和滚动方式等展示效果,方便用户上下滑动查看不同的收藏歌曲信息。 private LinearLayoutManager mManager; + // 收藏歌曲列表的适配器,负责将收藏歌曲的数据绑定到RecyclerView的每个Item视图上进行展示, + // 并处理相关点击等交互逻辑,例如设置每首歌曲在列表中的显示样式,以及响应点击播放等操作。 private LoveSongAdapter mAdapter; + // 存储收藏歌曲信息(Love类型)的列表,用于展示以及后续的操作(如根据收藏相关事件更新列表内容、获取歌曲信息用于播放等), + // 该列表的数据会从数据库或者其他存储方式获取并填充,然后传递给适配器用于展示在界面上。 private List mLoveList; + // 用于与播放服务进行交互的Binder对象,通过它可以调用播放服务中的方法,如播放收藏歌曲等操作, + // 在与服务成功连接后获取其实例,以便后续在需要播放歌曲时使用。 private PlayerService.PlayStatusBinder mPlayStatusBinder; + // 定义与播放服务的连接对象,用于监听服务的连接与断开状态,并获取服务提供的Binder对象进行交互, + // 是实现Fragment与播放服务通信的关键部分,保障了相关播放服务功能(如播放收藏歌曲等)的正常调用, + // 确保Fragment能和播放服务协同工作,实现完整的收藏歌曲播放相关功能逻辑。 private ServiceConnection connection = new ServiceConnection() { + /** + * 当与服务成功连接时调用,获取服务提供的IBinder对象,并强转为对应的PlayStatusBinder类型, + // 这一步是为了能通过强转后的对象调用播放服务中特定的、针对PlayStatusBinder类型定义的方法, + // 例如播放歌曲等操作对应的方法,从而实现对收藏歌曲的播放控制。 + * @param name 连接的服务的组件名称,标识所连接的服务在系统中的唯一性, + // 可以通过这个名称来区分不同的服务,确保连接到正确的目标服务。 + * @param service 服务返回的IBinder对象,通过它可以和服务进行交互通信, + // 是实现跨组件(Fragment与Service)交互的重要媒介,通过它可以调用服务端暴露的方法。 + */ @Override public void onServiceConnected(ComponentName name, IBinder service) { mPlayStatusBinder = (PlayerService.PlayStatusBinder) service; } + /** + * 当与服务意外断开连接时调用,这里暂时没有具体实现逻辑,可根据需求添加相关处理, + // 比如重新尝试连接、提示用户等操作,例如可以弹出一个Toast提示用户播放服务异常断开, + // 或者自动重新发起连接请求,以增强应用在面对服务异常情况时的稳定性和用户友好性。 + * @param name 断开连接的服务的组件名称,用于识别是哪个服务断开了连接, + // 方便根据不同服务进行针对性的异常处理(如果有多个服务连接的情况)。 + */ @Override public void onServiceDisconnected(ComponentName name) { } }; + /** + * 创建Fragment的视图,加载对应的布局文件,获取布局中的各个视图组件, + // 同时注册到EventBus用于接收相关事件(如歌曲收藏状态变化等事件),为后续操作做准备, + // 并创建一个空的收藏歌曲列表,用于后续填充收藏歌曲数据。 + * @param inflater 用于将布局文件实例化为视图对象的布局填充器, + // 它依据给定的布局资源文件,解析并创建对应的视图层次结构,是Android中构建界面视图的重要工具。 + * @param container 父视图容器,通常是Activity中用于承载Fragment视图的布局容器, + // Fragment的视图会被添加到这个容器中,以正确显示在Activity的界面上。 + * @param savedInstanceState 保存的实例状态信息(如果有的话), + // 可以用于在Fragment重建(比如屏幕旋转等情况)时恢复之前的界面状态、数据等内容,保持用户体验的连贯性。 + * @return 返回创建好的视图对象,该对象将作为Fragment的界面展示内容,添加到父视图容器中呈现给用户。 + */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // 加载fragment_love布局文件并创建视图对象,这一步根据指定的布局资源ID(R.layout.fragment_love), + // 使用布局填充器将布局文件解析并实例化为一个视图层次结构对象。 View mView = inflater.inflate(R.layout.fragment_love, container, false); + // 注册当前Fragment到EventBus,用于接收事件(例如歌曲新增收藏、取消收藏等相关事件), + // 这样当其他地方发布了与歌曲收藏相关的事件时,该Fragment能够接收到通知并做出相应的界面更新操作。 EventBus.getDefault().register(this); //注册事件订阅者 + // 从加载的布局中找到对应的用于展示收藏歌曲列表的RecyclerView组件,以便后续对其进行操作(如设置属性、添加适配器等)。 mRecycler = mView.findViewById(R.id.recycler_love_songs); + // 从加载的布局中找到对应的返回上一界面的ImageView组件,方便后续设置点击事件等操作。 mBackIv = mView.findViewById(R.id.iv_back); + // 创建一个空的收藏歌曲列表,后续将从数据库等途径获取收藏歌曲的详细信息并填充到该列表中。 mLoveList = new ArrayList<>(); return mView; } + /** + * 当Fragment所在的Activity创建完成后调用,主要用于启动播放服务并绑定服务以获取相关功能, + // 调用展示收藏歌曲列表的方法以及设置返回按钮点击事件监听的方法,完成界面的初始化及与播放服务建立通信连接, + // 为后续展示收藏歌曲列表以及播放歌曲操作做准备。 + * @param savedInstanceState 保存的实例状态信息(如果有的话),可用于恢复之前的相关状态(如果有需要), + // 不过在当前代码中未体现具体的恢复逻辑,一般可以用于一些诸如之前已经获取到的部分收藏歌曲相关数据等情况的恢复。 + */ @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - //启动服务 + // 创建启动播放服务的Intent对象,指定要启动的服务类为PlayerService,用于后续与播放服务的交互操作, + // Intent是Android中用于启动组件(如Activity、Service等)或者传递消息的重要机制,这里用于指定要启动的播放服务。 Intent playIntent = new Intent(getActivity(), PlayerService.class); + // 绑定播放服务,通过传入的ServiceConnection对象(connection)监听服务连接状态, + // 并设置绑定模式为自动创建服务(如果服务未启动则自动启动),确保播放服务能正常运行并与Fragment建立通信连接, + // 这一步操作使得Fragment能够与播放服务进行双向通信,获取服务提供的功能并向服务传递相关指令(如播放歌曲等)。 Objects.requireNonNull(getActivity()).bindService(playIntent, connection, Context.BIND_AUTO_CREATE); + // 调用方法展示收藏歌曲列表,包括从数据库获取数据、设置布局管理器、创建适配器以及设置列表项点击事件监听等操作, + // 使得RecyclerView能够正确展示收藏歌曲的相关信息,并能响应用户的点击播放等操作。 showSongList(); + // 调用方法设置返回按钮的点击事件监听,使得点击返回按钮时能够执行相应的返回上一界面的操作,遵循常见的界面导航逻辑。 onClick(); } + + /** + * 当Fragment被销毁时调用,进行一些清理操作,如注销在EventBus的注册、解除与播放服务的绑定等, + // 避免内存泄漏等问题,释放相关资源,确保应用的稳定运行和资源合理利用,同时也符合Android组件生命周期管理的规范要求。 + */ @Override - public void onDestroy(){ + public void onDestroy() { super.onDestroy(); + // 注销当前Fragment在EventBus的注册,防止接收不必要的事件造成潜在的异常或者资源浪费等情况, + // 确保Fragment在不再需要接收事件时停止相关监听操作。 EventBus.getDefault().unregister(this); + // 解除与播放服务的绑定,释放相关资源,避免因未正确关闭连接导致的资源泄漏问题。 Objects.requireNonNull(getActivity()).unbindService(connection); } + /** + * 订阅EventBus的事件,当接收到SongCollectionEvent类型的事件时,根据事件中的相关信息进行相应的界面更新操作, + // 先清空收藏歌曲列表,再重新从数据库获取收藏歌曲数据填充列表,通知RecyclerView的适配器数据集已改变,触发界面刷新, + // 如果事件表示是新增收藏歌曲且当前有正在播放歌曲(通过FileUtil.getSong()判断),则将RecyclerView滚动到对应歌曲位置进行展示, + // 确保界面展示与实际收藏歌曲情况保持一致,能及时反映收藏歌曲的变化情况,并在合适情况下定位到相关歌曲位置。 + * @param songCollectionEvent 接收到的SongCollectionEvent事件对象,包含了歌曲收藏相关的各种信息, + // 如是否是新增收藏(通过isLove()方法判断)等情况,根据这些信息来决定如何更新界面展示内容。 + */ @Subscribe(threadMode = ThreadMode.MAIN ) - public void onMessageEvent(SongCollectionEvent songCollectionEvent){ + public void onMessageEvent(SongCollectionEvent songCollectionEvent) { mLoveList.clear(); mLoveList.addAll(orderList(LitePal.findAll(Love.class))); mAdapter.notifyDataSetChanged(); - if(songCollectionEvent.isLove()){//定位歌曲 - if (FileUtil.getSong() != null) { + if (songCollectionEvent.isLove()) {//定位歌曲 + if (FileUtil.getSong()!= null) { + // 将RecyclerView滚动到指定位置,通过获取当前播放歌曲的位置信息(FileUtil.getSong().getPosition()), + // 并加上一定偏移量(这里是4),以合适的视觉效果展示歌曲所在位置,滚动高度依据RecyclerView自身高度进行调整, + // 实现当新增收藏歌曲且该歌曲正在播放时,在界面上定位到该歌曲的功能,方便用户查看。 mManager.scrollToPositionWithOffset(FileUtil.getSong().getPosition() + 4, mRecycler.getHeight()); } } } + /** + * 展示收藏歌曲列表的方法,设置RecyclerView的尺寸固定属性,从数据库获取收藏歌曲数据并进行逆序排列整理, + * 创建收藏歌曲列表的适配器,创建线性布局管理器并设置给RecyclerView,最后将适配器设置给RecyclerView用于展示收藏歌曲记录列表, + * 同时为收藏歌曲列表的适配器设置列表项点击事件监听,点击时进行一系列歌曲相关信息的设置(如歌曲名称、歌手、播放地址等), + * 保存歌曲信息到本地,并调用播放服务的方法开始播放对应收藏歌曲,完成列表展示及点击播放功能的配置工作。 + */ private void showSongList() { + // 设置RecyclerView的尺寸固定属性为true,这样可以提高RecyclerView的性能, + // 避免在数据变化时重复计算尺寸等操作,适用于列表项尺寸相对固定的情况(如这里的歌曲列表)。 mRecycler.setHasFixedSize(true); + // 调用orderList方法对从数据库获取的所有收藏歌曲数据(通过LitePal框架查询Love类对应的表数据)进行逆序排列整理, + // 逆序排列可能是为了按照某种特定顺序(如时间倒序等)展示收藏歌曲,更符合用户查看习惯。 mLoveList.addAll(orderList(LitePal.findAll(Love.class))); + // 创建收藏歌曲列表的适配器,传入当前Activity上下文和整理后的收藏歌曲列表数据,用于将数据绑定到视图上展示, + // 适配器负责将数据与RecyclerView的视图项进行绑定,设置每个视图项的展示样式,并处理相关交互逻辑(如点击事件)。 mAdapter = new LoveSongAdapter(getActivity(), mLoveList); + // 创建线性布局管理器,设置为垂直方向排列(默认情况),用于管理RecyclerView中收藏歌曲列表的布局展示, + // 确定列表项是垂直排列的,方便用户上下滑动查看不同的收藏歌曲信息,符合常见的列表展示习惯。 mManager = new LinearLayoutManager(getActivity()); + // 将线性布局管理器设置给RecyclerView,确定其布局方式,使得音乐列表能按照设定的方式展示, + // 确保RecyclerView的布局效果符合预期,方便用户查看和操作列表中的歌曲信息。 mRecycler.setLayoutManager(mManager); + // 将创建好的适配器设置给RecyclerView,使其能够展示收藏歌曲记录列表, + // 这样RecyclerView就能依据适配器提供的数据和展示逻辑,将收藏歌曲的信息展示给用户了。 mRecycler.setAdapter(mAdapter); + // 为收藏歌曲列表的适配器设置列表项点击事件监听,点击时进行以下操作,用于实现点击播放收藏歌曲的功能。 mAdapter.setOnItemClickListener(new OnItemClickListener() { @Override public void onClick(int position) { + // 获取点击位置对应的收藏歌曲信息对象,以便后续提取该歌曲的各项详细信息进行相关操作。 Love love = mLoveList.get(position); + // 创建一个Song对象,用于设置当前点击歌曲的相关信息,方便后续保存和播放操作, + // 这个Song对象可能是应用中统一用于表示歌曲信息的实体类,整合了歌曲各方面的关键属性。 Song song = new Song(); + // 设置歌曲的唯一标识,通常在数据库或者整个应用体系中有唯一性,用于准确区分不同歌曲。 song.setSongId(love.getSongId()); + // 设置歌曲的QQ标识(可能用于和QQ音乐等相关平台交互或者区分来源等情况,具体依据应用逻辑)。 song.setQqId(love.getQqId()); + // 设置歌曲的名称,用于在界面展示以及后续可能的查找、排序等操作中使用,方便用户识别歌曲。 song.setSongName(love.getName()); + // 设置歌曲的演唱者信息,同样有助于用户明确歌曲来源以及在界面展示等场景中提供更详细的歌曲描述。 song.setSinger(love.getSinger()); + // 设置歌曲是否为在线歌曲的标识,根据收藏歌曲记录中的对应属性进行赋值, + // 后续可能根据这个标识来决定播放时的获取方式(如在线播放还是本地播放等)。 song.setOnline(love.isOnline()); + // 设置歌曲的播放地址,这是实际播放歌曲时需要的关键信息,播放服务会依据这个地址来获取音频资源进行播放。 song.setUrl(love.getUrl()); + // 设置歌曲的封面图片地址(如果有的话),可用于在界面展示歌曲时显示对应的封面图片,提升视觉效果和用户体验。 song.setImgUrl(love.getPic()); + // 设置歌曲在列表中的位置索引,方便记录当前点击歌曲在列表中的顺序等相关操作,比如定位播放位置等。 song.setPosition(position); + // 设置歌曲的时长信息,用于在界面展示歌曲时长或者进行一些与时长相关的操作(如播放进度计算等)。 song.setDuration(love.getDuration()); + // 设置歌曲的媒体资源唯一标识(可能用于更细致地区分不同版本或者来源的媒体资源等情况)。 song.setMediaId(love.getMediaId()); + // 设置歌曲所属的列表类型为收藏类型,用于标识歌曲来源或者在应用中进行分类管理等操作, + // 例如在不同的播放逻辑或者统计场景下可以依据这个类型进行区分处理。 song.setListType(Constant.LIST_TYPE_LOVE); + // 通过FileUtil工具类将歌曲相关信息保存到本地,具体保存的方式和位置由FileUtil的实现决定, + // 可能是保存到本地文件或者数据库中,方便后续操作(如记录播放历史、恢复播放状态等), + // 确保应用能持久化用户的操作记录以及播放相关信息,提升用户体验的连贯性。 FileUtil.saveSong(song); + // 通过获取到的与播放服务交互的Binder对象(mPlayStatusBinder)调用播放服务中的播放方法, + // 传入Constant.LIST_TYPE_LOVE表示播放的是收藏类型的歌曲,从而实现点击收藏歌曲列表中的歌曲后进行播放的功能。 mPlayStatusBinder.play(Constant.LIST_TYPE_LOVE); } }); } + /** + * 设置返回按钮点击事件监听的方法,为绑定的返回上一界面的ImageView设置点击事件监听, + * 点击时通过Fragment管理器弹出当前Fragment所在的栈,返回上一界面,遵循常见的界面导航逻辑, + * 方便用户在界面间进行切换操作,使得用户可以按照自己的浏览路径方便地回退,提升应用的易用性。 + */ private void onClick() { mBackIv.setOnClickListener(v -> getActivity().getSupportFragmentManager().popBackStack()); } - private List orderList(List tempList){ - List loveList=new ArrayList<>(); + + /** + * 对传入的收藏歌曲列表数据进行逆序排列的方法,创建一个新的空列表, + * 将传入列表中的元素从后往前(逆序)逐个添加到新列表中,最后返回整理后的逆序列表, + * 可用于按照时间倒序等需求展示收藏歌曲记录,使得最新收藏的歌曲排在列表前面等展示效果,更符合用户查看习惯。 + * @param tempList 从数据库等获取的原始收藏歌曲列表数据,需要进行逆序排列整理, + * 它包含了从数据库获取到的所有收藏歌曲的信息集合,但是顺序可能不符合展示需求,需要进行调整。 + * @return 返回逆序排列后的收藏歌曲列表数据,这个返回的列表将用于后续在界面上展示收藏歌曲, + * 以更合理的顺序呈现给用户,方便用户快速找到最近收藏的歌曲等操作。 + */ + private List orderList(List tempList) { + List loveList = new ArrayList<>(); loveList.clear(); - for(int i=tempList.size()-1;i>=0;i--){ + for (int i = tempList.size() - 1; i >= 0; i--) { loveList.add(tempList.get(i)); } return loveList; } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/view/main/download/DownloadFragment.java b/app/src/main/java/com/example/musicplayer/view/main/download/DownloadFragment.java index 8abbe67..d329860 100644 --- a/app/src/main/java/com/example/musicplayer/view/main/download/DownloadFragment.java +++ b/app/src/main/java/com/example/musicplayer/view/main/download/DownloadFragment.java @@ -29,49 +29,118 @@ import butterknife.Unbinder; * time : 2019/09/16 * desc : 下载模块 * + * 这个类表示音乐播放器应用中下载功能的整体展示Fragment,作为下载模块的入口界面, + * 它整合了已下载歌曲和正在下载歌曲两个不同状态歌曲展示的相关Fragment,通过TabLayout与ViewPager的组合, + * 实现了以选项卡形式切换不同下载状态歌曲列表的功能,同时处理返回按钮的点击事件,方便用户在该模块内进行导航操作, + * 为用户提供了统一管理下载相关内容的交互界面。 */ - public class DownloadFragment extends Fragment { + // 通过ButterKnife框架绑定的返回上一界面的ImageView组件,点击该图标可实现返回功能, + // 遵循常见的界面导航逻辑,让用户可以方便地回到之前浏览的页面,增强应用的操作便利性。 @BindView(R.id.backIv) ImageView backIv; + // 通过ButterKnife框架绑定的TabLayout组件,用于展示选项卡,在这里它将提供不同下载状态(如已下载、正在下载)的切换标签, + // 用户可以通过点击不同标签切换到对应的下载歌曲列表展示页面,实现多页面切换展示的交互功能。 @BindView(R.id.tabLayout) TabLayout tabLayout; + // 通过ButterKnife框架绑定的ViewPager组件,它与TabLayout配合使用,用于承载和切换不同的Fragment页面, + // 根据TabLayout中选中的标签来展示对应的Fragment(如展示已下载歌曲列表的Fragment或者正在下载歌曲列表的Fragment), + // 提供了流畅的页面切换视觉效果和交互体验。 @BindView(R.id.page) ViewPager page; + // 用于解除ButterKnife框架绑定的对象,在合适的时机(如Fragment销毁时)需要调用其unbind方法来释放相关资源,避免内存泄漏。 private Unbinder unbinder; + // 存储选项卡标题文本的列表,用于设置TabLayout中每个选项卡显示的标题内容, + // 这里的标题对应不同的下载状态,比如“已下载”“正在下载”等,方便用户直观了解每个标签对应的功能和展示内容。 private List mTitleList; + // 存储要在ViewPager中展示的Fragment的列表,每个Fragment代表了不同下载状态歌曲列表的展示页面, + // 通过将对应的Fragment添加到该列表中,ViewPager就能根据用户在TabLayout中的选择切换展示不同的Fragment页面。 private List mFragments; + // 用于ViewPager的适配器,负责将Fragment列表与对应的标题列表关联起来,并管理ViewPager中Fragment页面的展示与切换逻辑, + // 它继承自合适的Adapter类(此处是自定义的TabAdapter),实现了将数据(Fragment和标题)适配到ViewPager组件上的功能。 private TabAdapter mAdapter; + // 定义了选项卡标题的字符串数组,包含了要在TabLayout中显示的各个选项卡的标题文本内容, + // 这里明确了有两个选项卡,分别对应“已下载”和“正在下载”两种下载歌曲的状态分类,方便后续初始化TabLayout时使用。 private String[] mTitles = {"已下载", "正在下载"}; + /** + * 创建Fragment的视图,加载对应的布局文件,通过ButterKnife框架绑定视图组件, + * 调用初始化选项卡相关设置的方法以及设置返回按钮点击事件监听的方法,为展示下载模块界面及交互功能做准备, + * 最后返回创建好的视图对象,该对象将作为Fragment的界面展示内容,添加到父视图容器中呈现给用户。 + * + * @param inflater 用于将布局文件实例化为视图对象的布局填充器, + * 它依据给定的布局资源文件,解析并创建对应的视图层次结构,是Android中构建界面视图的重要工具。 + * @param container 父视图容器,通常是Activity中用于承载Fragment视图的布局容器, + * Fragment的视图会被添加到这个容器中,以正确显示在Activity的界面上。 + * @param savedInstanceState 保存的实例状态信息(如果有的话), + * 可以用于在Fragment重建(比如屏幕旋转等情况)时恢复之前的界面状态、数据等内容,保持用户体验的连贯性。 + * @return 返回创建好的视图对象,该对象将作为Fragment的界面展示内容,添加到父视图容器中呈现给用户。 + */ @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + // 加载fragment_download布局文件并创建视图对象,这一步根据指定的布局资源ID(R.layout.fragment_download), + // 使用布局填充器将布局文件解析并实例化为一个视图层次结构对象。 View view = inflater.inflate(R.layout.fragment_download, container, false); + // 通过ButterKnife框架将视图与代码中的变量进行绑定,使得可以方便地操作布局中的视图组件, + // 例如通过绑定后的变量来设置ImageView、TabLayout、ViewPager等组件的属性、添加点击事件监听等操作。 unbinder = ButterKnife.bind(this, view); + // 调用方法初始化TabLayout和ViewPager相关设置,包括创建标题列表、添加要展示的Fragment、创建适配器以及关联TabLayout与ViewPager等操作, + // 完成下载模块界面中选项卡切换展示不同下载状态歌曲列表的初始化配置工作。 initTab(); + // 调用方法设置返回按钮的点击事件监听,使得点击返回按钮时能够执行相应的返回上一界面的操作,遵循常见的界面导航逻辑。 onClick(); return view; } + /** + * 初始化TabLayout和ViewPager相关设置的方法,创建标题列表和Fragment列表, + * 将预先定义好的选项卡标题添加到标题列表中,把代表已下载歌曲列表和正在下载歌曲列表的Fragment添加到Fragment列表中, + * 创建用于ViewPager的适配器并传入Fragment管理器、Fragment列表以及标题列表进行初始化, + * 设置ViewPager的适配器,最后将TabLayout与ViewPager进行关联,使得选项卡切换能正确控制ViewPager中Fragment页面的切换展示, + * 完成下载模块界面中选项卡与对应Fragment页面交互展示的初始化配置工作。 + */ private void initTab() { + // 创建一个空的用于存储选项卡标题文本的列表,后续将添加具体的标题内容(如“已下载”“正在下载”等)到该列表中。 mTitleList = new ArrayList<>(); + // 创建一个空的用于存储要在ViewPager中展示的Fragment的列表,后续将添加对应的Fragment实例到该列表中。 mFragments = new ArrayList<>(); + // 将预先定义好的选项卡标题字符串数组转换为列表形式,并添加到标题列表中,这样标题列表就包含了所有要在TabLayout中显示的标题内容, + // 用于后续设置TabLayout的各个选项卡的标题文本。 mTitleList.addAll(Arrays.asList(mTitles)); + // 创建一个代表已下载歌曲列表展示的Fragment实例,并添加到Fragment列表中,该Fragment负责展示已下载完成的歌曲相关信息及操作界面。 mFragments.add(new DownloadMusicFragment()); + // 创建一个代表正在下载歌曲列表展示的Fragment实例,并添加到Fragment列表中,该Fragment负责展示正在下载的歌曲相关信息及操作界面。 mFragments.add(new DownloadingFragment()); + // 创建用于ViewPager的适配器,传入当前Fragment的子Fragment管理器(通过getChildFragmentManager方法获取)、 + // Fragment列表以及标题列表,用于将这些数据关联起来,实现管理ViewPager中Fragment页面展示与切换的功能。 mAdapter = new TabAdapter(getChildFragmentManager(), mFragments, mTitleList); + // 将创建好的适配器设置给ViewPager,使得ViewPager能够依据适配器提供的信息来展示和切换不同的Fragment页面, + // 确定了ViewPager展示内容的来源和切换逻辑。 page.setAdapter(mAdapter); + // 将TabLayout与ViewPager进行关联,使得TabLayout中的选项卡切换操作能够同步控制ViewPager中Fragment页面的切换展示, + // 实现用户点击不同选项卡就能看到对应下载状态歌曲列表的交互功能,确保两者协同工作,提供流畅的界面切换体验。 tabLayout.setupWithViewPager(page); } - private void onClick(){ + + /** + * 设置返回按钮点击事件监听的方法,为绑定的返回上一界面的ImageView设置点击事件监听, + * 点击时通过Fragment管理器弹出当前Fragment所在的栈,返回上一界面,遵循常见的界面导航逻辑, + * 方便用户在界面间进行切换操作,使得用户可以按照自己的浏览路径方便地回退,提升应用的易用性。 + */ + private void onClick() { backIv.setOnClickListener(view -> Objects.requireNonNull(getActivity()).getSupportFragmentManager().popBackStack()); } + /** + * 当Fragment的视图即将被销毁时调用,进行一些清理操作,如解除ButterKnife框架的绑定, + * 释放相关资源,避免因未正确解除绑定关系导致的内存泄漏问题,确保应用的稳定运行和资源合理利用, + * 同时也符合Android组件生命周期管理的规范要求。 + */ @Override public void onDestroyView() { super.onDestroyView(); unbinder.unbind(); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/view/main/download/DownloadMusicFragment.java b/app/src/main/java/com/example/musicplayer/view/main/download/DownloadMusicFragment.java index 4ad56cd..db992d3 100644 --- a/app/src/main/java/com/example/musicplayer/view/main/download/DownloadMusicFragment.java +++ b/app/src/main/java/com/example/musicplayer/view/main/download/DownloadMusicFragment.java @@ -47,105 +47,240 @@ import static com.example.musicplayer.app.Api.STORAGE_SONG_FILE; * time : 2019/09/16 * desc : 下载歌曲列表 * + * 这个类表示下载歌曲列表展示的Fragment,用于展示已经下载完成的歌曲相关信息, + * 可以与播放服务进行交互来实现播放已下载歌曲的功能,能响应下载相关事件(如歌曲下载成功等事件)以更新界面展示内容, + * 通过RecyclerView展示下载歌曲的列表,并处理列表项的点击事件(如点击播放已下载歌曲等操作), + * 为用户提供管理和操作已下载歌曲的功能体验,是音乐播放器应用中下载功能模块用于展示已下载歌曲的重要部分。 */ - public class DownloadMusicFragment extends Fragment { private static final String TAG = "DownloadMusicFragment"; + // 通过ButterKnife框架绑定的RecyclerView视图组件,用于展示已下载歌曲的列表, + // 可以展示每首已下载歌曲的相关信息(如歌曲名称、歌手等),并支持列表项的点击等交互操作(如点击播放歌曲等)。 @BindView(R.id.songRecycle) RecyclerView songRecycle; + // 用于解除ButterKnife框架绑定的对象,在合适的时机(如Fragment销毁时)需要调用其unbind方法来释放相关资源,避免内存泄漏。 Unbinder unbinder; + // 用于管理RecyclerView布局的线性布局管理器,设置已下载歌曲列表的布局方式(如垂直排列等), + // 决定了列表项在RecyclerView中的排列顺序和滚动方式等展示效果,方便用户上下滑动查看不同的已下载歌曲信息。 private LinearLayoutManager mManager; + // 下载歌曲列表的适配器,负责将已下载歌曲的数据绑定到RecyclerView的每个Item视图上进行展示, + // 并处理相关点击等交互逻辑,例如设置每首歌曲在列表中的显示样式,以及响应点击播放等操作。 private DownloadSongAdapter mAdapter; + // 存储已下载歌曲信息(DownloadSong类型)的列表,用于展示以及后续的操作(如根据下载相关事件更新列表内容、获取歌曲信息用于播放等), + // 该列表的数据会从本地文件或者通过相关工具类获取并填充,然后传递给适配器用于展示在界面上。 private List mDownloadSongList; //已下载歌曲列表 + // 用于与播放服务进行交互的Binder对象,通过它可以调用播放服务中的方法,如播放已下载歌曲等操作, + // 在与服务成功连接后获取其实例,以便后续在需要播放歌曲时使用。 private PlayerService.PlayStatusBinder mPlayStatusBinder; + // 定义与播放服务的连接对象,用于监听服务的连接与断开状态,并获取服务提供的Binder对象进行交互, + // 是实现Fragment与播放服务通信的关键部分,保障了相关播放服务功能(如播放已下载歌曲等)的正常调用, + // 确保Fragment能和播放服务协同工作,实现完整的已下载歌曲播放相关功能逻辑。 private ServiceConnection connection = new ServiceConnection() { + /** + * 当与服务成功连接时调用,获取服务提供的IBinder对象,并强转为对应的PlayStatusBinder类型, + // 这一步是为了能通过强转后的对象调用播放服务中特定的、针对PlayStatusBinder类型定义的方法, + // 例如播放歌曲等操作对应的方法,从而实现对已下载歌曲的播放控制。 + * @param name 连接的服务的组件名称,标识所连接的服务在系统中的唯一性, + // 可以通过这个名称来区分不同的服务,确保连接到正确的目标服务。 + * @param service 服务返回的IBinder对象,通过它可以和服务进行交互通信, + // 是实现跨组件(Fragment与Service)交互的重要媒介,通过它可以调用服务端暴露的方法。 + */ @Override public void onServiceConnected(ComponentName name, IBinder service) { mPlayStatusBinder = (PlayerService.PlayStatusBinder) service; } + /** + * 当与服务意外断开连接时调用,这里暂时没有具体实现逻辑,可根据需求添加相关处理, + // 比如重新尝试连接、提示用户等操作,例如可以弹出一个Toast提示用户播放服务异常断开, + // 或者自动重新发起连接请求,以增强应用在面对服务异常情况时的稳定性和用户友好性。 + * @param name 断开连接的服务的组件名称,用于识别是哪个服务断开了连接, + // 方便根据不同服务进行针对性的异常处理(如果有多个服务连接的情况)。 + */ @Override public void onServiceDisconnected(ComponentName name) { } }; + /** + * 创建Fragment的视图,加载对应的布局文件,获取布局中的各个视图组件, + // 同时注册到EventBus用于接收相关事件(如歌曲下载成功、已下载歌曲有更新等事件), + // 并通过ButterKnife框架将视图与代码中的变量进行绑定,还创建了一个空的已下载歌曲列表,为后续操作做准备。 + * @param inflater 用于将布局文件实例化为视图对象的布局填充器, + // 它依据给定的布局资源文件,解析并创建对应的视图层次结构,是Android中构建界面视图的重要工具。 + * @param container 父视图容器,通常是Activity中用于承载Fragment视图的布局容器, + // Fragment的视图会被添加到这个容器中,以正确显示在Activity的界面上。 + * @param savedInstanceState 保存的实例状态信息(如果有的话), + // 可以用于在Fragment重建(比如屏幕旋转等情况)时恢复之前的界面状态、数据等内容,保持用户体验的连贯性。 + * @return 返回创建好的视图对象,该对象将作为Fragment的界面展示内容,添加到父视图容器中呈现给用户。 + */ @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + // 加载fragment_download_music布局文件并创建视图对象,这一步根据指定的布局资源ID(R.layout.fragment_download_music), + // 使用布局填充器将布局文件解析并实例化为一个视图层次结构对象。 View view = inflater.inflate(R.layout.fragment_download_music, container, false); + // 注册当前Fragment到EventBus,用于接收事件(例如歌曲下载成功、已下载歌曲列表有更新等相关事件), + // 这样当其他地方发布了与下载相关的事件时,该Fragment能够接收到通知并做出相应的界面更新操作。 EventBus.getDefault().register(this); + // 通过ButterKnife框架将视图与代码中的变量进行绑定,使得可以方便地操作布局中的视图组件, + // 例如通过绑定后的变量来设置RecyclerView的属性、添加点击事件监听等操作。 unbinder = ButterKnife.bind(this, view); + // 创建一个空的已下载歌曲列表,后续将从本地文件等途径获取已下载歌曲的详细信息并填充到该列表中。 mDownloadSongList = new ArrayList<>(); return view; } + /** + * 当Fragment所在的Activity创建完成后调用,主要用于启动播放服务并绑定服务以获取相关功能, + // 调用展示已下载歌曲列表的方法,完成界面的初始化及与播放服务建立通信连接,为后续展示已下载歌曲列表以及播放歌曲操作做准备。 + * @param savedInstanceState 保存的实例状态信息(如果有的话),可用于恢复之前的相关状态(如果有需要), + // 不过在当前代码中未体现具体的恢复逻辑,一般可以用于一些诸如之前已经获取到的部分已下载歌曲相关数据等情况的恢复。 + */ @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - //启动服务 + // 创建启动播放服务的Intent对象,指定要启动的服务类为PlayerService,用于后续与播放服务的交互操作, + // Intent是Android中用于启动组件(如Activity、Service等)或者传递消息的重要机制,这里用于指定要启动的播放服务。 Intent playIntent = new Intent(getActivity(), PlayerService.class); + // 绑定播放服务,通过传入的ServiceConnection对象(connection)监听服务连接状态, + // 并设置绑定模式为自动创建服务(如果服务未启动则自动启动),确保播放服务能正常运行并与Fragment建立通信连接, + // 这一步操作使得Fragment能够与播放服务进行双向通信,获取服务提供的功能并向服务传递相关指令(如播放歌曲等)。 Objects.requireNonNull(getActivity()).bindService(playIntent, connection, Context.BIND_AUTO_CREATE); + // 调用方法展示已下载歌曲列表,包括从本地文件获取数据、设置布局管理器、创建适配器以及设置列表项点击事件监听等操作, + // 使得RecyclerView能够正确展示已下载歌曲的相关信息,并能响应用户的点击播放等操作。 showSongList(); } - private void showSongList(){ + /** + * 展示已下载歌曲列表的方法,先从本地文件获取已下载歌曲数据并进行逆序排列整理, + // 创建已下载歌曲列表的适配器,创建线性布局管理器并设置给RecyclerView,最后将适配器设置给RecyclerView用于展示已下载歌曲记录列表, + // 同时为已下载歌曲列表的适配器设置列表项点击事件监听,点击时进行一系列歌曲相关信息的设置(如歌曲名称、歌手、播放地址等), + // 保存歌曲信息到本地,并调用播放服务的方法开始播放对应已下载歌曲,完成列表展示及点击播放功能的配置工作。 + */ + private void showSongList() { + // 调用orderList方法对从本地文件(通过DownloadUtil.getSongFromFile方法,依据指定的文件路径STORAGE_SONG_FILE获取) + // 获取到的已下载歌曲数据进行逆序排列整理,逆序排列可能是为了按照某种特定顺序(如时间倒序等)展示已下载歌曲,更符合用户查看习惯。 mDownloadSongList = orderList(DownloadUtil.getSongFromFile(STORAGE_SONG_FILE)); + // 创建已下载歌曲列表的适配器,传入当前Activity上下文和整理后的已下载歌曲列表数据,用于将数据绑定到视图上展示, + // 适配器负责将数据与RecyclerView的视图项进行绑定,设置每个视图项的展示样式,并处理相关交互逻辑(如点击事件)。 mAdapter = new DownloadSongAdapter(getActivity(), mDownloadSongList); + // 创建线性布局管理器,设置为垂直方向排列(默认情况),用于管理RecyclerView中已下载歌曲列表的布局展示, + // 确定列表项是垂直排列的,方便用户上下滑动查看不同的已下载歌曲信息,符合常见的列表展示习惯。 mManager = new LinearLayoutManager(getActivity()); + // 将线性布局管理器设置给RecyclerView,确定其布局方式,使得音乐列表能按照设定的方式展示, + // 确保RecyclerView的布局效果符合预期,方便用户查看和操作列表中的歌曲信息。 songRecycle.setLayoutManager(mManager); + // 将创建好的适配器设置给RecyclerView,使其能够展示已下载歌曲记录列表, + // 这样RecyclerView就能依据适配器提供的数据和展示逻辑,将已下载歌曲的信息展示给用户了。 songRecycle.setAdapter(mAdapter); + // 为已下载歌曲列表的适配器设置列表项点击事件监听,点击时进行以下操作,用于实现点击播放已下载歌曲的功能。 mAdapter.setOnItemClickListener(position -> { + // 获取点击位置对应的已下载歌曲信息对象,以便后续提取该歌曲的各项详细信息进行相关操作。 DownloadSong downloadSong = mDownloadSongList.get(position); + // 创建一个Song对象,用于设置当前点击歌曲的相关信息,方便后续保存和播放操作, + // 这个Song对象可能是应用中统一用于表示歌曲信息的实体类,整合了歌曲各方面的关键属性。 Song song = new Song(); + // 设置歌曲的唯一标识,通常在数据库或者整个应用体系中有唯一性,用于准确区分不同歌曲。 song.setSongId(downloadSong.getSongId()); + // 设置歌曲的名称,用于在界面展示以及后续可能的查找、排序等操作中使用,方便用户识别歌曲。 song.setSongName(downloadSong.getName()); + // 设置歌曲的演唱者信息,同样有助于用户明确歌曲来源以及在界面展示等场景中提供更详细的歌曲描述。 song.setSinger(downloadSong.getSinger()); + // 设置歌曲是否为在线歌曲的标识为false,表示是已下载的本地歌曲,用于后续播放等操作中区分歌曲来源类型。 song.setOnline(false); + // 设置歌曲的播放地址,这是实际播放歌曲时需要的关键信息,播放服务会依据这个地址来获取音频资源进行播放。 song.setUrl(downloadSong.getUrl()); - Log.d(TAG, "showSongList: "+song.getUrl()); + Log.d(TAG, "showSongList: " + song.getUrl()); + // 设置歌曲的封面图片地址(如果有的话),可用于在界面展示歌曲时显示对应的封面图片,提升视觉效果和用户体验。 song.setImgUrl(downloadSong.getPic()); + // 设置歌曲在列表中的位置索引,方便记录当前点击歌曲在列表中的顺序等相关操作,比如定位播放位置等。 song.setPosition(position); + // 设置歌曲的时长信息,用于在界面展示歌曲时长或者进行一些与时长相关的操作(如播放进度计算等)。 song.setDuration(downloadSong.getDuration()); + // 设置歌曲所属的列表类型为下载类型,用于标识歌曲来源或者在应用中进行分类管理等操作, + // 例如在不同的播放逻辑或者统计场景下可以依据这个类型进行区分处理。 song.setListType(Constant.LIST_TYPE_DOWNLOAD); + // 设置歌曲的媒体资源唯一标识(可能用于更细致地区分不同版本或者来源的媒体资源等情况)。 song.setMediaId(downloadSong.getMediaId()); + // 设置歌曲的下载状态为已下载,用于记录歌曲的下载情况等相关属性设置。 song.setDownload(true); + // 通过FileUtil工具类将歌曲相关信息保存到本地,具体保存的方式和位置由FileUtil的实现决定, + // 可能是保存到本地文件或者数据库中,方便后续操作(如记录播放历史、恢复播放状态等), + // 确保应用能持久化用户的操作记录以及播放相关信息,提升用户体验的连贯性。 FileUtil.saveSong(song); + // 通过获取到的与播放服务交互的Binder对象(mPlayStatusBinder)调用播放服务中的播放方法, + // 传入Constant.LIST_TYPE_DOWNLOAD表示播放的是已下载类型的歌曲,从而实现点击已下载歌曲列表中的歌曲后进行播放的功能。 mPlayStatusBinder.play(Constant.LIST_TYPE_DOWNLOAD); }); } + /** + * 订阅EventBus的事件,当接收到DownloadEvent类型的事件且事件中的下载状态表示歌曲下载成功时, + // 进行相应的界面更新操作,先清空已下载歌曲列表,再重新从本地文件获取已下载歌曲数据填充列表, + // 最后通知RecyclerView的适配器数据集已改变,触发界面刷新,展示最新的已下载歌曲列表情况, + // 确保界面展示与实际已下载歌曲情况保持一致,能及时反映新下载歌曲的添加情况。 + * @param event 接收到的DownloadEvent事件对象,包含了下载相关的各种信息,如下载状态、下载歌曲信息等, + // 根据事件携带的具体信息,来判断是否是歌曲下载成功的情况,进而决定如何更新界面展示内容。 + */ @Subscribe(threadMode = ThreadMode.MAIN) - public void onDownloadSuccessEvent(DownloadEvent event){ - if(event.getDownloadStatus() == Constant.TYPE_DOWNLOAD_SUCCESS){ + public void onDownloadSuccessEvent(DownloadEvent event) { + if (event.getDownloadStatus() == Constant.TYPE_DOWNLOAD_SUCCESS) { mDownloadSongList.clear(); mDownloadSongList.addAll(orderList(DownloadUtil.getSongFromFile(STORAGE_SONG_FILE))); mAdapter.notifyDataSetChanged(); } } + /** + * 订阅EventBus的事件,当接收到SongDownloadedEvent类型的事件时, + * 通知RecyclerView的适配器数据集已改变,触发界面刷新,用于在已下载歌曲有其他相关更新情况(具体由SongDownloadedEvent定义)时, + * 使得界面能够及时展示最新的已下载歌曲状态,保证界面与实际数据的一致性,提升用户体验。 + * @param event 接收到的SongDownloadedEvent事件对象,虽然这里没有对其内部具体携带的信息做详细处理, + * 但该事件可能包含了如歌曲元数据更新、本地存储相关变动等影响已下载歌曲展示的信息, + * 通过通知适配器更新界面来应对这些潜在的变化情况。 + */ @Subscribe(threadMode = ThreadMode.MAIN) - public void onSongDownloadedEvent(SongDownloadedEvent event){ + public void onSongDownloadedEvent(SongDownloadedEvent event) { mAdapter.notifyDataSetChanged(); } + /** + * 当Fragment的视图即将被销毁时调用,进行一些清理操作,如解除ButterKnife框架的绑定、注销在EventBus的注册等, + * 避免内存泄漏等问题,释放相关资源,确保应用的稳定运行和资源合理利用,同时也符合Android组件生命周期管理的规范要求。 + */ @Override public void onDestroyView() { super.onDestroyView(); + // 调用ButterKnife框架的unbind方法,解除视图与代码中变量的绑定关系,释放相关资源, + // 避免因绑定关系未正确解除而导致的内存泄漏问题。 unbinder.unbind(); + // 注销当前Fragment在EventBus的注册,防止接收不必要的事件造成潜在的异常或者资源浪费等情况, + // 确保Fragment在不再需要接收事件时停止相关监听操作。 EventBus.getDefault().unregister(this); } - private List orderList(List tempList){ - List loveList=new ArrayList<>(); + + /** + * 对传入的已下载歌曲列表数据进行逆序排列的方法,创建一个新的空列表, + * 将传入列表中的元素从后往前(逆序)逐个添加到新列表中,最后返回整理后的逆序列表, + * 可用于按照时间倒序等需求展示已下载歌曲记录,使得最新下载的歌曲排在列表前面等展示效果,更符合用户查看习惯。 + * @param tempList 从本地文件等获取的原始已下载歌曲列表数据,需要进行逆序排列整理, + * 它包含了从本地获取到的所有已下载歌曲的信息集合,但是顺序可能不符合展示需求,需要进行调整。 + * @return 返回逆序排列后的已下载歌曲列表数据,这个返回的列表将用于后续在界面上展示已下载歌曲, + * 以更合理的顺序呈现给用户,方便用户快速找到最近下载的歌曲等操作。 + */ + private List orderList(List tempList) { + List loveList = new ArrayList<>(); loveList.clear(); - for(int i=tempList.size()-1;i>=0;i--){ + for (int i = tempList.size() - 1; i >= 0; i--) { loveList.add(tempList.get(i)); } return loveList; } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/view/main/download/DownloadingFragment.java b/app/src/main/java/com/example/musicplayer/view/main/download/DownloadingFragment.java index 2399cc4..33c61e7 100644 --- a/app/src/main/java/com/example/musicplayer/view/main/download/DownloadingFragment.java +++ b/app/src/main/java/com/example/musicplayer/view/main/download/DownloadingFragment.java @@ -47,123 +47,241 @@ import butterknife.Unbinder; * time : 2019/09/16 * desc : 正在下载歌曲列表 * + * 这个类表示正在下载歌曲列表展示的Fragment,用于展示当前正在下载的歌曲相关信息, + * 可以与下载服务进行交互来控制下载操作(如暂停、取消等),能响应下载相关事件以更新界面展示内容, + * 通过RecyclerView展示正在下载歌曲的列表,并处理列表项的点击事件(如暂停、取消下载等操作), + * 为用户提供管理正在下载歌曲的功能体验,是音乐播放器应用中下载功能模块的重要组成部分。 */ - public class DownloadingFragment extends Fragment { private static final String TAG = "DownloadingFragment"; + // 通过ButterKnife框架绑定的RecyclerView视图组件,用于展示正在下载歌曲的列表, + // 可以展示每首正在下载歌曲的相关信息(如歌曲名称、下载进度等),并支持列表项的交互操作(如点击暂停、取消下载等)。 @BindView(R.id.songDowningRecycle) RecyclerView songDowningRecycle; + // 用于解除ButterKnife框架绑定的对象,在合适的时机(如Fragment销毁时)需要调用其unbind方法来释放相关资源,避免内存泄漏。 Unbinder unbinder; + // 正在下载歌曲列表的适配器,负责将正在下载歌曲的数据绑定到RecyclerView的每个Item视图上进行展示, + // 并处理相关点击等交互逻辑,例如设置每首歌曲在列表中的显示样式,以及响应点击暂停、取消下载等操作。 private DownloadingAdapter mAdapter; + // 用于管理RecyclerView布局的线性布局管理器,设置正在下载歌曲列表的布局方式(如垂直排列等), + // 决定了列表项在RecyclerView中的排列顺序和滚动方式等展示效果,方便用户上下滑动查看不同的正在下载歌曲信息。 private LinearLayoutManager mLinearLayoutManager; + // 存储正在下载歌曲信息(DownloadInfo类型)的列表,用于展示以及后续的操作(如根据下载状态更新列表项等), + // 该列表的数据会从数据库或者通过与下载服务交互等方式获取并填充,然后传递给适配器用于展示在界面上。 private List mDownloadInfoList; //下载队列 + // 用于与下载服务进行交互的Binder对象,通过它可以调用下载服务中的方法,如开始下载、暂停下载、取消下载等操作, + // 在与服务成功连接后获取其实例,以便后续在需要控制歌曲下载时使用。 private DownloadService.DownloadBinder mDownloadBinder; - //绑定下载服务 + // 定义与下载服务的连接对象,用于监听服务的连接与断开状态,并获取服务提供的Binder对象进行交互, + // 是实现Fragment与下载服务通信的关键部分,保障了相关下载服务功能(如控制下载操作等)的正常调用, + // 确保Fragment能和下载服务协同工作,实现完整的正在下载歌曲管理相关功能逻辑。 + // 绑定下载服务 private ServiceConnection mDownloadConnection = new ServiceConnection() { + /** + * 当与服务成功连接时调用,获取服务提供的IBinder对象,并强转为对应的DownloadBinder类型, + // 这一步是为了能通过强转后的对象调用下载服务中特定的、针对DownloadBinder类型定义的方法, + // 例如开始下载、暂停下载等操作对应的方法,从而实现对下载过程的控制。 + * @param componentName 连接的服务的组件名称,标识所连接的服务在系统中的唯一性, + // 可以通过这个名称来区分不同的服务,确保连接到正确的目标服务。 + * @param iBinder 服务返回的IBinder对象,通过它可以和服务进行交互通信, + // 是实现跨组件(Fragment与Service)交互的重要媒介,通过它可以调用服务端暴露的方法。 + */ @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { mDownloadBinder = (DownloadService.DownloadBinder) iBinder; } + /** + * 当与服务意外断开连接时调用,这里暂时没有具体实现逻辑,可根据需求添加相关处理, + // 比如重新尝试连接、提示用户等操作,例如可以弹出一个Toast提示用户下载服务异常断开, + // 或者自动重新发起连接请求,以增强应用在面对服务异常情况时的稳定性和用户友好性。 + * @param componentName 断开连接的服务的组件名称,用于识别是哪个服务断开了连接, + // 方便根据不同服务进行针对性的异常处理(如果有多个服务连接的情况)。 + */ @Override public void onServiceDisconnected(ComponentName componentName) { } }; + /** + * 创建Fragment的视图,加载对应的布局文件,获取布局中的各个视图组件, + // 同时注册到EventBus用于接收相关事件(如下载进度更新、下载完成等事件), + // 并通过ButterKnife框架将视图与代码中的变量进行绑定,为后续操作做准备。 + * @param inflater 用于将布局文件实例化为视图对象的布局填充器, + // 它依据给定的布局资源文件,解析并创建对应的视图层次结构,是Android中构建界面视图的重要工具。 + * @param container 父视图容器,通常是Activity中用于承载Fragment视图的布局容器, + // Fragment的视图会被添加到这个容器中,以正确显示在Activity的界面上。 + * @param savedInstanceState 保存的实例状态信息(如果有的话), + // 可以用于在Fragment重建(比如屏幕旋转等情况)时恢复之前的界面状态、数据等内容,保持用户体验的连贯性。 + * @return 返回创建好的视图对象,该对象将作为Fragment的界面展示内容,添加到父视图容器中呈现给用户。 + */ @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + // 加载fragment_download_ing布局文件并创建视图对象,这一步根据指定的布局资源ID(R.layout.fragment_download_ing), + // 使用布局填充器将布局文件解析并实例化为一个视图层次结构对象。 View view = inflater.inflate(R.layout.fragment_download_ing, container, false); + // 注册当前Fragment到EventBus,用于接收事件(例如下载进度更新、下载完成、下载取消等相关事件), + // 这样当其他地方发布了与下载相关的事件时,该Fragment能够接收到通知并做出相应的界面更新操作。 EventBus.getDefault().register(this); + // 通过ButterKnife框架将视图与代码中的变量进行绑定,使得可以方便地操作布局中的视图组件, + // 例如通过绑定后的变量来设置RecyclerView的属性、添加点击事件监听等操作。 unbinder = ButterKnife.bind(this, view); return view; } - + /** + * 当Fragment所在的Activity创建完成后调用,主要用于初始化RecyclerView相关设置、绑定下载服务, + // 完成界面的初始化及与下载服务建立通信连接,为后续展示正在下载歌曲列表以及控制下载操作做准备。 + * @param savedInstanceState 保存的实例状态信息(如果有的话),可用于恢复之前的相关状态(如果有需要), + // 不过在当前代码中未体现具体的恢复逻辑,一般可以用于一些诸如之前已经获取到的部分下载相关数据等情况的恢复。 + */ @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { + // 调用方法初始化RecyclerView相关设置,包括创建数据列表、设置布局管理器、创建适配器以及设置列表项点击事件监听等操作, + // 使得RecyclerView能够正确展示正在下载歌曲的相关信息,并能响应用户的操作(如暂停、取消下载等)。 initRecycler(); - //绑定服务 + // 创建启动下载服务的Intent对象,指定要启动的服务类为DownloadService,用于后续与下载服务的交互操作, + // Intent是Android中用于启动组件(如Activity、Service等)或者传递消息的重要机制,这里用于指定要启动的下载服务。 Intent downIntent = new Intent(getActivity(), DownloadService.class); + // 绑定下载服务,通过传入的ServiceConnection对象(mDownloadConnection)监听服务连接状态, + // 并设置绑定模式为自动创建服务(如果服务未启动则自动启动),确保下载服务能正常运行并与Fragment建立通信连接, + // 这一步操作使得Fragment能够与下载服务进行双向通信,获取服务提供的功能并向服务传递相关指令(如开始下载、暂停下载等)。 getActivity().bindService(downIntent, mDownloadConnection, Context.BIND_AUTO_CREATE); super.onActivityCreated(savedInstanceState); } + /** + * 初始化RecyclerView相关设置的方法,包括创建正在下载歌曲信息列表、从数据库获取数据填充列表、 + // 设置RecyclerView的相关属性(如解决进度刷新闪屏问题)、创建线性布局管理器、创建适配器以及设置列表项的点击事件监听(暂停和取消下载), + // 全面完成RecyclerView展示正在下载歌曲列表的初始化配置工作,使得界面能够正确展示正在下载歌曲的信息,并对用户的操作做出响应。 + */ private void initRecycler() { + // 创建用于存储正在下载歌曲信息的空列表,后续将从数据库或者其他途径获取正在下载歌曲的详细信息并填充到该列表中。 mDownloadInfoList = new LinkedList<>(); - mDownloadInfoList.addAll(LitePal.findAll(DownloadInfo.class,true)); + // 使用LitePal框架查询获取数据库中所有的正在下载歌曲信息(DownloadInfo类对应的表数据),并添加到正在下载歌曲信息列表中, + // LitePal是一个方便的Android数据库操作框架,简化了数据库查询等操作,这里用于获取存储在数据库中的正在下载歌曲记录信息。 + mDownloadInfoList.addAll(LitePal.findAll(DownloadInfo.class, true)); + // 设置RecyclerView的ItemAnimator为null,用于解决进度刷新时可能出现的闪屏问题, + // 这样在更新下载进度等操作导致界面刷新时,能有更平滑的视觉效果,提升用户体验。 songDowningRecycle.setItemAnimator(null); //解决进度刷新闪屏问题 + // 创建线性布局管理器,设置为垂直方向排列(默认情况),用于管理RecyclerView中正在下载歌曲列表的布局展示, + // 确定列表项是垂直排列的,方便用户上下滑动查看不同的正在下载歌曲信息,符合常见的列表展示习惯。 mLinearLayoutManager = new LinearLayoutManager(getActivity()); + // 创建正在下载歌曲列表的适配器,传入正在下载歌曲信息列表,用于将数据绑定到视图上展示, + // 适配器负责将数据与RecyclerView的视图项进行绑定,设置每个视图项的展示样式,并处理相关交互逻辑(如点击事件)。 mAdapter = new DownloadingAdapter(mDownloadInfoList); + // 将线性布局管理器设置给RecyclerView,确定其布局方式,使得音乐列表能按照设定的方式展示, + // 确保RecyclerView的布局效果符合预期,方便用户查看和操作列表中的歌曲信息。 songDowningRecycle.setLayoutManager(mLinearLayoutManager); + // 将创建好的适配器设置给RecyclerView,使其能够展示正在下载歌曲记录列表, + // 这样RecyclerView就能依据适配器提供的数据和展示逻辑,将正在下载歌曲的信息展示给用户了。 songDowningRecycle.setAdapter(mAdapter); - //暂停 + // 为正在下载歌曲列表的适配器设置列表项点击事件监听,用于处理暂停下载操作,点击时进行以下操作。 mAdapter.setOnItemClickListener(position -> { + // 获取点击位置对应的正在下载歌曲信息对象,以便后续根据其当前下载状态来决定执行开始下载还是暂停下载操作。 DownloadInfo downloadInfo = mDownloadInfoList.get(position); - Log.d(TAG, "initRecycler: "+mDownloadInfoList.get(position).getStatus()); - //判断是否为正在播放的歌曲 - if(downloadInfo.getStatus() == Constant.DOWNLOAD_PAUSED){ + Log.d(TAG, "initRecycler: " + mDownloadInfoList.get(position).getStatus()); + // 判断是否为正在播放的歌曲(这里可能通过歌曲的下载状态来判断,具体依据Constant.DOWNLOAD_PAUSED常量的定义), + // 如果当前歌曲的下载状态是暂停状态,则调用下载服务的方法开始下载该歌曲。 + if (downloadInfo.getStatus() == Constant.DOWNLOAD_PAUSED) { mDownloadBinder.startDownload(downloadInfo); - }else { - //传入要暂停的音乐id + } else { + // 如果歌曲不是暂停状态(可能是正在下载等其他状态),则传入要暂停的音乐id,调用下载服务的方法暂停该歌曲的下载。 mDownloadBinder.pauseDownload(downloadInfo.getSongId()); } }); - //取消下载 + // 为正在下载歌曲列表的适配器设置列表项点击删除(取消下载)事件监听,点击时进行以下操作, + // 通过弹出一个确认对话框来提示用户是否确认取消下载操作,用户确认后调用下载服务的方法取消对应歌曲的下载。 mAdapter.setOnDeleteClickListener(position -> { - SpeedDialog speedDialog = new SpeedDialog(getActivity(),SpeedDialog.SELECT_TYPE); + // 创建一个SpeedDialog对话框实例,用于显示确认取消下载的提示信息,设置对话框的类型为选择类型(可能包含确定、取消等按钮)。 + SpeedDialog speedDialog = new SpeedDialog(getActivity(), SpeedDialog.SELECT_TYPE); + // 设置对话框的标题,通过获取字符串资源来显示友好的提示标题,告知用户当前操作是取消下载。 speedDialog.setTitle(getString(R.string.download_cancel)) - .setMessage(getString(R.string.download_cancel_message)) - .setSureText(getString(R.string.download_sure)) - .setSureClickListener(dialog -> { + // 设置对话框的详细提示信息,同样通过获取字符串资源来显示更详细的提示内容,让用户明确取消下载的后果等信息。 + .setMessage(getString(R.string.download_cancel_message)) + // 设置对话框中确认按钮的文本内容,使用字符串资源来设置一个友好的确认按钮文本,如“确定”等。 + .setSureText(getString(R.string.download_sure)) + // 设置对话框中确认按钮的点击事件监听,当用户点击确认按钮时,调用下载服务的方法取消对应位置歌曲的下载, + // 传入要取消下载的歌曲信息对象(从正在下载歌曲信息列表中获取对应位置的歌曲信息)。 + .setSureClickListener(dialog -> { mDownloadBinder.cancelDownload(mDownloadInfoList.get(position)); }) - .show(); + // 显示对话框,将其展示给用户,等待用户进行操作(确认或取消),以决定是否取消对应歌曲的下载。 + .show(); }); } + /** + * 订阅EventBus的事件,当接收到DownloadEvent类型的事件时,根据事件中携带的下载状态信息, + * 进行相应的界面更新操作,如更新下载进度、刷新列表显示、处理下载成功或取消等情况, + * 实现根据下载过程中的不同状态变化及时更新界面展示内容的功能,保证用户看到的界面与实际下载情况一致。 + * @param event 接收到的DownloadEvent事件对象,包含了下载相关的各种信息,如下载状态、下载歌曲信息等, + * 根据事件携带的具体信息,来决定如何更新界面展示内容,例如如果是下载进度更新,就需要更新对应歌曲的进度条显示等。 + */ @Subscribe(threadMode = ThreadMode.MAIN) public void onDownloadingMessage(DownloadEvent event) { int status = event.getDownloadStatus(); if (status == Constant.TYPE_DOWNLOADING || status == Constant.TYPE_DOWNLOAD_PAUSED) {//进度条更新 DownloadInfo downloadInfo = event.getDownloadInfo(); + // 先从正在下载歌曲信息列表中移除对应位置的歌曲信息, mDownloadInfoList.remove(downloadInfo.getPosition()); - mDownloadInfoList.add(downloadInfo.getPosition(),downloadInfo); + // 再将更新后的歌曲信息添加回原来的位置,这样可以确保列表中对应歌曲的信息是最新的, + mDownloadInfoList.add(downloadInfo.getPosition(), downloadInfo); + // 通知RecyclerView的适配器对应位置的列表项数据已改变,触发界面更新,使得下载进度条等信息能及时刷新显示。 mAdapter.notifyItemChanged(downloadInfo.getPosition()); - }else if(status == Constant.TYPE_DOWNLOAD_SUCCESS){//歌曲下载成功 + } else if (status == Constant.TYPE_DOWNLOAD_SUCCESS) {//歌曲下载成功 + // 调用方法重新从数据库中读取需要下载的歌曲信息,更新正在下载歌曲信息列表, + // 因为下载成功后可能列表中的数据状态发生了变化(比如该歌曲不再处于正在下载状态了),需要重新获取最新数据。 resetDownloadInfoList(); + // 通知RecyclerView的适配器数据集已改变,触发整个RecyclerView界面刷新,展示最新的正在下载歌曲列表情况, + // 例如将已下载成功的歌曲从正在下载列表中移除等,保持界面展示与实际数据的一致性。 mAdapter.notifyDataSetChanged(); - }else if(status == Constant.TYPE_DOWNLOAD_CANCELED){//下载取消 + } else if (status == Constant.TYPE_DOWNLOAD_CANCELED) {//下载取消 + // 同样调用方法重新从数据库中读取需要下载的歌曲信息,更新正在下载歌曲信息列表, + // 下载取消后列表中的数据也需要更新,去除已取消下载的歌曲相关信息,获取最新的正在下载歌曲情况。 resetDownloadInfoList(); + // 通知RecyclerView的适配器数据集已改变,触发界面刷新,使得界面展示能及时反映下载取消后的列表变化情况, + // 让用户看到准确的正在下载歌曲状态。 mAdapter.notifyDataSetChanged(); - }else if(status == Constant.TYPE_DOWNLOAD_ADD){//添加了正在下载歌曲 + } else if (status == Constant.TYPE_DOWNLOAD_ADD) {//添加了正在下载歌曲 + // 当有新的歌曲添加到正在下载队列时,调用此方法重新从数据库中读取正在下载歌曲信息, + // 这样可以获取到包含新添加歌曲信息的最新列表数据,用于更新界面展示。 resetDownloadInfoList(); } } - - //重新从数据库中读取需要下载的歌曲 - private void resetDownloadInfoList(){ + // 重新从数据库中读取需要下载的歌曲 + private void resetDownloadInfoList() { + // 先清空正在下载歌曲信息列表,准备重新填充最新的数据。 mDownloadInfoList.clear(); - List temp = LitePal.findAll(DownloadInfo.class,true); - if(temp.size()!=0){ + // 使用LitePal框架查询获取数据库中所有的正在下载歌曲信息(DownloadInfo类对应的表数据), + // 如果查询到有相关数据,就将其添加到正在下载歌曲信息列表中,以更新列表内容。 + List temp = LitePal.findAll(DownloadInfo.class, true); + if (temp.size()!= 0) { mDownloadInfoList.addAll(temp); } } - - + /** + * 当Fragment的视图即将被销毁时调用,进行一些清理操作,如注销在EventBus的注册、解除ButterKnife框架的绑定等, + * 避免内存泄漏等问题,释放相关资源,确保应用的稳定运行和资源合理利用,同时也符合Android组件生命周期管理的规范要求。 + */ @Override public void onDestroyView() { super.onDestroyView(); + // 注销当前Fragment在EventBus的注册,防止接收不必要的事件造成潜在的异常或者资源浪费等情况, + // 确保Fragment在不再需要接收事件时停止相关监听操作。 EventBus.getDefault().unregister(this); + // 调用ButterKnife框架的unbind方法,解除视图与代码中变量的绑定关系,释放相关资源, + // 避免因绑定关系未正确解除而导致的内存泄漏问题。 unbinder.unbind(); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/view/main/history/HistoryFragment.java b/app/src/main/java/com/example/musicplayer/view/main/history/HistoryFragment.java index 657ec6b..e0ea0b7 100644 --- a/app/src/main/java/com/example/musicplayer/view/main/history/HistoryFragment.java +++ b/app/src/main/java/com/example/musicplayer/view/main/history/HistoryFragment.java @@ -36,103 +36,259 @@ import java.util.List; /** * Created by 残渊 on 2018/12/2. + * 这个类表示历史播放音乐展示的Fragment,用于展示用户之前播放过的音乐记录列表, + * 是音乐播放器应用中负责呈现历史播放相关界面及交互逻辑的关键部分, + * 它能与播放服务进行交互获取历史播放数据、响应相关事件以更新界面展示、通过RecyclerView来展示历史播放歌曲列表, + * 并处理列表项点击等交互操作,为用户提供查看和再次播放历史音乐的功能体验。 */ - public class HistoryFragment extends Fragment { - private static final String TAG="HistoryFragment"; + private static final String TAG = "HistoryFragment"; + // 用于展示历史播放音乐列表的RecyclerView组件,通过设置适配器等展示历史播放歌曲的相关信息, + // 如歌曲名称、歌手等,同时支持列表项的点击等交互操作,方便用户操作历史播放记录中的歌曲。 private RecyclerView mRecycler; + // 用于返回上一界面的ImageView组件,通常点击该图标可实现返回功能,遵循常见的界面导航逻辑, + // 使得用户可以方便地回到之前浏览的页面,增强应用的操作便利性。 private ImageView mBackIv; + // 用于管理RecyclerView布局的线性布局管理器,设置历史播放音乐列表的布局方式(如垂直排列等), + // 决定了列表项在RecyclerView中的排列顺序和滚动方式等展示效果,方便用户上下滑动查看不同的历史播放歌曲记录。 private LinearLayoutManager mManager; + // 历史播放音乐列表的适配器,负责将历史播放歌曲数据绑定到RecyclerView的每个Item视图上进行展示, + // 例如设置每首歌曲在列表中的显示样式,同时处理相关点击等交互逻辑,如响应点击某首歌曲时的播放等操作。 private HistoryAdapter mAdapter; + // 用于展示包含历史播放音乐列表的线性布局,可能用于整体布局调整或者和其他相关组件组合展示等用途, + // 比如可以在该线性布局内添加一些装饰性元素或者其他与列表相关的附属信息展示等。 private LinearLayout mSongListLinear; + // 用于在历史播放音乐列表为空时展示提示信息等相关布局的RelativeLayout组件,提示用户当前无历史播放记录, + // 例如可以在该布局中显示一段友好的提示文字,告知用户暂无历史播放歌曲可供查看,提升用户体验。 private RelativeLayout mEmptyRelative; + // 存储历史播放歌曲数据(HistorySong类型)的列表,用于展示以及后续的操作(如点击播放等), + // 该列表会从数据库或者其他存储方式获取历史播放歌曲的详细信息并填充,然后传递给适配器用于展示在界面上。 private List mHistoryList; + // 用于显示标题的TextView组件,从代码中看设置为“最近播放”,用于标识该界面展示的是历史播放相关内容, + // 让用户一眼能明确当前界面的功能和所展示数据的性质,符合界面设计的友好性原则。 private TextView mTitleTv; + // 用于与播放服务进行交互的Binder对象,通过它可以调用播放服务中的方法,如获取历史播放列表、控制播放等操作, + // 在与服务成功连接后获取其实例,以便后续在需要与播放服务交互时(比如播放历史歌曲、获取更多播放历史相关信息等操作)使用。 private PlayerService.PlayStatusBinder mPlayStatusBinder; + + // 定义与播放服务的连接对象,用于监听服务的连接与断开状态,并获取服务提供的Binder对象进行交互, + // 是实现Fragment与播放服务通信的关键部分,保障了相关播放服务功能(如获取历史播放列表、执行播放操作等)的正常调用, + // 确保Fragment能和播放服务协同工作,实现完整的历史播放相关功能逻辑。 private ServiceConnection connection = new ServiceConnection() { + /** + * 当与服务成功连接时调用,获取服务提供的IBinder对象,并强转为对应的PlayStatusBinder类型, + // 这一步是为了能通过强转后的对象调用播放服务中特定的、针对PlayStatusBinder类型定义的方法, + // 同时调用播放服务中的获取历史播放列表方法(mPlayStatusBinder.getHistoryList()), + // 用于获取用户之前的播放记录信息,以便后续展示在界面上,实现数据的获取和准备展示的操作流程。 + * @param name 连接的服务的组件名称,标识所连接的服务在系统中的唯一性, + // 可以通过这个名称来区分不同的服务,确保连接到正确的目标服务。 + * @param service 服务返回的IBinder对象,通过它可以和服务进行交互通信, + // 是实现跨组件(Fragment与Service)交互的重要媒介,通过它可以调用服务端暴露的方法。 + */ @Override public void onServiceConnected(ComponentName name, IBinder service) { mPlayStatusBinder = (PlayerService.PlayStatusBinder) service; mPlayStatusBinder.getHistoryList(); } + /** + * 当与服务意外断开连接时调用,这里暂时没有具体实现逻辑,可根据需求添加相关处理, + // 比如重新尝试连接、提示用户等操作,例如可以弹出一个Toast提示用户播放服务异常断开, + // 或者自动重新发起连接请求,以增强应用在面对服务异常情况时的稳定性和用户友好性。 + * @param name 断开连接的服务的组件名称,用于识别是哪个服务断开了连接, + // 方便根据不同服务进行针对性的异常处理(如果有多个服务连接的情况)。 + */ @Override public void onServiceDisconnected(ComponentName name) { } }; + /** + * 创建Fragment的视图,加载对应的布局文件,获取布局中的各个视图组件, + // 这是Fragment展示界面的基础操作,通过加载布局文件将定义好的界面结构实例化为视图对象, + // 同时注册到EventBus用于接收相关事件(如历史播放列表更新等事件),为后续操作做准备, + // 使得Fragment能够响应外部发布的相关事件,及时更新界面展示内容。 + * @param inflater 用于将布局文件实例化为视图对象的布局填充器, + // 它依据给定的布局资源文件,解析并创建对应的视图层次结构,是Android中构建界面视图的重要工具。 + * @param container 父视图容器,通常是Activity中用于承载Fragment视图的布局容器, + // Fragment的视图会被添加到这个容器中,以正确显示在Activity的界面上。 + * @param savedInstanceState 保存的实例状态信息(如果有的话), + // 可以用于在Fragment重建(比如屏幕旋转等情况)时恢复之前的界面状态、数据等内容,保持用户体验的连贯性。 + * @return 返回创建好的视图对象,该对象将作为Fragment的界面展示内容,添加到父视图容器中呈现给用户。 + */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // 加载fragment_love布局文件并创建视图对象(这里布局文件名可能不太准确对应功能,也许是复用的布局或者后续需要修改为更合适的名字), + // 这一步根据指定的布局资源ID(R.layout.fragment_love),使用布局填充器将布局文件解析并实例化为一个视图层次结构对象。 View view = inflater.inflate(R.layout.fragment_love, container, false); + // 注册当前Fragment到EventBus,用于接收事件(例如历史播放相关的更新事件等,用于更新界面等操作), + // 这样当其他地方发布了与历史播放相关的事件时,该Fragment能够接收到通知并做出相应的界面更新操作。 EventBus.getDefault().register(this); + // 从加载的布局中找到对应的用于展示历史播放音乐列表的RecyclerView组件, + // 通过视图对象的findViewById方法,依据布局文件中定义的组件ID,获取对应的视图实例,以便后续对其进行操作。 mRecycler = view.findViewById(R.id.recycler_love_songs); + // 从加载的布局中找到对应的返回上一界面的ImageView组件,同样是通过ID查找视图实例,方便后续设置点击事件等操作。 mBackIv = view.findViewById(R.id.iv_back); + // 从加载的布局中找到对应的在列表为空时展示提示信息的RelativeLayout组件,获取该布局实例用于后续控制其显示隐藏等操作。 mEmptyRelative = view.findViewById(R.id.relative_empty); + // 从加载的布局中找到对应的包含历史播放音乐列表的线性布局组件,以备后续对整个列表区域进行布局相关操作(如添加子视图、设置背景等)。 mSongListLinear = view.findViewById(R.id.linear_song_list); + // 从加载的布局中找到对应的用于显示标题的TextView组件,获取该实例用于设置标题文本等操作。 mTitleTv = view.findViewById(R.id.tv_title); + // 设置标题TextView的文本内容为“最近播放”,明确界面展示的是历史播放记录相关内容, + // 让用户能直观地了解当前界面所呈现信息的性质,符合界面设计的清晰易懂原则。 mTitleTv.setText("最近播放"); return view; } + /** + * 当Fragment所在的Activity创建完成后调用,主要用于启动播放服务并绑定服务以获取相关功能, + // 通过绑定服务建立与播放服务的通信连接,以便后续调用服务中的方法获取历史播放数据等操作, + // 调用展示历史播放歌曲列表的方法以及设置各种点击事件的监听逻辑,完成界面的初始化及交互功能配置, + // 使得界面能够正确展示历史播放歌曲列表,并对用户的操作(如点击歌曲、点击返回按钮等)做出响应。 + * @param savedInstanceState 保存的实例状态信息(如果有的话),可用于恢复之前的相关状态(如果有需要), + // 例如之前已经获取到的部分历史播放数据等情况,不过在当前代码中未体现具体的恢复逻辑。 + */ @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - //启动服务 + // 创建启动播放服务的Intent对象,指定要启动的服务类为PlayerService,用于后续与播放服务的交互操作, + // Intent是Android中用于启动组件(如Activity、Service等)或者传递消息的重要机制,这里用于指定要启动的播放服务。 Intent playIntent = new Intent(getActivity(), PlayerService.class); + // 绑定播放服务,通过传入的ServiceConnection对象监听服务连接状态, + // 并设置绑定模式为自动创建服务(如果服务未启动则自动启动),确保播放服务能正常运行并与Fragment建立通信连接, + // 这一步操作使得Fragment能够与播放服务进行双向通信,获取服务提供的功能并向服务传递相关指令(如获取历史播放列表、执行播放操作等)。 getActivity().bindService(playIntent, connection, Context.BIND_AUTO_CREATE); + // 调用方法展示历史播放歌曲列表,包括从数据库获取数据、设置适配器、布局管理器等操作,使得列表能正确展示, + // 完成RecyclerView展示历史播放歌曲的相关配置工作,让用户能够看到历史播放记录的具体内容。 showSongList(); + // 调用方法设置各种点击事件的监听逻辑,如历史播放歌曲列表项点击、返回上一界面点击等操作,实现交互功能, + // 通过设置点击事件监听,使得用户在界面上的操作能够触发相应的业务逻辑,如播放歌曲、返回上一界面等。 onClick(); } + + /** + * 当Fragment被销毁时调用,进行一些清理操作,如解除与播放服务的绑定、注销在EventBus的注册等, + // 解除服务绑定可以避免因未正确关闭连接导致的资源泄漏问题,注销EventBus注册防止接收不必要的事件造成的潜在异常, + // 避免内存泄漏等问题,释放相关资源,确保应用的稳定运行和资源合理利用,提高应用的性能和稳定性。 + */ @Override - public void onDestroy(){ + public void onDestroy() { super.onDestroy(); getActivity().unbindService(connection); EventBus.getDefault().unregister(this); } - @Subscribe(threadMode = ThreadMode.MAIN ) - public void onMessageEvent(SongHistoryEvent event){ + /** + * 订阅EventBus的事件,当接收到SongHistoryEvent类型的事件时, + // SongHistoryEvent可能是一个自定义的事件类,用于传递与历史播放歌曲相关的变化信息(如新增、删除记录等情况), + // 通知历史播放音乐列表适配器数据集已改变,触发RecyclerView更新界面展示内容, + // 通过调用适配器的notifyDataSetChanged方法,告知RecyclerView数据发生了变化,需要重新绘制界面来展示最新的数据状态, + // 实现历史播放列表相关数据变化时界面能及时更新展示的功能,保证用户看到的界面内容始终与实际数据一致。 + * @param event 接收到的SongHistoryEvent事件对象,包含了历史播放歌曲相关的变化信息(如新增、删除记录等情况), + // 根据事件携带的具体信息,来决定如何更新界面展示内容,例如如果是新增记录,就需要在列表中添加对应的展示项等。 + */ + @Subscribe(threadMode = ThreadMode.MAIN) + public void onMessageEvent(SongHistoryEvent event) { mAdapter.notifyDataSetChanged(); } + /** + * 展示历史播放歌曲列表的方法,先调用orderList方法对从数据库获取的历史播放歌曲数据进行逆序排列整理, + // 逆序排列可能是为了按照时间倒序等需求展示历史播放记录,使得最新播放的歌曲排在列表前面等展示效果,更符合用户查看习惯, + // 创建历史播放音乐列表的适配器,创建线性布局管理器并设置给RecyclerView,最后将适配器设置给RecyclerView用于展示历史播放记录列表, + // 完成列表展示的相关配置工作,使得RecyclerView能够正确展示历史播放歌曲的详细信息,方便用户查看和操作。 + */ private void showSongList() { + // 调用orderList方法对从数据库获取的所有历史播放歌曲数据(通过LitePal框架查询HistorySong类对应的表数据)进行逆序排列整理, + // LitePal是一个方便的Android数据库操作框架,简化了数据库查询等操作,这里用于获取存储在数据库中的历史播放歌曲记录信息。 mHistoryList = orderList(LitePal.findAll(HistorySong.class)); + // 创建历史播放音乐列表的适配器,传入当前Activity上下文和整理后的历史播放歌曲列表数据,用于将数据绑定到视图上展示, + // 适配器负责将数据与RecyclerView的视图项进行绑定,设置每个视图项的展示样式,并处理相关交互逻辑(如点击事件)。 mAdapter = new HistoryAdapter(getActivity(), mHistoryList); + // 创建线性布局管理器,设置为垂直方向排列(默认情况),用于管理RecyclerView中历史播放音乐列表的布局展示, + // 确定列表项是垂直排列的,方便用户上下滑动查看不同的历史播放歌曲记录,符合常见的列表展示习惯。 mManager = new LinearLayoutManager(getActivity()); + // 将线性布局管理器设置给RecyclerView,确定其布局方式,使得音乐列表能按照设定的方式展示, + // 确保RecyclerView的布局效果符合预期,方便用户查看和操作列表中的歌曲信息。 mRecycler.setLayoutManager(mManager); + // 将创建好的适配器设置给RecyclerView,使其能够展示历史播放记录列表, + // 这样RecyclerView就能依据适配器提供的数据和展示逻辑,将历史播放歌曲信息展示给用户了。 mRecycler.setAdapter(mAdapter); } + /** + * 设置各种点击事件的监听逻辑的方法,包括历史播放歌曲列表项点击事件和返回上一界面的点击事件。 + * 通过设置这些点击事件监听,实现了用户与界面的交互功能,使得用户的操作能够触发相应的业务逻辑, + * 点击历史播放歌曲列表项时,进行一系列歌曲相关信息的设置(如歌曲名称、歌手、播放地址等), + * 保存歌曲信息到本地,并调用播放服务的方法开始播放对应历史播放歌曲,实现用户再次播放历史歌曲的功能需求; + * 点击返回图标时,通过Fragment管理器返回上一界面,遵循常见的界面导航逻辑,方便用户在界面间进行切换操作。 + */ private void onClick() { + // 为历史播放音乐列表的适配器设置列表项点击事件监听,点击时进行以下操作, + // 这里定义了用户点击历史播放歌曲列表项时的具体响应逻辑,实现了播放历史歌曲的核心功能。 mAdapter.setOnItemClickListener(position -> { + // 获取点击位置对应的历史播放歌曲数据对象,以便后续提取该歌曲的各项详细信息进行相关操作。 HistorySong history = mHistoryList.get(position); + // 创建一个Song对象,用于设置当前点击歌曲的相关信息,方便后续保存和播放操作, + // 这个Song对象可能是应用中统一用于表示歌曲信息的实体类,整合了歌曲各方面的关键属性。 Song song = new Song(); + // 设置歌曲的唯一标识,通常在数据库或者整个应用体系中有唯一性,用于准确区分不同歌曲。 song.setSongId(history.getSongId()); + // 设置歌曲的名称,用于在界面展示以及后续可能的查找、排序等操作中使用,方便用户识别歌曲。 song.setSongName(history.getName()); + // 设置歌曲的演唱者信息,同样有助于用户明确歌曲来源以及在界面展示等场景中提供更详细的歌曲描述。 song.setSinger(history.getSinger()); + // 设置歌曲是否为在线歌曲的标识,根据历史播放记录中对应歌曲的该属性进行赋值, + // 后续可能根据这个标识来决定播放时的获取方式(如在线播放还是本地播放等)。 song.setOnline(history.isOnline()); + // 设置歌曲的播放地址,这是实际播放歌曲时需要的关键信息,播放服务会依据这个地址来获取音频资源进行播放。 song.setUrl(history.getUrl()); + // 设置歌曲的封面图片地址(如果有的话),可用于在界面展示歌曲时显示对应的封面图片,提升视觉效果和用户体验。 song.setImgUrl(history.getPic()); + // 设置歌曲在列表中的位置索引,方便记录当前点击歌曲在列表中的顺序等相关操作,比如定位播放位置等。 song.setPosition(position); + // 设置歌曲的时长信息,用于在界面展示歌曲时长或者进行一些与时长相关的操作(如播放进度计算等)。 song.setDuration(history.getDuration()); + // 设置歌曲的媒体资源唯一标识(可能用于更细致地区分不同版本或者来源的媒体资源等情况)。 song.setMediaId(history.getMediaId()); + // 设置歌曲所属的列表类型为历史播放类型,用于标识歌曲来源或者在应用中进行分类管理等操作, + // 例如在不同的播放逻辑或者统计场景下可以依据这个类型进行区分处理。 song.setListType(Constant.LIST_TYPE_HISTORY); + // 通过FileUtil工具类将歌曲相关信息保存到本地,具体保存的方式和位置由FileUtil的实现决定, + // 可能是保存到本地文件或者数据库中,方便后续操作(如记录播放历史、恢复播放状态等), + // 确保应用能持久化用户的操作记录以及播放相关信息,提升用户体验的连贯性。 FileUtil.saveSong(song); + // 通过获取到的与播放服务交互的Binder对象(mPlayStatusBinder)调用播放服务中的播放方法, + // 传入Constant.LIST_TYPE_HISTORY表示播放的是历史播放类型的歌曲,从而实现点击历史播放列表中的歌曲后进行播放的功能, + // 这一步是触发实际播放操作的关键,通过与播放服务的交互,让用户能够再次收听之前播放过的歌曲。 mPlayStatusBinder.play(Constant.LIST_TYPE_HISTORY); }); + // 为返回上一界面的ImageView设置点击事件监听,点击时通过Fragment管理器弹出当前Fragment所在的栈,返回上一界面, + // 遵循常见的界面导航逻辑,方便用户在界面间进行切换操作,使得用户可以按照自己的浏览路径方便地回退,提升应用的易用性。 mBackIv.setOnClickListener(v -> getActivity().getSupportFragmentManager().popBackStack()); } - private List orderList(List tempList){ - List historyList=new ArrayList<>(); + + /** + * 对传入的历史播放歌曲列表数据进行逆序排列的方法,创建一个新的空列表, + * 将传入列表中的元素从后往前(逆序)逐个添加到新列表中,最后返回整理后的逆序列表, + * 可用于按照时间倒序等需求展示历史播放记录,使得最新播放的歌曲排在列表前面等展示效果,更符合用户查看习惯。 + * @param tempList 从数据库获取的原始历史播放歌曲列表数据,需要进行逆序排列整理, + * 它包含了从数据库查询出来的所有历史播放歌曲的信息集合,但是顺序可能不符合展示需求,需要进行调整。 + * @return 返回逆序排列后的历史播放歌曲列表数据,这个返回的列表将用于后续在界面上展示历史播放歌曲, + * 以更合理的顺序呈现给用户,方便用户快速找到最近播放的歌曲等操作。 + */ + private List orderList(List tempList) { + List historyList = new ArrayList<>(); historyList.clear(); - for(int i=tempList.size()-1;i>=0;i--){ + for (int i = tempList.size() - 1; i >= 0; i--) { historyList.add(tempList.get(i)); } return historyList; } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/view/main/local/LocalFragment.java b/app/src/main/java/com/example/musicplayer/view/main/local/LocalFragment.java index 19081da..3b2267d 100644 --- a/app/src/main/java/com/example/musicplayer/view/main/local/LocalFragment.java +++ b/app/src/main/java/com/example/musicplayer/view/main/local/LocalFragment.java @@ -36,41 +36,77 @@ import java.util.Objects; import butterknife.BindView; - +/** + * 这个类表示本地音乐展示的Fragment,用于展示本地存储的音乐列表, + * 遵循MVP模式,实现了ILocalContract.View接口定义的视图相关操作规范, + * 同时继承自BaseMvpFragment,具备与Presenter交互、处理服务连接、响应相关事件以及管理本地音乐列表展示等功能, + * 是音乐播放器中负责呈现本地音乐相关界面及交互逻辑的重要部分。 + */ public class LocalFragment extends BaseMvpFragment implements ILocalContract.View { private static final String TAG = "LocalFragment"; + // 通过ButterKnife框架绑定的RecyclerView视图组件,用于展示本地音乐列表,支持列表项的展示和交互操作, + // 例如通过设置适配器展示每首本地音乐的相关信息,并响应列表项的点击等事件。 @BindView(R.id.normalView) RecyclerView mRecycler; + // 通过ButterKnife框架绑定的ImageView视图组件,可能用于触发查找本地音乐等相关操作(从代码逻辑推测), + // 比如点击后可以重新扫描本地存储设备以获取最新的本地音乐文件信息等。 @BindView(R.id.iv_find_local_song) ImageView mFindLocalMusicIv; + // 通过ButterKnife框架绑定的ImageView视图组件,通常用于点击返回上一界面等操作,提供返回功能, + // 方便用户在该界面导航回之前浏览的页面,遵循常见的界面交互逻辑。 @BindView(R.id.iv_back) ImageView mBackIv; + // 通过ButterKnife框架绑定的RelativeLayout视图组件,可能用于在本地音乐列表为空时展示提示信息等相关布局展示, + // 例如显示一个友好的提示文本告知用户当前没有本地音乐可供播放等情况。 @BindView(R.id.linear_empty) RelativeLayout mEmptyViewLinear; - + // 存储本地音乐数据(LocalSong类型)的列表,用于展示以及后续的操作(如点击播放等), + // 该列表会从数据库或者本地文件扫描等方式获取本地音乐信息并填充,然后传递给适配器用于展示在界面上。 private List mLocalSongsList; + // 本地音乐的Presenter对象,用于处理业务逻辑,如获取本地音乐数据、与数据层交互等,遵循MVP模式, + // 它负责处理和协调视图层(本Fragment)与数据层之间的通信和操作,使得界面展示与数据获取等逻辑分离。 private LocalPresenter mPresenter; + // 本地音乐列表的适配器,负责将本地音乐数据绑定到RecyclerView的每个Item视图上进行展示, + // 并处理相关点击等交互逻辑,例如设置每首音乐在列表中的显示样式,以及响应点击某首音乐时的播放等操作。 private SongAdapter songAdapter; + // 用于管理RecyclerView布局的线性布局管理器,设置列表的布局方式(如垂直排列等), + // 决定了本地音乐列表在RecyclerView中的排列顺序和滚动方式等展示效果。 private LinearLayoutManager layoutManager; - //在onServiceConnected中获取PlayStatusBinder的实例,从而调用服务里面的方法 + // 用于与播放服务进行交互的Binder对象,通过它可以调用播放服务中的方法,如控制本地音乐播放、暂停、切换等操作, + // 在onServiceConnected方法中获取其实例,以便后续在用户操作音乐播放相关功能时使用。 private PlayerService.PlayStatusBinder mPlayStatusBinder; - + // 定义与播放服务的连接对象,用于监听服务的连接与断开状态,并获取服务提供的Binder对象进行交互, + // 是实现Fragment与播放服务通信的关键部分,确保播放相关操作能正确执行。 private ServiceConnection connection = new ServiceConnection() { + /** + * 当与服务成功连接时调用,获取服务提供的IBinder对象,并强转为对应的PlayStatusBinder类型, + * 以便后续调用播放服务中的方法进行播放控制等操作,例如调用播放、暂停、下一首等方法来实现音乐播放功能。 + * @param name 连接的服务的组件名称,标识所连接的服务在系统中的唯一性。 + * @param service 服务返回的IBinder对象,通过它可以和服务进行交互通信。 + */ @Override public void onServiceConnected(ComponentName name, IBinder service) { mPlayStatusBinder = (PlayerService.PlayStatusBinder) service; } + /** + * 当与服务意外断开连接时调用,这里暂时没有具体实现逻辑,可根据需求添加相关处理, + * 比如重新尝试连接、提示用户等操作,例如可以弹出一个Toast提示用户播放服务异常断开,或者自动重新发起连接请求。 + * @param name 断开连接的服务的组件名称,用于识别是哪个服务断开了连接。 + */ @Override public void onServiceDisconnected(ComponentName name) { } }; - + /** + * 当Fragment被销毁时调用,进行一些清理操作,如解除与播放服务的绑定、注销在EventBus的注册等, + * 避免内存泄漏等问题,释放相关资源,确保应用的稳定性和资源的合理利用。 + */ @Override public void onDestroy() { super.onDestroy(); @@ -78,47 +114,92 @@ public class LocalFragment extends BaseMvpFragment implements IL EventBus.getDefault().unregister(this); } + /** + * 获取与当前Fragment关联的本地音乐Presenter对象,创建一个LocalPresenter实例并返回, + * 以符合MVP模式中视图与Presenter的交互要求,确保Presenter能处理相关业务逻辑并与视图进行通信, + * 使得Fragment专注于界面展示,而业务逻辑处理交给Presenter来完成。 + * @return 返回本地音乐Presenter对象,即创建的LocalPresenter实例,用于后续与数据层交互等操作。 + */ @Override protected LocalPresenter getPresenter() { mPresenter = new LocalPresenter(); return mPresenter; } + /** + * 初始化视图相关操作的方法,先调用父类的initView方法(可能有一些基础的视图初始化逻辑), + * 然后注册到EventBus用于接收事件,调用注册和绑定服务的方法以及初始化本地音乐RecyclerView的相关设置, + * 完成该Fragment界面相关的初始化准备工作,为后续展示音乐列表和响应交互操作做铺垫。 + */ @Override public void initView() { super.initView(); + // 注册当前Fragment到EventBus,用于接收事件(例如本地音乐相关的更新事件等,用于更新界面等操作), + // 这样当有相关事件发布时,该Fragment可以及时响应并更新界面显示内容。 EventBus.getDefault().register(this); + // 调用方法注册和绑定播放服务,用于后续与播放服务的交互操作,确保能正确控制音乐播放。 registerAndBindReceive(); + // 调用方法初始化本地音乐RecyclerView的相关设置,如设置布局管理器、加载本地音乐数据、设置适配器等操作, + // 使得RecyclerView能正确展示本地音乐列表信息。 initLocalRecycler(); } + /** + * 订阅EventBus的事件,当接收到SongLocalEvent类型的事件时, + * 通知本地音乐列表适配器数据集已改变,触发RecyclerView更新界面展示内容, + * 如果存在正在播放的歌曲(通过FileUtil.getSong获取),则滚动RecyclerView到对应歌曲的位置(有一定偏移量), + * 实现播放歌曲变化时列表相应更新展示的功能,保证界面展示与实际播放歌曲的同步性。 + * @param event 接收到的SongLocalEvent事件对象,可能包含了本地歌曲相关的变化信息(如新增、播放位置变化等情况), + * 根据事件携带的信息来决定如何更新界面展示。 + */ @Subscribe(threadMode = ThreadMode.MAIN) - public void onMessageEvent(SongLocalEvent event){ + public void onMessageEvent(SongLocalEvent event) { songAdapter.notifyDataSetChanged(); - if (FileUtil.getSong() != null) { + if (FileUtil.getSong()!= null) { layoutManager.scrollToPositionWithOffset(FileUtil.getSong().getPosition() + 4, mRecycler.getHeight()); } } - + /** + * 当Fragment所在的Activity创建完成后调用,主要用于调用设置各种点击事件的监听逻辑的方法, + * 完成界面的交互功能配置,使得用户在界面上的操作(如点击按钮、列表项等)能触发相应的功能实现。 + * @param savedInstanceState 保存的实例状态信息(如果有的话),可以用于恢复之前的界面状态等操作。 + */ @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); onClick(); } + /** + * 在Fragment中加载数据的抽象方法(这里没有具体实现,可能在父类中有默认逻辑或者根据具体需求在子类中按需实现), + * 一般用于发起获取数据等操作来填充界面展示内容(例如获取本地音乐数据等情况), + * 具体的加载逻辑可能涉及到与数据层交互、文件读取等操作,不同的业务场景可以有不同的实现方式。 + */ @Override protected void loadData() { } + /** + * 获取Fragment对应的布局资源ID,返回fragment_local布局资源的ID, + * 表明该Fragment加载对应的这个布局文件来展示界面内容,该布局文件定义了本地音乐界面的整体结构和各个视图组件的布局方式。 + * @return 布局资源ID,即R.layout.fragment_local,用于告诉系统加载对应的布局文件进行界面展示。 + */ @Override protected int getLayoutId() { return R.layout.fragment_local; } - + /** + * 展示本地音乐列表数据的方法,先清空本地音乐列表集合,再添加传入的音乐列表数据, + * 设置RecyclerView为可见,隐藏空视图布局(如果之前显示的话),设置布局管理器,创建并设置本地音乐列表适配器, + * 同时为列表项设置点击事件监听,点击时进行一系列歌曲相关信息的设置(如歌曲名称、歌手、播放地址等), + * 保存歌曲信息到本地,并调用播放服务的方法开始播放本地音乐,实现了音乐列表展示和点击播放的核心功能。 + * @param mp3InfoList 本地音乐列表数据集合,包含了本地歌曲的各种相关信息(如名称、歌手、地址等), + * 这些数据将用于在界面上展示每首音乐的详细信息,并在点击时进行播放操作。 + */ @Override public void showMusicList(final List mp3InfoList) { mLocalSongsList.clear(); @@ -126,11 +207,11 @@ public class LocalFragment extends BaseMvpFragment implements IL mRecycler.setVisibility(View.VISIBLE); mEmptyViewLinear.setVisibility(View.GONE); mRecycler.setLayoutManager(layoutManager); - //令recyclerView定位到当前播放的位置 + // 令recyclerView定位到当前播放的位置,方便用户在播放音乐时能快速定位到正在播放的歌曲所在位置。 songAdapter = new SongAdapter(mActivity, mLocalSongsList); mRecycler.setAdapter(songAdapter); songAdapter.setOnItemClickListener(position -> { - //将点击的序列化到本地 + // 将点击的序列化到本地,可能是保存当前点击歌曲的相关信息,方便后续记录用户操作等用途。 LocalSong mp3Info = mLocalSongsList.get(position); Song song = new Song(); song.setSongName(mp3Info.getName()); @@ -148,41 +229,76 @@ public class LocalFragment extends BaseMvpFragment implements IL } + /** + * 展示错误视图的方法,通常在本地音乐数据获取出现问题(比如没有本地音乐文件等情况)时调用, + * 显示一个Toast提示信息告知用户本地音乐为空,同时隐藏RecyclerView,显示空视图布局, + * 提示用户当前无本地音乐可展示,提供友好的用户提示,提升用户体验。 + */ @Override public void showErrorView() { showToast("本地音乐为空"); mRecycler.setVisibility(View.GONE); mEmptyViewLinear.setVisibility(View.VISIBLE); } + /** - * 注册服务 + * 注册服务的方法,创建启动播放服务的Intent对象,指定要启动的服务类为PlayerService, + * 然后绑定播放服务,通过传入的ServiceConnection对象监听服务连接状态, + * 并设置绑定模式为自动创建服务(如果服务未启动则自动启动),用于后续与播放服务的交互操作, + * 确保播放服务能正常运行并与Fragment建立通信连接。 */ private void registerAndBindReceive() { - //启动服务 + // 创建启动播放服务的Intent对象,指定要启动的服务类为PlayerService, + // Intent是Android中用于启动组件(如Activity、Service等)的消息传递对象。 Intent playIntent = new Intent(getActivity(), PlayerService.class); + // 绑定播放服务,通过传入的ServiceConnection对象监听服务连接状态, + // 并设置绑定模式为自动创建服务(如果服务未启动则自动启动), + // 这样可以保证在需要与播放服务交互时服务已处于运行状态。 mActivity.bindService(playIntent, connection, Context.BIND_AUTO_CREATE); } + /** + * 初始化本地音乐RecyclerView相关设置的方法,创建本地音乐列表集合,创建线性布局管理器, + * 清空本地音乐列表集合并从数据库(通过LitePal框架)获取所有本地音乐数据添加到集合中, + * 根据音乐列表数据数量判断RecyclerView和空视图布局的显示隐藏状态, + * 设置布局管理器,创建并设置本地音乐列表适配器,同时为列表项设置点击事件监听(与showMusicList方法中类似的逻辑), + * 点击时进行一系列歌曲相关信息的设置,保存歌曲信息到本地,并调用播放服务的方法开始播放本地音乐, + * 全面完成RecyclerView展示本地音乐列表的初始化配置工作。 + */ private void initLocalRecycler() { + // 创建用于存储本地音乐数据的空集合,后续将从数据库或本地文件扫描获取的音乐信息填充到该集合中。 mLocalSongsList = new ArrayList<>(); + // 创建线性布局管理器,设置为垂直方向排列(默认情况),用于管理RecyclerView中本地音乐列表的布局展示, + // 确定列表项是垂直排列的,方便用户上下滑动查看不同的音乐。 layoutManager = new LinearLayoutManager(getActivity()); mLocalSongsList.clear(); + // 使用LitePal框架查询获取数据库中所有的本地音乐数据(LocalSong类对应的表数据),添加到本地音乐列表集合中, + // LitePal是一个方便的Android数据库操作框架,简化了数据库查询等操作。 mLocalSongsList.addAll(LitePal.findAll(LocalSong.class)); + // 如果本地音乐列表集合中没有数据(即没有本地音乐文件),进行相应的界面显示设置。 if (mLocalSongsList.size() == 0) { + // 将RecyclerView设置为不可见,因为没有音乐可展示,避免显示一个空白的列表给用户造成困惑。 mRecycler.setVisibility(View.GONE); + // 将空视图布局设置为可见,用于提示用户当前无本地音乐,例如可以在该布局中显示一段友好的提示文字。 mEmptyViewLinear.setVisibility(View.VISIBLE); } else { + // 如果有本地音乐数据,则将空视图布局设置为不可见,准备展示音乐列表。 mEmptyViewLinear.setVisibility(View.GONE); + // 将RecyclerView设置为可见,准备展示本地音乐列表,让用户可以看到并操作音乐列表。 mRecycler.setVisibility(View.VISIBLE); + // 将线性布局管理器设置给RecyclerView,确定其布局方式,使得音乐列表能按照设定的方式展示。 mRecycler.setLayoutManager(layoutManager); - //令recyclerView定位到当前播放的位置 + // 令recyclerView定位到当前播放的位置,方便用户快速找到正在播放的歌曲。 songAdapter = new SongAdapter(mActivity, mLocalSongsList); mRecycler.setAdapter(songAdapter); - if (FileUtil.getSong() != null) { + if (FileUtil.getSong()!= null) { + // 滚动RecyclerView到指定位置,这里是根据正在播放歌曲的位置进行滚动, + // 并设置一定的偏移量(FileUtil.getSong().getPosition() - 4),目的是让正在播放的歌曲在RecyclerView中处于合适的展示位置, + // mRecycler.getHeight()可能用于根据RecyclerView的高度来进一步调整滚动的偏移量,以达到更好的视觉效果,使得正在播放的歌曲能更突出显示。 layoutManager.scrollToPositionWithOffset(FileUtil.getSong().getPosition() - 4, mRecycler.getHeight()); } songAdapter.setOnItemClickListener(position -> { - //将点击的序列化到本地 + // 将点击的序列化到本地,可能是保存当前点击歌曲的相关信息,方便后续记录用户操作等用途。 LocalSong mp3Info = mLocalSongsList.get(position); Song song = new Song(); song.setSongName(mp3Info.getName()); @@ -193,18 +309,31 @@ public class LocalFragment extends BaseMvpFragment implements IL song.setOnline(false); song.setSongId(mp3Info.getSongId()); song.setListType(Constant.LIST_TYPE_LOCAL); + // 通过FileUtil工具类将歌曲相关信息保存到本地,具体保存的方式和位置由FileUtil的实现决定, + // 可能是保存到本地文件或者数据库中,方便下次启动应用时能恢复之前的播放状态等操作。 FileUtil.saveSong(song); + // 通过获取到的与播放服务交互的Binder对象(mPlayStatusBinder)调用播放服务中的播放方法, + // 传入Constant.LIST_TYPE_LOCAL表示播放的是本地音乐类型,从而实现点击列表中的本地歌曲后进行播放的功能。 mPlayStatusBinder.play(Constant.LIST_TYPE_LOCAL); }); } } - - //按钮事件 + /** + * 设置各种点击事件的监听逻辑的方法,包括查找本地音乐的点击事件和返回上一界面的点击事件。 + * 点击查找本地音乐的ImageView时,调用Presenter的方法获取本地音乐信息(可能是重新扫描本地音乐文件等操作); + * 点击返回的ImageView时,通过Fragment管理器弹出当前Fragment所在的栈,返回上一界面, + * 为用户提供了基本的操作交互功能,方便在界面间进行导航和操作本地音乐相关功能。 + */ + // 按钮事件 private void onClick() { - mFindLocalMusicIv.setOnClickListener(v -> mPresenter.getLocalMp3Info()); //得到本地列表 - mBackIv.setOnClickListener(v -> Objects.requireNonNull(getActivity()).getSupportFragmentManager().popBackStack()); //返回 + // 为查找本地音乐的ImageView设置点击事件监听,点击时调用Presenter的方法获取本地音乐列表信息, + // 比如可能触发重新扫描本地存储设备查找新添加的音乐文件等操作,以更新本地音乐列表展示。 + mFindLocalMusicIv.setOnClickListener(v -> mPresenter.getLocalMp3Info()); + // 为返回的ImageView设置点击事件监听,点击时通过Fragment管理器弹出栈,返回上一界面, + // FragmentManager负责管理Fragment的添加、替换、移除以及返回栈等操作,popBackStack方法会将当前Fragment从栈顶弹出,显示上一个Fragment的界面。 + mBackIv.setOnClickListener(v -> Objects.requireNonNull(getActivity()).getSupportFragmentManager().popBackStack()); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/view/search/AlbumContentFragment.java b/app/src/main/java/com/example/musicplayer/view/search/AlbumContentFragment.java index 0cccacd..0a8f202 100644 --- a/app/src/main/java/com/example/musicplayer/view/search/AlbumContentFragment.java +++ b/app/src/main/java/com/example/musicplayer/view/search/AlbumContentFragment.java @@ -52,49 +52,94 @@ import static com.example.musicplayer.app.Constant.SINGER_NAME_KEY; /** * Created by 残渊 on 2018/11/25. + * 这个类表示专辑内容的Fragment,用于展示专辑相关的详细信息,如专辑封面、歌手、发行时间等, + * 并且包含了专辑内歌曲列表、专辑信息等不同页面的切换展示以及专辑收藏相关功能。 */ - public class AlbumContentFragment extends Fragment { private static final String TAG = "AlbumContentFragment"; - - - private String mAlbumName, mSingerName, mAlbumPic, mPublicTime, mId; - + // 专辑名称 + private String mAlbumName, + // 歌手名称 + mSingerName, + // 专辑封面图片的路径或资源标识等 + mAlbumPic, + // 专辑发行时间 + mPublicTime, + // 专辑的唯一标识(例如ID) + mId; + + // 用于实现页面滑动切换效果的ViewPager,来自MaterialViewPager库 private MaterialViewPager mViewPager; + // 顶部的Toolbar,用于显示标题、操作按钮等 private Toolbar toolbar; + // 专辑背景布局,用于设置专辑封面等作为背景展示效果 private RelativeLayout mAlbumBackground; + // 用于显示歌手名称的TextView private TextView mSingerNameTv; + // 用于显示发行时间的TextView private TextView mPublicTimeTv; + // 用于显示专辑封面图片的ImageView private ImageView mAlbumPicIv; + // 表示收藏按钮的菜单项 private MenuItem mLoveBtn; + // 用于记录专辑是否已被收藏的状态 private boolean mLove; + /** + * Fragment创建时调用,设置该Fragment有菜单选项(用于显示右上角的收藏按钮菜单) + * @param savedInstanceState 保存的实例状态信息(如果有的话) + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true);//加上这句话,menu才会显示出来 } + /** + * 创建Fragment的视图,加载对应的布局文件,并获取布局中的相关视图组件 + * @param inflater 用于将布局文件实例化为视图对象的布局填充器 + * @param container 父视图容器 + * @param savedInstanceState 保存的实例状态信息(如果有的话) + * @return 返回创建好的视图对象 + */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { getBundle(); + // 加载fragment_album_content布局文件并创建视图对象 View view = inflater.inflate(R.layout.fragment_album_content, container, false); + // 从布局中找到对应的MaterialViewPager组件 mViewPager = view.findViewById(R.id.materialViewPager); + // 获取MaterialViewPager中的Toolbar组件 toolbar = mViewPager.getToolbar(); + // 从MaterialViewPager的头部背景容器中找到对应的RelativeLayout组件,用于设置专辑背景 mAlbumBackground = mViewPager.getHeaderBackgroundContainer().findViewById(R.id.relative_album); + // 从MaterialViewPager的头部背景容器中找到对应的ImageView组件,用于显示专辑封面图片 mAlbumPicIv = mViewPager.getHeaderBackgroundContainer().findViewById(R.id.iv_album); + // 从MaterialViewPager的头部背景容器中找到对应的TextView组件,用于显示歌手名称 mSingerNameTv = mViewPager.getHeaderBackgroundContainer().findViewById(R.id.tv_singer_name); + // 从MaterialViewPager的头部背景容器中找到对应的TextView组件,用于显示发行时间 mPublicTimeTv = mViewPager.getHeaderBackgroundContainer().findViewById(R.id.tv_public_time); return view; } + /** + * 当Fragment所在的Activity创建完成后调用,用于进一步初始化视图组件和相关设置 + * @param savedInstanceState 保存的实例状态信息(如果有的话) + */ @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); initView(); } + + /** + * 用于创建菜单选项,这里主要是 inflate(加载)收藏按钮对应的菜单布局文件,并获取到收藏按钮菜单项, + * 同时根据收藏状态显示对应的图标 + * @param menu 菜单对象,用于添加菜单项等操作 + * @param inflater 菜单填充器,用于加载菜单布局文件 + */ @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.love, menu); @@ -103,43 +148,69 @@ public class AlbumContentFragment extends Fragment { super.onCreateOptionsMenu(menu, inflater); } + /** + * 初始化视图组件的相关设置,包括设置标题、加载专辑封面图片、设置文本内容、配置Toolbar显示样式以及 + * 为ViewPager设置页面切换的适配器等操作 + */ @SuppressLint("SetTextI18n") private void initView() { + // 设置Toolbar的标题为专辑名称 toolbar.setTitle(mAlbumName); + // 创建一个SimpleTarget对象,用于接收Glide加载图片后的结果,在图片加载完成后进行相关处理 SimpleTarget target = new SimpleTarget(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) { @Override public void onResourceReady(@Nullable Drawable resource, Transition transition) { + // 将加载后的Drawable资源转换为Bitmap,以便后续设置背景等操作 Bitmap bitmap = ((BitmapDrawable) resource).getBitmap(); + // 使用工具方法将Bitmap转换为可设置为背景的Drawable,并设置为专辑背景 mAlbumBackground.setBackground(CommonUtil.getForegroundDrawable(bitmap)); + // 将Bitmap设置为ImageView的显示图片,用于显示专辑封面 mAlbumPicIv.setImageBitmap(bitmap); } }; + // 使用Glide库加载专辑封面图片,设置占位图、加载出错时的图片,并将加载结果传递给SimpleTarget对象处理 Glide.with(getActivity()) - .load(mAlbumPic) - .apply(RequestOptions.placeholderOf(R.drawable.welcome)) - .apply(RequestOptions.errorOf(R.drawable.welcome)) - .into(target); + .load(mAlbumPic) + .apply(RequestOptions.placeholderOf(R.drawable.welcome)) + .apply(RequestOptions.errorOf(R.drawable.welcome)) + .into(target); + // 设置歌手名称的TextView显示内容,添加前缀 "歌手 " mSingerNameTv.setText("歌手 " + mSingerName); + // 设置发行时间的TextView显示内容,添加前缀 "发行时间 " mPublicTimeTv.setText("发行时间 " + mPublicTime); + // 设置Toolbar标题文字颜色为白色 toolbar.setTitleTextColor(getActivity().getResources().getColor(R.color.white)); - if (toolbar != null) { + if (toolbar!= null) { + // 将Toolbar设置为Activity的支持ActionBar(用于显示标题、导航按钮等功能) ((AppCompatActivity) getActivity()).setSupportActionBar(toolbar); final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); - if (actionBar != null) { + if (actionBar!= null) { + // 设置ActionBar显示返回按钮(用于导航返回上一个页面) actionBar.setDisplayHomeAsUpEnabled(true); + // 设置ActionBar显示返回按钮(确保图标显示等) actionBar.setDisplayShowHomeEnabled(true); + // 设置ActionBar显示标题(已设置过标题内容) actionBar.setDisplayShowTitleEnabled(true); + // 不使用logo作为显示元素(这里可能是按照需求禁用) actionBar.setDisplayUseLogoEnabled(false); + // 设置Home按钮可用(点击返回按钮等操作生效) actionBar.setHomeButtonEnabled(true); } } - //返回键的监听 + // 为Toolbar的返回按钮设置点击监听事件,点击时弹出Fragment栈,返回上一个Fragment页面 toolbar.setNavigationOnClickListener(v -> getActivity().getSupportFragmentManager().popBackStack()); + + // 为MaterialViewPager的ViewPager设置页面切换的适配器,用于展示不同的Fragment页面(如歌曲列表、专辑信息页面) mViewPager.getViewPager().setAdapter(new FragmentStatePagerAdapter(getActivity().getSupportFragmentManager()) { + /** + * 根据传入的位置参数,返回对应的Fragment实例,用于在ViewPager中显示不同页面 + * @param position 页面位置索引,0表示歌曲列表页面,1表示专辑信息页面 + * @return 返回对应的Fragment实例 + */ @Override public Fragment getItem(int position) { switch (position) { @@ -152,11 +223,20 @@ public class AlbumContentFragment extends Fragment { } } + /** + * 返回ViewPager中页面的总数,这里固定为2个页面(歌曲列表和专辑信息页面) + * @return 页面总数 + */ @Override public int getCount() { return 2; } + /** + * 根据页面位置索引,返回对应的页面标题,用于在ViewPager的标题栏显示 + * @param position 页面位置索引,0表示歌曲列表页面,1表示专辑信息页面 + * @return 对应的页面标题字符串 + */ @Override public CharSequence getPageTitle(int position) { switch (position) { @@ -169,43 +249,60 @@ public class AlbumContentFragment extends Fragment { } }); + // 将ViewPager与标题栏关联起来,使标题栏能根据ViewPager的页面切换显示对应的标题和指示器效果 mViewPager.getPagerTitleStrip().setViewPager(mViewPager.getViewPager()); + // 设置标题栏指示器的颜色(例如切换页面时的滑动条颜色) mViewPager.getPagerTitleStrip().setIndicatorColorResource(R.color.yellow); + // 设置标题栏每个标题标签的背景颜色 mViewPager.getPagerTitleStrip().setTabBackground(R.color.tab); + // 设置标题栏文字的颜色状态列表(例如不同状态下的文字颜色,按下、正常等状态) mViewPager.getPagerTitleStrip().setTextColorStateListResource(R.color.white); } + /** + * 处理菜单选项的点击事件,这里主要是处理收藏按钮的点击逻辑,根据专辑当前收藏状态进行相应操作, + * 如收藏或取消收藏专辑,并更新界面显示、数据库记录以及发送收藏状态改变的事件 + * @param item 被点击的菜单项 + * @return 返回是否处理了该点击事件,如果返回true,表示已处理,不再继续向上传递事件 + */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.btn_love: if (mLove) { + // 如果专辑已收藏,执行取消收藏操作,从数据库中删除对应的收藏记录 LitePal.deleteAllAsync(AlbumCollection.class, "albumId=?", mId).listen(new UpdateOrDeleteCallback() { @Override public void onFinish(int rowsAffected) { + // 更新收藏按钮图标为未收藏状态图标 mLoveBtn.setIcon(R.drawable.favorites); + // 显示提示Toast消息,告知用户已取消收藏 CommonUtil.showToast(getActivity(), "你已取消收藏该专辑"); } }); } else { + // 如果专辑未收藏,创建一个AlbumCollection对象,设置专辑相关信息,准备保存到数据库作为收藏记录 AlbumCollection albumCollection = new AlbumCollection(); albumCollection.setAlbumId(mId); albumCollection.setAlbumName(mAlbumName); albumCollection.setAlbumPic(mAlbumPic); albumCollection.setPublicTime(mPublicTime); albumCollection.setSingerName(mSingerName); + // 异步保存收藏记录到数据库 albumCollection.saveAsync().listen(new SaveCallback() { @Override public void onFinish(boolean success) { + // 更新收藏按钮图标为已收藏状态图标 mLoveBtn.setIcon(R.drawable.favorites_selected); + // 显示提示Toast消息,告知用户收藏专辑成功 CommonUtil.showToast(getActivity(), "收藏专辑成功"); } }); } - mLove = !mLove; - //发送收藏改变的事件 + mLove =!mLove; + // 发送专辑收藏状态改变的事件,通过EventBus通知其他可能关注该事件的地方进行相应处理 EventBus.getDefault().post(new AlbumCollectionEvent()); break; } @@ -213,8 +310,11 @@ public class AlbumContentFragment extends Fragment { } + /** + * 根据数据库中是否存在该专辑的收藏记录,来设置收藏按钮的图标显示,并更新收藏状态变量 + */ private void showLove() { - if (LitePal.where("albumId=?", mId).find(AlbumCollection.class).size() != 0) { + if (LitePal.where("albumId=?", mId).find(AlbumCollection.class).size()!= 0) { mLove = true; mLoveBtn.setIcon(R.drawable.favorites_selected); } else { @@ -224,6 +324,16 @@ public class AlbumContentFragment extends Fragment { } + /** + * 静态工厂方法,用于创建AlbumContentFragment实例,并传入专辑相关的必要参数, + * 将参数封装到Bundle中设置给Fragment实例 + * @param id 专辑的唯一标识(例如ID) + * @param albumName 专辑名称 + * @param albumPic 专辑封面图片的路径或资源标识等 + * @param singerName 歌手名称 + * @param publicTime 专辑发行时间 + * @return 返回创建好的AlbumContentFragment实例 + */ public static Fragment newInstance(String id, String albumName, String albumPic, String singerName, String publicTime) { AlbumContentFragment albumContentFragment = new AlbumContentFragment(); @@ -237,9 +347,12 @@ public class AlbumContentFragment extends Fragment { return albumContentFragment; } + /** + * 从传入的Bundle中获取之前设置的专辑相关参数(如果有的话),赋值给对应的成员变量 + */ private void getBundle() { Bundle bundle = getArguments(); - if (bundle != null) { + if (bundle!= null) { mId = bundle.getString(ALBUM_ID_KEY); mAlbumName = bundle.getString(ALBUM_NAME_KEY); mAlbumPic = bundle.getString(ALBUM_PIC_KEY); @@ -249,67 +362,132 @@ public class AlbumContentFragment extends Fragment { } /** + * 内部类,表示搜索相关的Fragment,用于处理搜索输入、显示搜索历史以及根据搜索内容切换展示不同的搜索结果页面等功能 + * 这个Fragment嵌套在其他布局中使用,用于实现搜索模块的交互逻辑。 * Created by 残渊 on 2018/11/20. */ + public static class SearchFragment extends Fragment { + private static final String TAG = "SearchFragment"; + // 用于输入搜索内容的编辑文本框 + private EditText mSeekEdit; + // 用于触发搜索操作的按钮(这里是一个具有点击涟漪效果的视图) + private RippleView mSeekTv; + // 用于返回上一个页面的按钮(这里是一个具有点击涟漪效果的视图) + private RippleView mBackIv; + /** + * 创建SearchFragment的视图,加载对应的布局文件,并获取布局中的相关视图组件, + * 同时默认先显示搜索 + /** + * 内部类,表示搜索相关的Fragment,用于处理搜索输入、显示搜索历史以及根据搜索内容切换展示不同的搜索结果页面等功能 + * 这个Fragment嵌套在其他布局中使用,用于实现搜索模块的交互逻辑。 + * Created by 残渊 on 2018/11/20. + */ public static class SearchFragment extends Fragment { private static final String TAG = "SearchFragment"; + // 用于输入搜索内容的编辑文本框 private EditText mSeekEdit; + // 用于触发搜索操作的按钮(这里是一个具有点击涟漪效果的视图) private RippleView mSeekTv; + // 用于返回上一个页面的按钮(这里是一个具有点击涟漪效果的视图) private RippleView mBackIv; + /** + * 创建SearchFragment的视图,加载对应的布局文件,并获取布局中的相关视图组件, + * 同时默认先显示搜索历史的Fragment + * @param inflater 用于将布局文件实例化为视图对象的布局填充器 + * @param container 父视图容器 + * @param savedInstanceState 保存的实例状态信息(如果有的话) + * @return 返回创建好的视图对象 + */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // 加载fragment_search布局文件并创建视图对象 View view = inflater.inflate(R.layout.fragment_search, container, false); + // 从布局中找到对应的EditText组件,用于输入搜索内容 mSeekEdit = view.findViewById(R.id.edit_seek); + // 从布局中找到对应的RippleView组件,作为搜索按钮 mSeekTv = view.findViewById(R.id.tv_search); + // 从布局中找到对应的RippleView组件,作为返回按钮 mBackIv = view.findViewById(R.id.iv_back); + // 初始时显示搜索历史的Fragment页面 replaceFragment(new SearchHistoryFragment()); return view; } + /** + * 当Fragment所在的Activity创建完成后调用,进行一些视图相关的交互设置, + * 例如弹出键盘、设置搜索按钮点击事件、编辑文本框的触摸事件以及返回按钮的点击事件等 + * @param saveInstanceState 保存的实例状态信息(如果有的话) + */ @SuppressLint("ClickableViewAccessibility") @Override public void onActivityCreated(Bundle saveInstanceState) { super.onActivityCreated(saveInstanceState); + // 弹出软键盘,聚焦到搜索编辑文本框上,方便用户直接输入搜索内容 CommonUtil.showKeyboard(mSeekEdit, getActivity()); + // 为搜索按钮设置点击监听事件 mSeekTv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { + // 关闭软键盘,避免遮挡界面等情况 CommonUtil.closeKeybord(mSeekEdit, getActivity()); - mSeekEdit.setCursorVisible(false);//隐藏光标 - if(mSeekEdit.getText().toString().trim().length()==0){ + // 隐藏编辑文本框的光标,可能是搜索后改变显示样式需求 + mSeekEdit.setCursorVisible(false); + // 如果用户输入的内容为空(仅包含空格等也视为空),则将编辑文本框的内容设置为提示文本内容 + if (mSeekEdit.getText().toString().trim().length() == 0) { mSeekEdit.setText(mSeekEdit.getHint().toString().trim()); } + // 将此次搜索内容保存到数据库中,作为搜索历史记录 saveDatabase(mSeekEdit.getText().toString()); + // 根据输入的搜索内容,切换显示对应的搜索结果Fragment页面 replaceFragment(ContentFragment.newInstance(mSeekEdit.getText().toString())); } }); + + // 为编辑文本框设置触摸事件监听,当用户触摸文本框时,显示光标,方便用户输入编辑 mSeekEdit.setOnTouchListener((v, event) -> { if (MotionEvent.ACTION_DOWN == event.getAction()) { mSeekEdit.setCursorVisible(true); } return false; }); + + // 为返回按钮设置点击监听事件,点击时关闭软键盘,并弹出Fragment栈,返回上一个Fragment页面 mBackIv.setOnClickListener(v -> { - CommonUtil.closeKeybord(mSeekEdit,getActivity()); + CommonUtil.closeKeybord(mSeekEdit, getActivity()); getActivity().getSupportFragmentManager().popBackStack(); }); } + /** + * 将输入的搜索内容保存到数据库中作为搜索历史记录,先检查是否已存在相同的搜索历史, + * 如果存在则删除旧记录(可能是为了更新历史记录顺序等需求),然后再插入新的搜索历史记录 + * @param seekHistory 要保存的搜索历史内容字符串 + */ private void saveDatabase(String seekHistory) { + // 从数据库中查询是否已存在相同的搜索历史记录 List searchHistoryList = LitePal.where("history=?", seekHistory).find(SearchHistory.class); + // 如果查询到有一条相同的搜索历史记录 if (searchHistoryList.size() == 1) { + // 删除这条已存在的旧搜索历史记录 LitePal.delete(SearchHistory.class, searchHistoryList.get(0).getId()); } + // 创建一个新的SearchHistory对象,用于保存此次搜索历史记录 SearchHistory searchHistory = new SearchHistory(); searchHistory.setHistory(seekHistory); + // 将新的搜索历史记录保存到数据库中 searchHistory.save(); } + /** + * 用于设置编辑文本框的内容,并进行一系列相关操作,如隐藏光标、设置光标位置、关闭软键盘以及切换显示对应的搜索结果Fragment页面等 + * 这个方法可以在外部调用,用于直接设置搜索内容并触发后续相关逻辑。 + * @param seek 要设置的搜索内容字符串 + */ public void setSeekEdit(String seek) { mSeekEdit.setText(seek); mSeekEdit.setCursorVisible(false);//隐藏光标 @@ -319,6 +497,12 @@ public class AlbumContentFragment extends Fragment { replaceFragment(ContentFragment.newInstance(mSeekEdit.getText().toString())); } + /** + * 用于切换显示不同的Fragment页面,通过FragmentTransaction进行Fragment的替换操作, + * 将传入的Fragment替换到指定的容器布局(R.id.container)中,实现页面的切换展示效果。 + * 这个方法在搜索按钮点击、设置搜索内容等情况下调用,用于更新显示搜索结果等不同的Fragment页面。 + * @param fragment 要切换显示的Fragment实例 + */ //搜索后的页面 private void replaceFragment(Fragment fragment) { FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); @@ -327,3 +511,4 @@ public class AlbumContentFragment extends Fragment { } } } + \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/view/search/AlbumSongFragment.java b/app/src/main/java/com/example/musicplayer/view/search/AlbumSongFragment.java index 758138e..0178b32 100644 --- a/app/src/main/java/com/example/musicplayer/view/search/AlbumSongFragment.java +++ b/app/src/main/java/com/example/musicplayer/view/search/AlbumSongFragment.java @@ -47,71 +47,130 @@ import static com.example.musicplayer.app.Constant.PUBLIC_TIME_KEY; /** * Created by 残渊 on 2018/11/25. + * 这个类表示专辑歌曲相关的Fragment,根据不同类型(歌曲列表或专辑信息)展示不同的界面内容, + * 包含了加载专辑歌曲数据、显示专辑信息、处理与播放服务的交互以及响应相关事件等功能。 + * 它继承自BaseMvpFragment,遵循IAlbumSongContract.View接口定义的视图相关操作规范。 */ - -public class AlbumSongFragment extends BaseMvpFragment implements IAlbumSongContract.View{ +public class AlbumSongFragment extends BaseMvpFragment implements IAlbumSongContract.View { + // 用于区分Fragment展示类型的键值,用于在传递参数和获取参数时作为标识 private static final String TYPE_KEY = "type_key"; + // 表示展示专辑歌曲列表的类型常量,用于判断当前Fragment应展示歌曲列表相关界面 public static final int ALBUM_SONG = 0; + // 表示展示专辑信息的类型常量,用于判断当前Fragment应展示专辑详细信息相关界面 public static final int ALBUM_INFORMATION = 1; + // 专辑歌曲的Presenter对象,用于处理业务逻辑,如获取专辑相关数据等,遵循MVP模式 private AlbumSongPresenter mPresenter; + // 专辑的唯一标识(例如ID),用于后续获取对应专辑的详细信息等操作 private String mId; + // 用于展示专辑信息页面中可滚动的视图容器,方便显示较多内容时滚动查看 private NestedScrollView mScrollView; - private TextView mNameTv, mLanguageTv,mDescTv,mCompany,mPublicTimeTv,mTypeTv; + // 用于显示专辑名称的TextView + private TextView mNameTv, + // 用于显示语言相关信息的TextView + mLanguageTv, + // 用于显示专辑描述信息的TextView + mDescTv, + // 用于显示发行公司信息的TextView + mCompany, + // 用于显示发行时间的TextView(此处与成员变量mPublicTime可能有重复赋值情况,具体看使用场景) + mPublicTimeTv, + // 用于显示专辑类型信息的TextView + mTypeTv; + // 当前Fragment的展示类型,通过参数传入判断是展示歌曲列表还是专辑信息 private int mType; + // 专辑的发行时间,从传入参数获取,用于展示等操作 private String mPublicTime; + // 专辑的描述信息,后续从获取的数据中赋值,用于展示 private String mDesc; - //用来判断网络问题及加载问题 + // 用于显示加载动画的视图组件,提示用户正在加载数据,例如网络请求加载专辑相关信息时展示 private AVLoadingIndicatorView mLoading; + // 用于显示加载相关提示文本的TextView,例如在加载出现问题等情况下展示提示信息 private TextView mLoadingTv; + // 用于显示网络错误相关提示图标的ImageView,当出现网络问题时展示给用户提示 private ImageView mNetworkErrorIv; - - + // 存储专辑歌曲列表数据的集合,从获取的数据中赋值,用于展示歌曲列表等操作 private List mSongsList; + // 用于展示专辑歌曲列表的RecyclerView组件,通过设置适配器等展示歌曲信息列表 private RecyclerView mRecycle; + // 用于管理RecyclerView布局的线性布局管理器,设置列表的布局方式(如垂直排列等) private LinearLayoutManager mLinearManager; + // 专辑歌曲列表的适配器,负责将歌曲数据绑定到RecyclerView的每个Item视图上进行展示,并处理相关点击等交互逻辑 private AlbumSongAdapter mAdapter; + // 用于启动播放服务的Intent对象,通过它可以启动与播放相关的后台服务 private Intent playIntent; + // 用于与播放服务进行交互的Binder对象,通过它可以调用播放服务中的方法,如控制播放等操作 private PlayerService.PlayStatusBinder mPlayStatusBinder; + // 定义与播放服务的连接对象,用于监听服务的连接与断开状态,并获取服务提供的Binder对象进行交互 private ServiceConnection connection = new ServiceConnection() { + /** + * 当与服务成功连接时调用,获取服务提供的IBinder对象,并强转为对应的PlayStatusBinder类型, + * 以便后续调用播放服务中的方法进行播放控制等操作 + * @param name 连接的服务的组件名称 + * @param service 服务返回的IBinder对象 + */ @Override public void onServiceConnected(ComponentName name, IBinder service) { mPlayStatusBinder = (PlayerService.PlayStatusBinder) service; } + /** + * 当与服务意外断开连接时调用,这里暂时没有具体实现逻辑,可根据需求添加相关处理, + * 比如重新尝试连接、提示用户等操作 + * @param name 断开连接的服务的组件名称 + */ @Override public void onServiceDisconnected(ComponentName name) { } }; - + /** + * 在Fragment中加载数据的方法,创建专辑歌曲的Presenter对象,并将当前视图(this)与之关联, + * 然后调用Presenter的方法去获取指定专辑的详细信息(根据传入的专辑ID和展示类型) + */ @Override protected void loadData() { - mPresenter =new AlbumSongPresenter(); + mPresenter = new AlbumSongPresenter(); mPresenter.attachView(this); - mPresenter.getAlbumDetail(mId,mType); + mPresenter.getAlbumDetail(mId, mType); } + /** + * 获取Fragment对应的布局资源ID,这里返回0,可能是在子类中根据不同条件动态决定加载的布局, + * 具体布局加载在onCreateView方法中处理 + * @return 布局资源ID,这里返回0 + */ @Override protected int getLayoutId() { return 0; } + /** + * 创建Fragment的视图,根据传入的展示类型(歌曲列表或专辑信息)加载不同的布局文件, + * 同时获取布局中的相关视图组件,并注册到EventBus中用于接收事件 + * @param inflater 用于将布局文件实例化为视图对象的布局填充器 + * @param container 父视图容器 + * @param savedInstanceState 保存的实例状态信息(如果有的话) + * @return 返回创建好的视图对象 + */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { getBundle(); + // 注册当前Fragment到EventBus,用于接收事件(例如歌曲相关事件,可能用于更新界面等操作) EventBus.getDefault().register(this); View view = null; + // 根据类型判断加载不同的布局文件 if (mType == ALBUM_SONG) { view = inflater.inflate(R.layout.fragment_album_recycler, container, false); mRecycle = view.findViewById(R.id.normalView); + // 获取数据库实例(LitePal框架相关操作,可能用于后续数据查询等) LitePal.getDatabase(); } else { view = inflater.inflate(R.layout.fragment_album_song, container, false); @@ -130,90 +189,156 @@ public class AlbumSongFragment extends BaseMvpFragment imple return view; } + /** + * 订阅EventBus的事件,当接收到SongAlbumEvent类型的事件时,调用适配器的方法通知数据集已改变, + * 可能用于更新歌曲列表等界面展示内容(例如歌曲信息有变化时更新显示) + * @param event 接收到的SongAlbumEvent事件对象 + */ @Subscribe(threadMode = ThreadMode.MAIN) - public void onSongAlbumEvent(SongAlbumEvent event){ + public void onSongAlbumEvent(SongAlbumEvent event) { mAdapter.notifyDataSetChanged(); } + /** + * 当Fragment所在的Activity创建完成后调用,根据展示类型进行不同的初始化操作, + * 如启动播放服务并绑定(歌曲列表类型时),或者注册滚动视图到MaterialViewPager帮助类(专辑信息类型时) + * @param save 保存的实例状态信息(如果有的话) + */ @Override public void onActivityCreated(Bundle save) { super.onActivityCreated(save); - if(mType==ALBUM_SONG){ - //启动服务 + if (mType == ALBUM_SONG) { + // 创建启动播放服务的Intent对象,指定要启动的服务类为PlayerService playIntent = new Intent(getActivity(), PlayerService.class); + // 绑定播放服务,通过传入的ServiceConnection对象监听服务连接状态, + // 并设置绑定模式为自动创建服务(如果服务未启动则自动启动) mActivity.bindService(playIntent, connection, Context.BIND_AUTO_CREATE); - }else{ + } else { + // 将可滚动视图(NestedScrollView)注册到MaterialViewPager帮助类, + // 可能用于实现一些与滚动效果相关的交互功能(例如标题栏随滚动变化等效果) MaterialViewPagerHelper.registerScrollView(getActivity(), mScrollView); } } + /** + * 当Fragment的视图将要销毁时调用,进行一些清理操作,如注销EventBus注册、解除与播放服务的绑定等 + */ @Override - public void onDestroyView(){ + public void onDestroyView() { + // 注销当前Fragment在EventBus的注册,避免内存泄漏等问题以及不再接收事件 EventBus.getDefault().unregister(this); - if(playIntent!=null){ + if (playIntent!= null) { + // 解除与播放服务的绑定,释放相关资源,避免内存泄漏等问题 Objects.requireNonNull(getActivity()).unbindService(connection); } super.onDestroyView(); } - private void getBundle(){ + /** + * 从传入的Bundle中获取之前设置的Fragment相关参数(如展示类型、专辑ID、发行时间等), + * 赋值给对应的成员变量,用于后续操作中判断展示内容和获取对应数据等操作 + */ + private void getBundle() { Bundle bundle = getArguments(); - if (bundle != null) { + if (bundle!= null) { mType = bundle.getInt(TYPE_KEY); - mId =bundle.getString(ALBUM_ID_KEY); + mId = bundle.getString(ALBUM_ID_KEY); mPublicTime = bundle.getString(PUBLIC_TIME_KEY); } } - public static Fragment newInstance(int type, String id,String publicTime) { + /** + * 静态工厂方法,用于创建AlbumSongFragment实例,并传入展示类型、专辑ID、发行时间等必要参数, + * 将参数封装到Bundle中设置给Fragment实例,方便根据不同情况创建并配置Fragment + * @param type 展示类型,取值为ALBUM_SONG或ALBUM_INFORMATION常量之一 + * @param id 专辑的唯一标识(例如ID) + * @param publicTime 专辑的发行时间 + * @return 返回创建好的AlbumSongFragment实例 + */ + public static Fragment newInstance(int type, String id, String publicTime) { AlbumSongFragment fragment = new AlbumSongFragment(); Bundle bundle = new Bundle(); bundle.putInt(TYPE_KEY, type); - bundle.putString(ALBUM_ID_KEY,id); + bundle.putString(ALBUM_ID_KEY, id); bundle.putString(PUBLIC_TIME_KEY, publicTime); fragment.setArguments(bundle); return fragment; } + /** + * 设置专辑歌曲列表数据,并进行相关的RecyclerView和适配器的配置操作, + * 同时为歌曲列表的每个Item设置点击事件监听,点击时进行一系列歌曲相关信息的设置和播放操作 + * @param songList 专辑歌曲列表数据集合 + */ @Override public void setAlbumSongList(final List songList) { - mLinearManager =new LinearLayoutManager(getActivity()); + // 创建线性布局管理器,设置为垂直方向排列(默认情况),用于管理RecyclerView中歌曲列表的布局展示 + mLinearManager = new LinearLayoutManager(getActivity()); + // 将线性布局管理器设置给RecyclerView,确定其布局方式 mRecycle.setLayoutManager(mLinearManager); - mAdapter =new AlbumSongAdapter(songList); + // 创建专辑歌曲列表的适配器,传入歌曲列表数据,用于将数据绑定到视图上展示 + mAdapter = new AlbumSongAdapter(songList); + // 为RecyclerView添加头部装饰器(可能用于实现一些特殊的头部效果,具体看MaterialViewPagerHeaderDecorator的实现) mRecycle.addItemDecoration(new MaterialViewPagerHeaderDecorator()); + // 将适配器设置给RecyclerView,使其能够展示歌曲列表数据 mRecycle.setAdapter(mAdapter); + // 为歌曲列表的适配器设置点击事件监听,点击某个歌曲Item时执行以下逻辑 mAdapter.setSongClick(position -> { - AlbumSong.DataBean.ListBean dataBean= songList.get(position); + // 获取点击位置对应的歌曲数据对象 + AlbumSong.DataBean.ListBean dataBean = songList.get(position); Song song = new Song(); + // 设置歌曲的唯一标识(例如歌曲ID) song.setSongId(dataBean.getSongmid()); + // 设置歌曲的歌手信息,通过调用getSinger方法拼接多个歌手名字(如果有多个歌手) song.setSinger(getSinger(dataBean)); + // 设置歌曲名称 song.setSongName(dataBean.getSongname()); + // 设置歌曲在列表中的位置索引 song.setPosition(position); + // 设置歌曲时长 song.setDuration(dataBean.getInterval()); + // 设置歌曲为在线状态(可根据实际情况判断是否准确,这里可能只是简单标记) song.setOnline(true); + // 设置歌曲列表类型为在线类型(同样可根据实际业务情况有不同取值和含义) song.setListType(Constant.LIST_TYPE_ONLINE); - song.setImgUrl(Api.ALBUM_PIC+dataBean.getAlbummid()+ Api.JPG); + // 设置歌曲封面图片的URL地址,通过拼接相关路径和参数生成 + song.setImgUrl(Api.ALBUM_PIC + dataBean.getAlbummid() + Api.JPG); + // 此处设置歌曲播放地址为null,可能后续会有根据在线情况等获取真实地址的逻辑 song.setUrl(null); + // 设置歌曲的媒体ID(具体用途看业务逻辑中如何使用这个字段) song.setMediaId(dataBean.getStrMediaMid()); - //判断是否已经下载 - song.setDownload(LitePal.where("songId=?", dataBean.getSongmid()).find(DownloadSong.class).size() != 0); + // 判断歌曲是否已经下载,通过查询LitePal数据库中是否存在对应歌曲ID的下载记录来确定 + song.setDownload(LitePal.where("songId=?", dataBean.getSongmid()).find(DownloadSong.class).size()!= 0); + // 保存歌曲相关信息(可能是保存到本地数据库或者其他存储方式,具体看FileUtil.saveSong的实现) FileUtil.saveSong(song); + // 通过与播放服务交互的Binder对象,调用播放服务中的播放方法,传入播放类型参数开始播放歌曲 mPlayStatusBinder.play(Constant.LIST_TYPE_ONLINE); }); } + /** + * 当获取专辑歌曲信息出现错误时调用,通过工具类显示一个Toast提示信息给用户,告知获取专辑信息失败 + */ @Override public void showAlbumSongError() { - CommonUtil.showToast(getActivity(),"获取专辑信息失败"); + CommonUtil.showToast(getActivity(), "获取专辑信息失败"); } + /** + * 展示专辑详细信息,将传入的专辑相关信息(名称、语言、公司、类型、描述等)设置到对应的TextView组件上进行显示 + * @param name 专辑名称 + * @param language 专辑语言信息 + * @param company 专辑发行公司信息 + * @param type 专辑类型信息 + * @param desc 专辑描述信息 + */ @Override - public void showAlbumMessage(String name, String language, String company, String type,String desc) { + public void showAlbumMessage(String name, String language, String company, String type, String desc) { mNameTv.setText(name); mLanguageTv.setText(language); mCompany.setText(company); @@ -222,23 +347,35 @@ public class AlbumSongFragment extends BaseMvpFragment imple mTypeTv.setText(type); } +* 显示加载动画,用于提示用户正在进行数据加载操作(例如网络请求加载专辑相关数据), + * 通常在开始加载数据前调用此方法展示加载提示给用户。 + */ @Override public void showLoading() { mLoading.show(); } + /** + * 隐藏加载动画以及相关的加载提示文本、错误提示图标等,同时根据当前Fragment展示类型(歌曲列表或专辑信息), + * 显示对应的视图内容(如歌曲列表RecyclerView或者包含专辑信息的ScrollView), + * 一般在数据加载完成后调用此方法更新界面显示状态。 + */ @Override public void hideLoading() { mLoading.hide(); mLoadingTv.setVisibility(View.GONE); - if(mType==ALBUM_SONG){ + if (mType == ALBUM_SONG) { mRecycle.setVisibility(View.VISIBLE); - }else { + } else { mScrollView.setVisibility(View.VISIBLE); } mNetworkErrorIv.setVisibility(View.GONE); } + /** + * 显示网络错误相关提示,隐藏加载动画和加载提示文本,显示网络错误提示图标, + * 用于在出现网络问题导致数据加载失败等情况下,提示用户当前存在网络异常情况。 + */ @Override public void showNetError() { mLoadingTv.setVisibility(View.GONE); @@ -246,17 +383,29 @@ public class AlbumSongFragment extends BaseMvpFragment imple mNetworkErrorIv.setVisibility(View.VISIBLE); } + /** + * 获取与当前Fragment关联的专辑歌曲Presenter对象,这里返回null, + * 可能需要在具体的子类或者使用场景中重写该方法来正确返回对应的Presenter实例, + * 以符合MVP模式中视图与Presenter的交互要求。 + * @return 返回专辑歌曲Presenter对象,此处返回null + */ @Override protected AlbumSongPresenter getPresenter() { return null; } + /** + * 获取歌曲的歌手信息,因为一首歌曲可能存在多个歌手,所以将多个歌手名字拼接成一个字符串返回, + * 拼接格式为歌手1、歌手2……的形式,方便在界面上展示歌曲的完整歌手信息。 + * @param dataBean 包含歌曲相关信息的数据对象,从中获取歌手信息列表进行拼接处理 + * @return 返回拼接好的歌手信息字符串 + */ //获取歌手,因为歌手可能有很多个 - private String getSinger(AlbumSong.DataBean.ListBean dataBean){ + private String getSinger(AlbumSong.DataBean.ListBean dataBean) { StringBuilder singer = new StringBuilder(dataBean.getSinger().get(0).getName()); for (int i = 1; i < dataBean.getSinger().size(); i++) { singer.append("、").append(dataBean.getSinger().get(i).getName()); } return singer.toString(); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/view/search/ContentFragment.java b/app/src/main/java/com/example/musicplayer/view/search/ContentFragment.java index 89bb73f..06ee212 100644 --- a/app/src/main/java/com/example/musicplayer/view/search/ContentFragment.java +++ b/app/src/main/java/com/example/musicplayer/view/search/ContentFragment.java @@ -16,51 +16,96 @@ import java.util.List; /** * Created by 残渊 on 2018/11/25. + * 这个类表示内容展示的Fragment,主要用于创建包含多个页面切换功能的界面布局, + * 通过TabLayout和ViewPager的结合实现类似选项卡切换不同内容的效果, + * 每个选项卡对应的页面是不同的Fragment(如歌曲相关页面、专辑相关页面)。 */ - public class ContentFragment extends Fragment { + // 用于存储选项卡标题的列表,每个元素对应一个Tab的标题文本 private List mTitleList; + // 用于存储各个选项卡对应的Fragment实例的列表,每个元素对应一个Tab要展示的Fragment private List mFragments; + // 用于实现页面切换功能的ViewPager组件,可通过滑动或者点击Tab来切换不同的页面(Fragment) private ViewPager mPager; + // 自定义的TabAdapter适配器,用于将Fragment和标题等数据适配到ViewPager上,实现页面切换和标题显示逻辑 private TabAdapter mAdapter; + // 用于展示选项卡的TabLayout组件,提供可视化的选项卡切换交互界面 private TabLayout mTabLayout; + // 定义选项卡标题的字符串数组,这里包含了两个标题,分别是"歌曲"和"专辑",对应不同的展示内容 private String[] mTitles = {"歌曲", "专辑"}; + // 定义与不同选项卡对应的类型标识字符串数组,用于区分不同类型的搜索内容或者页面展示逻辑, + // 这里分别对应 "song"(歌曲相关)和 "album"(专辑相关) private String[] mTypes = {"song", "album"}; + // 用于接收从外部传入的参数Bundle,可能包含了搜索内容等相关数据,用于在Fragment内部传递和使用 private Bundle mBundle; + // 用于存储搜索内容的字符串,从传入的Bundle中获取,可能用于后续根据搜索内容展示相应的结果等操作 private String mSeek; + /** + * 创建Fragment的视图,加载对应的布局文件,获取布局中的相关视图组件(如ViewPager、TabLayout), + * 同时从传入的参数Bundle中获取搜索内容,并初始化用于存储标题和Fragment的列表。 + * @param inflater 用于将布局文件实例化为视图对象的布局填充器 + * @param container 父视图容器 + * @param savedInstanceState 保存的实例状态信息(如果有的话) + * @return 返回创建好的视图对象 + */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // 加载fragment_content布局文件并创建视图对象 View view = inflater.inflate(R.layout.fragment_content, container, false); + // 获取从外部传入的参数Bundle对象 mBundle = getArguments(); - if (mBundle != null) { + if (mBundle!= null) { + // 从Bundle中获取搜索内容字符串,其键值对应SearchContentFragment.SEEK_KEY(具体定义在其他地方) mSeek = mBundle.getString(SearchContentFragment.SEEK_KEY); } + // 从加载的布局中找到对应的ViewPager组件 mPager = view.findViewById(R.id.page); + // 从加载的布局中找到对应的TabLayout组件 mTabLayout = view.findViewById(R.id.tab_layout); + // 创建用于存储选项卡标题的空列表 mTitleList = new ArrayList<>(); + // 创建用于存储选项卡对应Fragment实例的空列表 mFragments = new ArrayList<>(); + // 调用方法初始化选项卡相关设置,包括添加标题、创建对应的Fragment实例以及设置适配器等操作 initTab(); return view; } + /** + * 初始化选项卡相关设置的方法,循环遍历定义好的标题数组,将标题添加到标题列表中, + * 同时根据类型创建对应的SearchContentFragment实例并添加到Fragment列表中, + * 最后创建TabAdapter适配器并设置给ViewPager,将TabLayout与ViewPager关联起来实现联动效果。 + */ private void initTab() { for (int i = 0; i < mTitles.length; i++) { + // 将标题数组中的每个标题添加到标题列表中 mTitleList.add(mTitles[i]); + // 根据类型创建对应的SearchContentFragment实例,并传入搜索内容和类型标识,添加到Fragment列表中 mFragments.add(SearchContentFragment.newInstance(mSeek, mTypes[i])); } + // 创建TabAdapter适配器,传入子Fragment管理器、Fragment列表和标题列表,用于适配ViewPager的页面切换逻辑 mAdapter = new TabAdapter(getChildFragmentManager(), mFragments, mTitleList); + // 将创建好的适配器设置给ViewPager,使其能够根据适配器中的配置展示不同的Fragment页面 mPager.setAdapter(mAdapter); + // 将TabLayout与ViewPager关联起来,使得TabLayout能够根据ViewPager的页面切换自动更新选中的选项卡, + // 同时点击TabLayout的选项卡也能切换ViewPager的页面 mTabLayout.setupWithViewPager(mPager); } + /** + * 静态工厂方法,用于创建ContentFragment实例,并传入搜索内容参数, + * 将参数封装到Bundle中设置给Fragment实例,方便在创建Fragment时传递必要的数据。 + * @param seek 搜索内容的字符串,用于在Fragment内部根据搜索内容展示相应的结果等操作 + * @return 返回创建好的ContentFragment实例 + */ public static Fragment newInstance(String seek) { ContentFragment fragment = new ContentFragment(); Bundle bundle = new Bundle(); @@ -68,4 +113,4 @@ public class ContentFragment extends Fragment { fragment.setArguments(bundle); return fragment; } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/view/search/SearchContentFragment.java b/app/src/main/java/com/example/musicplayer/view/search/SearchContentFragment.java index 7970419..b4969ec 100644 --- a/app/src/main/java/com/example/musicplayer/view/search/SearchContentFragment.java +++ b/app/src/main/java/com/example/musicplayer/view/search/SearchContentFragment.java @@ -45,48 +45,81 @@ import butterknife.BindView; /** * Created by 残渊 on 2018/11/21. + * 这个类表示搜索内容展示的Fragment,用于展示搜索歌曲或专辑的结果, + * 具备加载数据、处理下拉刷新和上拉加载更多、与播放服务交互以及响应相关事件等功能, + * 遵循MVP模式,实现了ISearchContentContract.View接口定义的视图相关操作规范, + * 同时继承自BaseLoadingFragment,可能具备一些基础的加载相关的功能和属性。 */ - public class SearchContentFragment extends BaseLoadingFragment implements ISearchContentContract.View { private static final String TAG = "SearchContentFragment"; + // 用于区分搜索类型的键值,在传递和获取参数时作为标识,表明是歌曲还是专辑等类型的搜索 public static final String TYPE_KEY = "type"; + // 用于传递搜索内容的键值,在传递和获取参数时作为标识,存储用户输入的搜索关键字等信息 public static final String SEEK_KEY = "seek"; + // 用于标识是否在线的键值(从代码中看可能未完整体现其使用场景,也许用于区分在线资源相关逻辑) public static final String IS_ONLINE = "online"; + // 用于记录当前搜索结果的页码偏移量,初始化为1,用于实现翻页搜索功能,每次加载更多数据时递增 private int mOffset = 1; //用于翻页搜索 - - + // 搜索内容的Presenter对象,用于处理业务逻辑,如发起搜索请求、获取更多数据等,遵循MVP模式 private SearchContentPresenter mPresenter; + // 用于管理RecyclerView布局的线性布局管理器,设置列表的布局方式(如垂直排列等) private LinearLayoutManager manager; + // 搜索内容的适配器,负责将搜索结果数据(歌曲或专辑列表数据)绑定到RecyclerView的每个Item视图上进行展示,并处理相关点击等交互逻辑 private SearchContentAdapter mAdapter; + // 存储搜索到的歌曲列表数据的集合,初始化为空的ArrayList,后续从获取的数据中添加元素并展示 private ArrayList mSongList = new ArrayList<>(); + // 存储搜索到的专辑列表数据的集合,初始化为null,后续根据搜索结果赋值并用于展示相关专辑信息 private List mAlbumList; + // 用于支持RecyclerView下拉刷新和上拉加载更多功能的适配器,对原始的mAdapter进行包装,添加相关功能支持 private LRecyclerViewAdapter mLRecyclerViewAdapter;//下拉刷新 + // 通过ButterKnife框架绑定的RecyclerView视图组件,用于展示搜索结果列表,支持下拉刷新和上拉加载更多功能 @BindView(R.id.normalView) LRecyclerView mRecycler; + // 通过ButterKnife框架绑定的ImageView视图组件,可能用于显示背景图片等相关用途(从代码中未明确体现具体使用场景) @BindView(R.id.iv_background) ImageView mBackgroundIv; + // 用于接收从外部传入的参数Bundle,可能包含了搜索类型、搜索内容等相关数据,用于在Fragment内部传递和使用 private Bundle mBundle; + // 用于存储搜索内容的字符串,从传入的Bundle中获取,代表用户输入的搜索关键字等信息 private String mSeek; + // 用于存储搜索类型的字符串,从传入的Bundle中获取,表明是歌曲还是专辑等类型的搜索 private String mType; + // 用于与播放服务进行交互的Binder对象,通过它可以调用播放服务中的方法,如控制播放等操作 private PlayerService.PlayStatusBinder mPlayStatusBinder; + // 定义与播放服务的连接对象,用于监听服务的连接与断开状态,并获取服务提供的Binder对象进行交互 private ServiceConnection connection = new ServiceConnection() { + /** + * 当与服务成功连接时调用,获取服务提供的IBinder对象,并强转为对应的PlayStatusBinder类型, + * 以便后续调用播放服务中的方法进行播放控制等操作 + * @param name 连接的服务的组件名称 + * @param service 服务返回的IBinder对象 + */ @Override public void onServiceConnected(ComponentName name, IBinder service) { mPlayStatusBinder = (PlayerService.PlayStatusBinder) service; } + /** + * 当与服务意外断开连接时调用,这里暂时没有具体实现逻辑,可根据需求添加相关处理, + * 比如重新尝试连接、提示用户等操作 + * @param name 断开连接的服务的组件名称 + */ @Override public void onServiceDisconnected(ComponentName name) { } }; + /** + * 在Fragment中加载数据的方法,根据搜索类型(歌曲或专辑)调用Presenter的相应搜索方法发起初始搜索请求, + * 同时调用searchMore方法设置上拉加载更多相关逻辑。 + */ @Override protected void loadData() { if (mType.equals("song")) { @@ -97,6 +130,11 @@ public class SearchContentFragment extends BaseLoadingFragment songListBeans) { mSongList.addAll(songListBeans); @@ -160,23 +235,28 @@ public class SearchContentFragment extends BaseLoadingFragment { SearchSong.DataBean.SongBean.ListBean dataBean = mSongList.get(position); Song song = new Song(); song.setSongId(dataBean.getSongmid()); song.setSinger(getSinger(dataBean)); song.setSongName(dataBean.getSongname()); - song.setImgUrl(Api.ALBUM_PIC+dataBean.getAlbummid()+Api.JPG); + song.setImgUrl(Api.ALBUM_PIC + dataBean.getAlbummid() + Api.JPG); song.setDuration(dataBean.getInterval()); song.setOnline(true); song.setMediaId(dataBean.getStrMediaMid()); song.setDownload(DownloadUtil.isExistOfDownloadSong(dataBean.getSongmid())); - //网络获取歌曲地址 + // 调用Presenter的方法获取歌曲的播放地址,用于后续播放操作 mPresenter.getSongUrl(song); }); } + /** + * 当上拉加载更多歌曲数据成功时调用,将新获取到的歌曲列表数据添加到已有的歌曲列表集合中, + * 然后通知适配器数据集已改变,同时通知RecyclerView上拉加载更多操作完成,更新界面显示状态。 + * @param songListBeans 新获取到的歌曲列表数据集合 + */ @Override public void searchMoreSuccess(ArrayList songListBeans) { mSongList.addAll(songListBeans); @@ -184,11 +264,20 @@ public class SearchContentFragment extends BaseLoadingFragment { @@ -217,17 +310,12 @@ public class SearchContentFragment extends BaseLoadingFragment albumList) { - mAlbumList = new ArrayList<>(); - mAlbumList.addAll(albumList); - mAdapter = new SearchContentAdapter(mAlbumList, mSeek, getActivity(), Constant.TYPE_ALBUM); - mLRecyclerViewAdapter = new LRecyclerViewAdapter(mAdapter); - mRecycler.setLayoutManager(manager); - mRecycler.setAdapter(mLRecyclerViewAdapter); - SearchContentAdapter.setAlbumClick(position -> toAlbumContentFragment(mAlbumList.get(position))); - } - + /** + * 当搜索专辑数据成功时调用,创建专辑列表数据集合并添加获取到的专辑数据, + * 创建搜索内容适配器并进行相关配置,将适配器设置给RecyclerView用于展示专辑列表, + * 同时为专辑列表的每个Item设置点击事件监听,点击时跳转到专辑内容展示的Fragment(通过调用toAlbumContentFragment方法)。 + * @param albumList 搜索到的专辑列表数据集合 + */ @Override public void searchAlbumMoreSuccess(List songListBeans) { mAlbumList.addAll(songListBeans); @@ -235,23 +323,34 @@ public class SearchContentFragment extends BaseLoadingFragment mSearchHistoryList; + // 临时存储搜索历史记录的集合,在一些操作过程中(如数据更新、筛选等)辅助使用,避免直接操作原始数据集合造成问题 private List mTempList; + /** + * 创建Fragment的视图,加载对应的布局文件,并获取布局中的用于展示搜索历史记录的RecyclerView组件。 + * @param inflater 用于将布局文件实例化为视图对象的布局填充器 + * @param container 父视图容器 + * @param savedInstanceState 保存的实例状态信息(如果有的话) + * @return 返回创建好的视图对象 + */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // 加载fragment_search_history布局文件并创建视图对象 View view = inflater.inflate(R.layout.fragment_search_history, container, false); + // 从加载的布局中找到对应的RecyclerView组件 mRecycler = view.findViewById(R.id.recycler_seek_history); return view; } + + /** + * 当Fragment所在的Activity创建完成后调用,主要用于调用展示搜索历史记录的方法以及设置各种点击事件的监听逻辑。 + * @param savedInstanceState 保存的实例状态信息(如果有的话) + */ @Override - public void onActivityCreated(Bundle savedInstanceState){ + public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); + // 调用方法展示搜索历史记录,包括从数据库获取数据、设置适配器、布局管理器等操作 showHistory(); + // 调用方法设置各种点击事件的监听逻辑,如清空历史、删除单条历史记录、点击历史记录重新进行搜索等操作 onClick(); } - - private void showHistory(){ + /** + * 展示搜索历史记录的方法,初始化搜索历史记录集合和临时集合,调用changeList方法更新数据列表, + * 创建搜索历史记录适配器,创建线性布局管理器并设置给RecyclerView,最后将适配器设置给RecyclerView用于展示历史记录列表。 + */ + private void showHistory() { + // 创建用于存储搜索历史记录的空集合 mSearchHistoryList = new ArrayList<>(); + // 创建用于临时存储搜索历史记录的空集合 mTempList = new ArrayList<>(); + // 调用方法更新数据列表,从数据库获取数据并整理到相应集合中 changeList(); + // 创建搜索历史记录适配器,传入搜索历史记录集合,用于将数据绑定到视图上展示 mAdapter = new SearchHistoryAdapter(mSearchHistoryList); + // 创建线性布局管理器,设置为垂直方向排列(默认情况),用于管理RecyclerView中搜索历史记录列表的布局展示 mLayoutManager = new LinearLayoutManager(getActivity()); + // 将线性布局管理器设置给RecyclerView,确定其布局方式 mRecycler.setLayoutManager(mLayoutManager); + // 将适配器设置给RecyclerView,使其能够展示搜索历史记录列表 mRecycler.setAdapter(mAdapter); } - private void onClick(){ + + /** + * 设置各种点击事件的监听逻辑的方法,包括设置清空所有搜索历史记录的点击事件、删除单条搜索历史记录的点击事件以及点击单条搜索历史记录重新进行搜索的点击事件。 + */ + private void onClick() { + // 为搜索历史记录适配器设置底部(例如可能是一个清空历史的按钮所在位置)点击事件监听 mAdapter.setFooterClickListener(new OnFooterClickListener() { @Override public void onClick() { + // 创建一个SpeedDialog对话框实例,用于提示用户确认操作(这里是清空所有搜索历史记录的确认对话框) SpeedDialog deleteDialog = new SpeedDialog(getActivity()); + // 设置对话框的标题为"删除" deleteDialog.setTitle("删除") - .setMessage("确定清空所有搜索历史吗?") - .setSureClickListener(dialog -> { - //删除数据库中的历史记录 + // 设置对话框的提示信息,询问用户是否确定清空所有搜索历史记录 + .setMessage("确定清空所有搜索历史吗?") + // 设置对话框确认按钮的点击事件监听,点击确认时执行以下逻辑 + .setSureClickListener(dialog -> { + // 使用LitePal框架删除数据库中所有的搜索历史记录(SearchHistory表中的所有数据) LitePal.deleteAll(SearchHistory.class); + // 将RecyclerView设置为不可见,因为历史记录已清空,无需展示列表了 mRecycler.setVisibility(View.GONE); - }).show(); + }) + // 显示对话框,呈现给用户进行操作确认 + .show(); } }); + + // 为搜索历史记录适配器设置删除单条历史记录的点击事件监听 mAdapter.setOnDeleteClickListener(new OnDeleteClickListener() { @Override public void onClick(int position) { + // 获取点击位置对应的搜索历史记录对象 SearchHistory searchHistory = mSearchHistoryList.get(position); - if(searchHistory.isSaved()){ + // 如果该搜索历史记录已保存(这里可能根据业务逻辑有保存状态的判断,比如是否已同步等情况,具体看SearchHistory类的实现) + if (searchHistory.isSaved()) { + // 调用该搜索历史记录对象的删除方法(可能是LitePal框架提供的操作或者自定义的数据库删除逻辑)删除这条记录 searchHistory.delete(); } - mTempList =LitePal.findAll(SearchHistory.class); + // 使用LitePal框架查询获取数据库中所有的搜索历史记录,更新临时集合 + mTempList = LitePal.findAll(SearchHistory.class); + // 调用方法更新数据列表,重新整理数据到展示集合中,并更新界面显示 changeList(); + // 通知适配器数据集已改变,触发RecyclerView更新界面展示内容,刷新列表显示最新的历史记录情况 mAdapter.notifyDataSetChanged(); } }); + + // 为搜索历史记录适配器设置点击单条历史记录重新进行搜索的点击事件监听 mAdapter.setOnItemClickListener(new OnItemClickListener() { @Override public void onClick(int position) { - ((AlbumContentFragment.SearchFragment)(getParentFragment())).setSeekEdit(mSearchHistoryList.get(position).getHistory()); + // 获取点击位置对应的搜索历史记录对象,获取其历史记录内容(即之前的搜索关键字) + SearchHistory searchHistory = mSearchHistoryList.get(position); + // 通过获取父Fragment(这里强制转换为AlbumContentFragment.SearchFragment类型,前提是确保类型正确), + // 调用其setSeekEdit方法,将历史记录内容设置到搜索编辑框中,实现点击历史记录重新进行搜索的功能 + ((AlbumContentFragment.SearchFragment) (getParentFragment())).setSeekEdit(searchHistory.getHistory()); } }); } - private void changeList(){ + + /** + * 更新数据列表的方法,先清空用于展示的搜索历史记录集合,然后从数据库中重新获取所有搜索历史记录到临时集合, + * 根据获取到的记录数量判断RecyclerView是否显示(如果没有记录则隐藏,有记录则显示), + * 最后将临时集合中的历史记录逆序添加到展示集合中(可能是为了按照时间等顺序倒序展示历史记录,具体看业务需求)。 + */ + private void changeList() { + // 清空用于展示的搜索历史记录集合 mSearchHistoryList.clear(); + // 使用LitePal框架查询获取数据库中所有的搜索历史记录,更新临时集合 mTempList = LitePal.findAll(SearchHistory.class); - if(mTempList.size()==0){ + // 如果临时集合中没有搜索历史记录(即数据库中没有历史记录了) + if (mTempList.size() == 0) { + // 将RecyclerView设置为不可见,因为没有历史记录可展示了 mRecycler.setVisibility(View.INVISIBLE); - }else{ + } else { + // 如果有历史记录,则将RecyclerView设置为可见,准备展示历史记录列表 mRecycler.setVisibility(View.VISIBLE); } - for(int i=mTempList.size()-1;i>=0;i--){ + // 循环遍历临时集合中的搜索历史记录,从后往前(逆序)添加到展示集合中 + for (int i = mTempList.size() - 1; i >= 0; i--) { SearchHistory searchHistory = mTempList.get(i); mSearchHistoryList.add(searchHistory); } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/widget/BackgroundAnimationRelativeLayout.java b/app/src/main/java/com/example/musicplayer/widget/BackgroundAnimationRelativeLayout.java index ad9195c..c4f2e60 100644 --- a/app/src/main/java/com/example/musicplayer/widget/BackgroundAnimationRelativeLayout.java +++ b/app/src/main/java/com/example/musicplayer/widget/BackgroundAnimationRelativeLayout.java @@ -13,62 +13,100 @@ import android.widget.RelativeLayout; import com.example.musicplayer.R; - - /** * 自定义一个控件,继承RelativeLayout - * */ + * 这个类的主要作用是实现一个具有背景动画效果的RelativeLayout控件, + * 可以让前景图以渐变的方式显示,并在动画结束后进行相关背景设置的更新操作。 + */ public class BackgroundAnimationRelativeLayout extends RelativeLayout { + // 动画持续的时间,单位是毫秒,这里设置为500毫秒,表示动画持续半秒钟 private final int DURATION_ANIMATION = 500; + // 表示LayerDrawable中背景图层的索引,值为0,用于在LayerDrawable数组中定位背景图层 private final int INDEX_BACKGROUND = 0; + // 表示LayerDrawable中前景图层的索引,值为1,用于在LayerDrawable数组中定位前景图层 private final int INDEX_FOREGROUND = 1; /** * LayerDrawable[0]: background drawable * LayerDrawable[1]: foreground drawable + * 用于存储包含背景和前景的LayerDrawable对象,通过索引可以分别操作背景和前景的Drawable。 */ private LayerDrawable layerDrawable; + // 用于控制动画的ObjectAnimator对象,用来实现属性动画效果 private ObjectAnimator objectAnimator; + // 构造函数,调用两个参数的构造函数,并传入默认的属性集合(null) public BackgroundAnimationRelativeLayout(Context context) { this(context, null); } + // 构造函数,调用三个参数的构造函数,并传入默认的样式属性(0) public BackgroundAnimationRelativeLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } - public BackgroundAnimationRelativeLayout(Context context, AttributeSet attrs, int - defStyleAttr) { + /** + * 完整的构造函数,用于初始化控件。 + * 首先调用父类的构造函数完成基本的初始化,然后初始化LayerDrawable和ObjectAnimator。 + * + * @param context 上下文对象,用于获取资源等操作。 + * @param attrs 控件的属性集合,可用于自定义属性的解析等。 + * @param defStyleAttr 默认的样式属性,通常用于兼容不同系统版本的样式设置。 + */ + public BackgroundAnimationRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initLayerDrawable(); initObjectAnimator(); } + /** + * 初始化LayerDrawable的方法。 + * 主要功能是获取背景的Drawable资源,并创建包含背景和前景的LayerDrawable对象, + * 初始化时先将前景与背景颜色设为一致。 + */ private void initLayerDrawable() { + // 获取名为ic_blackground的Drawable资源作为背景Drawable,这里应该是用于设置初始的背景样式。 Drawable backgroundDrawable = getContext().getDrawable(R.drawable.ic_blackground); + // 创建一个长度为2的Drawable数组,用于存放背景和前景的Drawable对象。 Drawable[] drawables = new Drawable[2]; /*初始化时先将前景与背景颜色设为一致*/ + // 将获取到的背景Drawable设置为背景图层的Drawable drawables[INDEX_BACKGROUND] = backgroundDrawable; + // 同样将背景Drawable设置为前景图层的Drawable,此时前景和背景显示效果一样,后续通过动画来改变前景显示效果。 drawables[INDEX_FOREGROUND] = backgroundDrawable; + // 使用包含背景和前景Drawable的数组创建LayerDrawable对象,用于后续对图层的操作。 layerDrawable = new LayerDrawable(drawables); } + /** + * 初始化ObjectAnimator的方法。 + * 创建一个ObjectAnimator对象,用于控制前景图透明度变化的动画效果, + * 并设置动画的持续时间、插值器以及添加动画更新和结束的监听器。 + */ private void initObjectAnimator() { + // 创建一个ObjectAnimator对象,用于对当前控件(this)的名为"number"的属性(这里可能是自定义属性,实际是通过监听器来控制透明度)进行动画操作, + // 动画的值从0f变化到1.0f,通过这个变化来控制前景图的透明度变化。 objectAnimator = ObjectAnimator.ofFloat(this, "number", 0f, 1.0f); + // 设置动画的持续时间为之前定义的DURATION_ANIMATION(500毫秒)。 objectAnimator.setDuration(DURATION_ANIMATION); + // 设置动画的插值器为加速插值器,使得动画在开始阶段加速进行,产生一种先慢后快的视觉效果。 objectAnimator.setInterpolator(new AccelerateInterpolator()); + // 添加动画更新监听器,在动画每一帧更新时执行相应操作。 objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { + // 根据动画当前的值(范围是0f到1.0f)计算出前景图的透明度值(范围是0到255),用于设置前景图的透明度。 int foregroundAlpha = (int) ((float) animation.getAnimatedValue() * 255); /*动态设置Drawable的透明度,让前景图逐渐显示*/ + // 设置前景图层的Drawable的透明度,使得前景图随着动画的进行逐渐显示出来。 layerDrawable.getDrawable(INDEX_FOREGROUND).setAlpha(foregroundAlpha); + // 将设置好透明度的LayerDrawable设置为控件的背景,从而实现前景图在背景上逐渐显示的效果。 BackgroundAnimationRelativeLayout.this.setBackground(layerDrawable); } }); + // 添加动画结束监听器,在动画结束时执行相应操作。 objectAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { @@ -77,6 +115,7 @@ public class BackgroundAnimationRelativeLayout extends RelativeLayout { @Override public void onAnimationEnd(Animator animation) { /*动画结束后,记得将原来的背景图及时更新*/ + // 动画结束后,将前景图层的Drawable设置为背景图层的Drawable,实现一种效果切换或者更新背景的操作。 layerDrawable.setDrawable(INDEX_BACKGROUND, layerDrawable.getDrawable( INDEX_FOREGROUND)); } @@ -92,15 +131,20 @@ public class BackgroundAnimationRelativeLayout extends RelativeLayout { } }); } + + /** + * 用于设置前景Drawable的方法,只有在API级别大于等于23时才可用(通过TargetApi注解指定)。 + * 主要功能是更新LayerDrawable中前景图层的Drawable对象。 + * + * @param drawable 要设置的前景Drawable对象。 + */ @TargetApi(23) public void setForeground(Drawable drawable) { layerDrawable.setDrawable(INDEX_FOREGROUND, drawable); } - //对外提供方法,用于开始渐变动画 + // 对外提供方法,用于开始渐变动画,调用ObjectAnimator的start方法来启动之前设置好的动画。 public void beginAnimation() { objectAnimator.start(); } - - -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/widget/DiscView.java b/app/src/main/java/com/example/musicplayer/widget/DiscView.java index 8d1daed..f67b651 100644 --- a/app/src/main/java/com/example/musicplayer/widget/DiscView.java +++ b/app/src/main/java/com/example/musicplayer/widget/DiscView.java @@ -24,30 +24,49 @@ import com.example.musicplayer.util.DisplayUtil; /** * Created by 残渊 on 2018/10/27. + * 这个类继承自RelativeLayout,用于创建一个自定义的视图,实现类似音乐播放器中唱片和唱针的动画效果以及音乐播放状态控制等功能。 */ - public class DiscView extends RelativeLayout { + // 用于显示唱针的ImageView,代表界面上的唱针元素 private ImageView mIvNeedle; + // 用于控制唱针旋转动画的ObjectAnimator对象,通过它来操作唱针的旋转动画效果 private ObjectAnimator mNeedleAnimator; - + // 用于控制唱片旋转动画的ObjectAnimator对象,用于实现唱片的旋转动画 private ObjectAnimator mObjectAnimator; - /*标记ViewPager是否处于偏移的状态*/ + /* + 标记ViewPager是否处于偏移的状态,用于判断在某些特定情况下是否执行相关动画逻辑, + 例如在ViewPager偏移时可能不启动唱盘旋转动画等情况。 + */ private boolean mViewPagerIsOffset = false; - /*标记唱针复位后,是否需要重新偏移到唱片处*/ + /* + 标记唱针复位后,是否需要重新偏移到唱片处,用于协调唱针动画和唱片动画之间的启动顺序和逻辑, + 比如在特定条件下确定是否要延迟启动唱盘旋转动画等情况。 + */ private boolean mIsNeed2StartPlayAnimator = false; + // 表示音乐当前的状态,初始化为MusicStatus.STOP,有播放(PLAY)、暂停(PAUSE)、停止(STOP)三种可能状态。 private MusicStatus musicStatus = MusicStatus.STOP; + // 定义唱针动画的持续时间,单位为毫秒,这里设置为500毫秒,表示唱针旋转动画完成一次的时间长度。 public static final int DURATION_NEEDLE_ANIAMTOR = 500; + // 唱针当前所处的状态,初始化为NeedleAnimatorStatus.IN_FAR_END,即初始状态下唱针处于远离唱片的位置。 private NeedleAnimatorStatus needleAnimatorStatus = NeedleAnimatorStatus.IN_FAR_END; - - private int mScreenWidth, mScreenHeight; - - - /*唱针当前所处的状态*/ + // 存储屏幕的宽度,通过CommonUtil工具类获取,用于后续根据屏幕尺寸来设置各种视图元素的大小和位置等。 + private int mScreenWidth; + // 存储屏幕的高度,同样通过CommonUtil工具类获取,作用与屏幕宽度类似,辅助布局相关的计算。 + private int mScreenHeight; + + /* + 定义唱针当前所处的状态的枚举类型,包含以下几种情况: + TO_FAR_END:表示唱针正在从唱盘往远处移动的过程中; + TO_NEAR_END:表示唱针正在从远处往唱盘移动的过程中; + IN_FAR_END:表示唱针静止时处于离开唱盘的位置; + IN_NEAR_END:表示唱针静止时处于贴近唱盘的位置。 + 用于清晰地表示和管理唱针在不同时刻的位置状态,以便根据状态来控制相应动画逻辑。 + */ private enum NeedleAnimatorStatus { /*移动时:从唱盘往远处移动*/ TO_FAR_END, @@ -59,27 +78,44 @@ public class DiscView extends RelativeLayout { IN_NEAR_END } - /*音乐当前的状态:只有播放、暂停、停止三种*/ + /* + 定义音乐当前的状态的枚举类型,明确只有三种状态: + PLAY:表示音乐正在播放; + PAUSE:表示音乐处于暂停状态; + STOP:表示音乐处于停止状态。 + 方便统一管理和判断音乐的播放情况,在不同方法中根据此状态来执行相应操作。 + */ public enum MusicStatus { PLAY, PAUSE, STOP } - - + // 构造函数,调用带有两个参数的构造函数,并传入null作为AttributeSet参数,这是一种常见的构造函数重载调用方式。 public DiscView(Context context) { this(context, null); } + // 构造函数,调用带有三个参数的构造函数,并传入0作为defStyleAttr参数,用于初始化视图,同时传递上下文和属性集信息。 public DiscView(Context context, AttributeSet attrs) { this(context, attrs, 0); } + /** + * 主要的构造函数,接收上下文、属性集和样式属性参数,调用父类的构造函数完成初始化, + * 并获取屏幕的宽度和高度,这两个尺寸信息后续会用于视图布局和元素大小设置等操作。 + * @param context 上下文环境,用于获取系统资源等操作。 + * @param attrs 属性集,用于解析在XML布局中定义的该视图的属性信息,可为null。 + * @param defStyleAttr 样式属性,用于指定默认的样式主题相关信息,这里传入0表示使用默认值。 + */ public DiscView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mScreenWidth = CommonUtil.getScreenWidth(context); mScreenHeight = CommonUtil.getScreenHeight(context); } + /** + * 在视图从XML布局文件中加载完成后会调用此方法,用于执行一些初始化操作, + * 比如初始化唱片图片、唱针相关设置以及动画相关的初始化等工作,确保视图展示所需的各种元素和效果准备就绪。 + */ @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -89,64 +125,111 @@ public class DiscView extends RelativeLayout { initObjectAnimator(); } + /** + * 初始化唱片相关的图片显示及布局参数设置,主要完成以下工作: + * 1. 找到代表唱片背景的ImageView; + * 2. 获取控制唱片旋转的ObjectAnimator实例; + * 3. 设置唱片背景的Drawable,这个Drawable是由空心圆盘和音乐专辑图片合成得到的; + * 4. 根据屏幕高度计算唱片在布局中的上边距,以调整唱片在界面上的显示位置。 + */ private void initDiscImg() { + // 通过findViewById方法在当前布局中查找id为iv_disc_background的ImageView,它代表唱片的背景元素。 ImageView mDiscBackground = findViewById(R.id.iv_disc_background); - mObjectAnimator=getDiscObjectAnimator(mDiscBackground); + // 获取用于控制唱片旋转动画的ObjectAnimator对象,传入唱片背景的ImageView作为参数,以便后续操作该视图的旋转动画。 + mObjectAnimator = getDiscObjectAnimator(mDiscBackground); + // 设置唱片背景的Drawable,通过调用getDiscDrawable方法获取合成后的Drawable(包含专辑图片和圆盘图片)。 mDiscBackground.setImageDrawable(getDiscDrawable( - BitmapFactory.decodeResource(getResources(),R.drawable.default_disc) + BitmapFactory.decodeResource(getResources(), R.drawable.default_disc) )); - + // 根据屏幕高度以及定义好的比例(DisplayUtil.SCALE_DISC_MARGIN_TOP)计算唱片的上边距,用于调整唱片在布局中的垂直位置。 int marginTop = (int) (DisplayUtil.SCALE_DISC_MARGIN_TOP * mScreenHeight); LayoutParams layoutParams = (LayoutParams) mDiscBackground - .getLayoutParams(); + .getLayoutParams(); + // 设置唱片背景ImageView的外边距,将上边距设置为计算得到的值,左右和下边距设置为0,从而定位唱片在布局中的位置。 layoutParams.setMargins(0, marginTop, 0, 0); mDiscBackground.setLayoutParams(layoutParams); } - - - + /** + * 初始化唱针相关的属性设置,包括唱针的大小、位置、旋转角度以及对应的布局参数等内容,具体如下: + * 1. 通过findViewById找到代表唱针的ImageView; + * 2. 根据屏幕宽度和高度以及定义好的比例(各种SCALE_*常量)计算唱针的宽度和高度,使其适配不同屏幕尺寸; + * 3. 通过设置外边距(部分外边距为负数)来隐藏唱针的一部分,实现特定的显示效果; + * 4. 对唱针的原始Bitmap进行缩放处理,使其大小符合计算后的尺寸; + * 5. 设置唱针旋转的中心点坐标(基于屏幕宽度的比例计算); + * 6. 设置唱针的初始旋转角度,并将处理后的Bitmap设置给唱针的ImageView,最后更新其布局参数。 + */ private void initNeedle() { - mIvNeedle = findViewById(R.id.iv_needle); + // 通过findViewById方法在当前布局中查找id为iv_needle的ImageView,它代表唱针元素。 + mIvNeedle = findViewById(R.id.iv_needle); + // 根据屏幕宽度以及定义好的比例(DisplayUtil.SCALE_NEEDLE_WIDTH)计算唱针的宽度,使其在不同屏幕上显示合适大小。 int needleWidth = (int) (DisplayUtil.SCALE_NEEDLE_WIDTH * mScreenWidth); + // 根据屏幕高度以及定义好的比例(DisplayUtil.SCALE_NEEDLE_HEIGHT)计算唱针的高度,同样用于适配屏幕尺寸。 int needleHeight = (int) (DisplayUtil.SCALE_NEEDLE_HEIGHT * mScreenHeight); - /*设置手柄的外边距为负数,让其隐藏一部分*/ + /* + 设置唱针手柄的外边距,通过将上边距设置为负数(基于屏幕高度和定义好的比例DisplayUtil.SCALE_NEEDLE_MARGIN_TOP计算), + 让唱针的一部分隐藏起来,达到特定的视觉效果。 + */ int marginTop = (int) (DisplayUtil.SCALE_NEEDLE_MARGIN_TOP * mScreenHeight) * -1; + // 根据屏幕宽度以及定义好的比例(DisplayUtil.SCALE_NEEDLE_MARGIN_LEFT)计算唱针的左边距,确定其水平位置。 int marginLeft = (int) (DisplayUtil.SCALE_NEEDLE_MARGIN_LEFT * mScreenWidth); + // 从资源文件(R.drawable.ic_needle)中获取唱针的原始Bitmap对象,用于后续的处理和显示。 Bitmap originBitmap = BitmapFactory.decodeResource(getResources(), R.drawable - .ic_needle); + .ic_needle); + // 根据计算得到的宽度和高度对原始唱针Bitmap进行缩放处理,创建一个新的缩放后的Bitmap,用于设置给唱针的ImageView。 Bitmap bitmap = Bitmap.createScaledBitmap(originBitmap, needleWidth, needleHeight, false); LayoutParams layoutParams = (LayoutParams) mIvNeedle.getLayoutParams(); + // 设置唱针ImageView的外边距,将左边距和上边距设置为计算得到的值,右边和下边距设置为0,确定唱针在布局中的位置。 layoutParams.setMargins(marginLeft, marginTop, 0, 0); + // 根据屏幕宽度以及定义好的比例(DisplayUtil.SCALE_NEEDLE_PIVOT_X)计算唱针旋转的中心点X坐标,用于确定旋转中心位置。 int pivotX = (int) (DisplayUtil.SCALE_NEEDLE_PIVOT_X * mScreenWidth); + // 根据屏幕宽度以及定义好的比例(DisplayUtil.SCALE_NEEDLE_PIVOT_Y)计算唱针旋转的中心点Y坐标,同样用于定位旋转中心。 int pivotY = (int) (DisplayUtil.SCALE_NEEDLE_PIVOT_Y * mScreenWidth); + // 设置唱针ImageView的旋转中心点的X坐标,使其绕该点进行旋转动画操作。 mIvNeedle.setPivotX(pivotX); + // 设置唱针ImageView的旋转中心点的Y坐标,与X坐标共同确定旋转中心位置。 mIvNeedle.setPivotY(pivotY); + // 设置唱针的初始旋转角度,角度值由DisplayUtil.ROTATION_INIT_NEEDLE定义,一般为远离唱片的初始角度。 mIvNeedle.setRotation(DisplayUtil.ROTATION_INIT_NEEDLE); + // 将缩放后的唱针Bitmap设置给唱针的ImageView,使其显示在界面上。 mIvNeedle.setImageBitmap(bitmap); + // 更新唱针ImageView的布局参数,使设置的外边距、旋转中心点等属性生效,完成唱针的初始化布局设置。 mIvNeedle.setLayoutParams(layoutParams); } + /** + * 初始化唱针的动画相关设置,包括以下关键操作: + * 1. 创建一个ObjectAnimator对象用于控制唱针的旋转动画,设置旋转角度从初始角度(DisplayUtil.ROTATION_INIT_NEEDLE)到0度(贴近唱片的角度)变化; + * 2. 设置唱针动画的持续时间为DURATION_NEEDLE_ANIAMTOR(500毫秒); + * 3. 设置动画的插值器为AccelerateInterpolator,实现加速的动画效果; + * 4. 为唱针动画添加AnimatorListener监听器,在动画开始、结束、取消和重复时执行相应的逻辑处理,例如根据动画开始前的唱针状态来确定动画进行时的状态, + * 在动画结束时根据唱针状态控制唱片动画的播放、暂停以及处理后续是否需要延迟启动唱盘旋转动画等逻辑。 + */ private void initObjectAnimator() { + // 创建一个ObjectAnimator对象,用于控制唱针的旋转动画,指定动画作用的视图(mIvNeedle)、动画属性(View.ROTATION表示旋转角度)以及起始和结束的角度值。 mNeedleAnimator = ObjectAnimator.ofFloat(mIvNeedle, View.ROTATION, DisplayUtil - .ROTATION_INIT_NEEDLE, 0); + .ROTATION_INIT_NEEDLE, 0); + // 设置唱针动画的持续时间,这里使用之前定义好的常量DURATION_NEEDLE_ANIAMTOR(500毫秒),确定动画完成一次旋转的时间长度。 mNeedleAnimator.setDuration(DURATION_NEEDLE_ANIAMTOR); + // 设置唱针动画的插值器为AccelerateInterpolator,使得唱针在旋转过程中呈现加速的动画效果,增强视觉上的真实感。 mNeedleAnimator.setInterpolator(new AccelerateInterpolator()); mNeedleAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { /** - * 根据动画开始前NeedleAnimatorStatus的状态, - * 即可得出动画进行时NeedleAnimatorStatus的状态 - * */ + * 根据动画开始前NeedleAnimatorStatus(唱针当前所处的状态)的状态, + * 来确定动画进行时NeedleAnimatorStatus的状态,以便后续根据状态进行相应的逻辑处理。 + * 例如,如果开始前唱针处于远离唱片(IN_FAR_END)状态,那么动画进行时就是往唱片方向移动(TO_NEAR_END); + * 如果开始前唱针处于贴近唱片(IN_NEAR_END)状态,动画进行时就是往远离唱片方向移动(TO_FAR_END)。 + */ if (needleAnimatorStatus == NeedleAnimatorStatus.IN_FAR_END) { needleAnimatorStatus = NeedleAnimatorStatus.TO_NEAR_END; } else if (needleAnimatorStatus == NeedleAnimatorStatus.IN_NEAR_END) { @@ -171,8 +254,10 @@ public class DiscView extends RelativeLayout { if (mIsNeed2StartPlayAnimator) { mIsNeed2StartPlayAnimator = false; /** - * 只有在ViewPager不处于偏移状态时,才开始唱盘旋转动画 - * */ + * 只有在ViewPager不处于偏移状态(mViewPagerIsOffset为false)时,才开始唱盘旋转动画, + * 否则需要等待合适的时机(比如ViewPager回到正常位置)再启动唱盘动画,通过延迟50毫秒后调用playAnimator方法来实现。 + * 这里的延迟是为了确保相关状态和条件准备就绪,避免出现动画冲突或不符合预期的显示效果。 + */ if (!mViewPagerIsOffset) { /*延时500ms*/ DiscView.this.postDelayed(new Runnable() { @@ -197,139 +282,7 @@ public class DiscView extends RelativeLayout { }); } - - - /** - * 得到唱盘图片 - * 唱盘图片由空心圆盘及音乐专辑图片“合成”得到 - */ - public Drawable getDiscDrawable(Bitmap bitmap) { - int discSize = (int) (mScreenWidth * DisplayUtil.SCALE_DISC_SIZE); - int musicPicSize = (int) (mScreenWidth * DisplayUtil.SCALE_MUSIC_PIC_SIZE); - - Bitmap bitmapDisc = Bitmap.createScaledBitmap(BitmapFactory.decodeResource(getResources(), R - .drawable.ic_disc), discSize, discSize, false); - Bitmap bitmapMusicPic = Bitmap.createScaledBitmap(bitmap, musicPicSize, musicPicSize, true); - BitmapDrawable discDrawable = new BitmapDrawable(bitmapDisc); - RoundedBitmapDrawable roundMusicDrawable = RoundedBitmapDrawableFactory.create - (getResources(), bitmapMusicPic); - - //抗锯齿 - discDrawable.setAntiAlias(true); - roundMusicDrawable.setAntiAlias(true); - - Drawable[] drawables = new Drawable[2]; - drawables[0] = roundMusicDrawable; - drawables[1] = discDrawable; - - LayerDrawable layerDrawable = new LayerDrawable(drawables); - int musicPicMargin = (int) ((DisplayUtil.SCALE_DISC_SIZE - DisplayUtil - .SCALE_MUSIC_PIC_SIZE) * mScreenWidth / 2); - //调整专辑图片的四周边距,让其显示在正中 - layerDrawable.setLayerInset(0, musicPicMargin, musicPicMargin, musicPicMargin, - musicPicMargin); - - return layerDrawable; - } - - public ObjectAnimator getDiscObjectAnimator(ImageView disc) { - ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(disc, View.ROTATION, 0, 360); - objectAnimator.setRepeatCount(ValueAnimator.INFINITE); - objectAnimator.setDuration(30 * 1000); - objectAnimator.setInterpolator(new LinearInterpolator()); - - return objectAnimator; - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - } - - /*播放动画*/ - private void playAnimator() { - /*唱针处于远端时,直接播放动画*/ - if (needleAnimatorStatus == NeedleAnimatorStatus.IN_FAR_END) { - mNeedleAnimator.start(); - } - /*唱针处于往远端移动时,设置标记,等动画结束后再播放动画*/ - else if (needleAnimatorStatus == NeedleAnimatorStatus.TO_FAR_END) { - mIsNeed2StartPlayAnimator = true; - } - } - - /*暂停动画*/ - private void pauseAnimator() { - /*播放时暂停动画*/ - if (needleAnimatorStatus == NeedleAnimatorStatus.IN_NEAR_END) { - pauseDiscAnimatior(); - } - /*唱针往唱盘移动时暂停动画*/ - else if (needleAnimatorStatus == NeedleAnimatorStatus.TO_NEAR_END) { - mNeedleAnimator.reverse(); - /** - * 若动画在没结束时执行reverse方法,则不会执行监听器的onStart方法,此时需要手动设置 - * */ - needleAnimatorStatus = NeedleAnimatorStatus.TO_FAR_END; - } - - } - - /*播放唱盘动画*/ - private void playDiscAnimator() { - if (mObjectAnimator.isPaused()) { - mObjectAnimator.resume(); - } else { - mObjectAnimator.start(); - } - - } - - /*暂停唱盘动画*/ - private void pauseDiscAnimatior() { - - mObjectAnimator.pause(); - mNeedleAnimator.reverse(); - } - - - public void play() { - playAnimator(); - } - - public void pause() { - musicStatus = MusicStatus.PAUSE; - pauseAnimator(); - } - - public void stop() { - musicStatus = MusicStatus.STOP; - pauseAnimator(); - } - - - - public void next() { - playAnimator(); - selectMusicWithButton(); - } - - public void last() { - playAnimator(); - selectMusicWithButton(); - } - - public boolean isPlaying() { - return musicStatus == MusicStatus.PLAY; - } - - private void selectMusicWithButton() { - if (musicStatus == MusicStatus.PLAY) { - mIsNeed2StartPlayAnimator = true; - pauseAnimator(); - } else if (musicStatus == MusicStatus.PAUSE) { - play(); - } - } -} + * 获取合成后的唱片Drawable,该Drawable由空心圆盘和音乐专辑图片组成,具体实现步骤如下: + * 1. 根据屏幕宽度以及定义好的比例(DisplayUtil.SCALE_DISC_SIZE和DisplayUtil.SCALE_MUSIC_PIC_SIZE)计算唱片和音乐专辑图片的显示尺寸; + * 2. 分别对空心圆盘的Bitmap和音乐专辑的Bitmap进行缩放处理,使其大小符合计算后的尺寸要求 \ No newline at end of file diff --git a/app/src/main/java/com/example/musicplayer/widget/MyListView.java b/app/src/main/java/com/example/musicplayer/widget/MyListView.java index d99f563..18a5dbd 100644 --- a/app/src/main/java/com/example/musicplayer/widget/MyListView.java +++ b/app/src/main/java/com/example/musicplayer/widget/MyListView.java @@ -6,16 +6,36 @@ import android.widget.ExpandableListView; import android.widget.ListView; /** - * Created by 残渊 on 2018/9/25. + * MyListView类,继承自ExpandableListView,用于自定义列表视图相关的功能。 + * 这里可能是在音乐播放器项目中,针对特定需求对ExpandableListView进行扩展或定制化的操作。 + * 作者是“残渊”,创建时间为2018年9月25日。 */ - public class MyListView extends ExpandableListView { + + /** + * 构造方法,用于创建MyListView的实例。 + * 该构造方法接收一个Context对象和一个AttributeSet对象作为参数。 + * Context对象用于获取应用程序环境相关的全局信息等,AttributeSet对象通常用于从XML布局文件中获取该视图定义的属性集合。 + * @param context 应用上下文,提供了访问应用资源、系统服务等的入口。 + * @param attrs 包含了在XML布局文件中为该视图定义的属性信息。 + */ public MyListView(Context context, AttributeSet attrs) { super(context, attrs); } + /** + * 重写onMeasure方法,该方法用于测量视图的大小(宽度和高度)。 + * 在这个自定义的实现中,对高度的测量规格(heightMeasureSpec)进行了特殊的设置。 + * 通过MeasureSpec.makeMeasureSpec方法来重新构造高度测量规格,目的是让列表视图在高度上具有特定的表现,比如能够自适应内容高度等情况(这里具体要看实际需求,从代码看是一种常见的处理方式让列表能更好地展示内容而不局限于固定高度)。 + * 参数widthMeasureSpec表示宽度的测量规格,它由父视图传递过来,决定了该视图可以使用的宽度相关信息。 + * 参数heightMeasureSpec表示高度的测量规格,原本也是由父视图传递过来,但在这里被重新设置了。 + * @param widthMeasureSpec 宽度测量规格。 + * @param heightMeasureSpec 高度测量规格,在这里会被重新构造后用于后续的测量逻辑。 + */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // 将高度测量规格重新设置,让高度可以根据内容尽量伸展(采用了一种常见的处理技巧,使用Integer.MAX_VALUE进行移位操作来构造合适的测量模式和大小值)。 + // MeasureSpec.AT_MOST表示视图的大小最多为指定的值,这里构造的新值使得列表高度可以在一定范围内自适应内容。 heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, heightMeasureSpec); }