android 实现简易音乐播放器

音乐App

源代码 : 简易音乐APP源代码

1、简介

一个简易的音乐APP,主要练习对四大组件的应用。感兴趣的可以看看。

播放界面如下:

在这里插入图片描述

歌曲列表界面如下:

在这里插入图片描述

项目结构如下:

在这里插入图片描述

在这里插入图片描述

接下来将对代码做详细介绍:

2、Music: 音频对象

public class Music {private String name;//歌曲的名称private String author;//歌曲的作者(歌手)private long time;//歌曲的时长private String id;//歌曲的唯一Id  private String url;//歌曲的地址
}

特殊说明: 由于本APP没有使用数据库而是使用 List 去存储对象信息,所以没找到合适的属性值去唯一代表一个音频。此id用的是 name+author进行字符串拼接而成。

这种做法很有可能会发生 id 碰撞。如有严格需求,请自行解决。

3、BaseActivity: 自定义Activity去继承AppCompatActivity。此Class主要用来存放一些全局都要访问的东西。

public class BaseActivity extends AppCompatActivity {//用来存放音频对象。public static List<Music> musicList = null;//用来标志 当前播放的是第几首歌, 值代表在 musicList 中的下标。public static int currentOrder = -1;//不多解释,就看成一个解析音频文件的工具即可protected MediaMetadataRetriever retriever;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);retriever = new MediaMetadataRetriever();}@SuppressLint("Range")protected void initMusicList() {//此处是有代码的,后面再具体讲解}}

4、activity_main.xml:主界面,这里主要是用了一个相对布局,没什么好讲的。

后面会把整个项目代码放到资源里,免费使用。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"tools:ignore="UselessParent"><LinearLayoutandroid:id="@+id/title"android:orientation="horizontal"android:layout_width="match_parent"android:layout_height="70sp"android:layout_alignParentTop="true"><TextViewandroid:layout_width="0dp"android:layout_weight="5"android:layout_height="match_parent"android:layout_marginStart="5sp"android:text="@string/app_name"android:textSize="30sp"android:textColor="#1295DA"android:gravity="center|start"/><ImageButtonandroid:id="@+id/btn_list"android:layout_width="0dp"android:layout_weight="1"android:layout_height="match_parent"android:background="@drawable/list"android:scaleType="fitCenter"/></LinearLayout><ImageButtonandroid:id="@+id/music"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/music"android:layout_marginTop="70sp"android:layout_centerInParent="true"android:layout_below="@+id/title"android:scaleType="fitCenter"/><LinearLayoutandroid:id="@+id/music_message"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="70sp"android:layout_below="@+id/music"android:orientation="vertical"><TextViewandroid:id="@+id/tv_music_name"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_marginStart="10sp"android:textSize="29sp"android:textColor="#000000"android:text="@string/default_music"/><TextViewandroid:id="@+id/tv_music_author"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_marginStart="10sp"android:textSize="25sp"android:text="@string/default_author"/></LinearLayout><SeekBarandroid:id="@+id/seekBar"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="60sp"android:layout_below="@+id/music_message"/><RelativeLayoutandroid:layout_below="@+id/seekBar"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><TextViewandroid:id="@+id/tv_now_time"android:layout_marginStart="10sp"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/default_music_time"/><TextViewandroid:id="@+id/tv_all_time"android:layout_marginEnd="15sp"android:layout_alignParentEnd="true"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/default_music_time"/></RelativeLayout><LinearLayoutandroid:layout_width="match_parent"android:layout_height="80sp"android:layout_alignParentBottom="true"android:layout_marginBottom="20sp"android:orientation="horizontal"><ImageButtonandroid:id="@+id/btn_last"android:layout_width="0dp"android:layout_height="match_parent"android:layout_marginEnd="1sp"android:layout_weight="1"android:background="@color/white"android:scaleType="fitCenter"android:src="@drawable/last" /><ImageButtonandroid:id="@+id/btn_start"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:background="@color/white"android:scaleType="fitCenter"android:src="@drawable/start" /><ImageButtonandroid:id="@+id/btn_next"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:background="@color/white"android:scaleType="fitCenter"android:src="@drawable/next" /></LinearLayout></RelativeLayout>
</LinearLayout>

5、MainActivity:主Activity 。代码很长,分模块讲解。

属性:

protected static String CURRENT_ID = "-1";  //当前正在播放的歌曲id
protected static Music currentMusic;
protected static boolean isBind = false;
protected ImageButton btn_list, btn_last, btn_start, btn_next;
protected SeekBar seekBar;
protected TextView tv_music_name, tv_music_author, tv_all_time, tv_now_time;
protected static int Flag = 0; //当前的状态 1:正在播放 0:暂停
protected MusicService.MusicBinder musicBinder;
protected MusicServiceConnection musicServiceConnection;
public static LocalBroadcastManager localBroadcastManager;
private static final int REQ_READ_EXTERNAL_STORAGE = 1;
private static Boolean IS_PERMISSION = false; //是否授予权限

5.1、onCreate()

protected void onCreate(Bundle savedInstanceState) {...//省略一些属性赋值。//获取权限requestPermissionByHand();//注册广播registerBroadCast();//绑定服务startAndBindService();//启动服务//进度条seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {if (currentMusic == null) {ToastUtil.toast(MainActivity.this, "未播放歌曲");}}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {int progress = seekBar.getProgress();tv_now_time.setText(format(progress));musicBinder.seekTo(progress);}});oprSeekBar(false);//刚开始不允许操作}
5.1.1、requestPermissionByHand(): 因为要读取音频文件,第一步肯定要先进行授权。代码就是很标准的权限获取流程。
  public void requestPermissionByHand() {//检查有没有这个权限int checkWriteStoragePermission = ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE);//如果没有被授予if (checkWriteStoragePermission != PackageManager.PERMISSION_GRANTED) {//请求权限,此处可以同时申请多个权限ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},REQ_READ_EXTERNAL_STORAGE);//这里会根据授权的结果,去调用onRequestPermissionsResult 相应的操作。} else {//如果已经有权限了,把这个标识设为 true,后面讲为什么。IS_PERMISSION = true;initMusicList();}}@Overridepublic void onRequestPermissionsResult(int requestCode, final String[] permissions, int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);switch (requestCode) {case REQ_READ_EXTERNAL_STORAGE:// 如果请求被取消了,那么结果数组就是空的if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {// 权限被授予了initMusicList();//初始化数据IS_PERMISSION = true;} else {//拒绝了权限请求,弹出提示,然后退出程序。ToastUtil.toast(MainActivity.this, "请前往设置授予权限");}break;default:break;}}

==注意:==当我们安装完应用后第一次启动时如果拒绝了权限请求。那么再次启动应用时,它会默认为禁止此权限,且 ActivityCompat.requestPermissions()将不会再弹出权限授予框进行选择。如果想获取权限,只能手动去手机应用设置处授权。

IS_PERMISSION: 这玩意是干啥用的?

主要是考虑到下列情景:

如果第一次授权被拒绝了,程序虽然自动结束了,但我发现其实它仍在后台进行(才疏学浅,没找到彻底杀死进程的方法)。这个时候我们去手动授权结束后,再次打开APP(),其实是执行了 onStop()->onRestart()->onResume()这样一个流程(activity的生命周期)。那我们这时应该再去判断一次,是否授权。如果缺少这次判断,那么应用将会一直退出。(虽然我们手动授权了,但是app自己不知道,必须告诉它一声)。

@Override
protected void onRestart() {super.onRestart();if (!IS_PERMISSION) {//当从后台进入时,判断应用是否已经有权限了 ,没有就去申请requestPermissionByHand();}
}

为什么不放在 onResume()里面呢? 这个主要是会出现重复授权请求的情况(可以自己思考一下哈)。

仔细留意可以看到,我们在授权完成后,其实是去执行了 BaeActivity.initMusicList()方法。

5.1.2 initMusicList(): 初始化音频数据
@SuppressLint("Range")
protected void initMusicList() {musicList = new ArrayList<>();ContentResolver contentResolver = getContentResolver(); //系统提供的内容提供者,可以通过去去访问一些数据。Cursor cursor = null;//读取sd卡//这一部分直接用就行try {cursor = contentResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,null, null, null, null);if (cursor != null) {while (cursor.moveToNext()) {//是否是音频int isMusic = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.IS_MUSIC));//时长long duration = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));//是音乐并且时长大于1分钟if (isMusic != 0 && duration >= 60 * 1000) {//歌名String musicName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));//歌手String musicAuthor = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));//文件路径String musicPath = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));//歌名,歌手,时长,专辑,图标,文件路径,sequence number of list in listviewMusic music = new Music(musicName, musicAuthor, duration, musicName + musicAuthor, musicPath);musicList.add(music);}}}} catch (Exception e) {e.printStackTrace();} finally {if (cursor != null)cursor.close();//用完要关闭}//主要是这一部分//这一部分是可有可无,上面一部分是读取的本地的音频文件//这一部分主要是 将两个音频文件塞进了app内部,进行测试系统功能,可删除//在上面系统结构图中可以看到 ,我在 /res/raw 下放了两首 MP3// 由于没找到具体去直接遍历的操作,所以这里使用了暴力去解决,即把文件名设置成有规律的,如:m1,m2这样。// 如果有好方法可以提出来。try {for (int i = 1; i <= 2; i++) {Uri uri = Uri.parse("android.resource://" + getPackageName() + "/raw/m" + i);retriever.setDataSource(this,uri);String musicName = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);if(musicName == null) musicName = "music"+i;//歌手String musicAuthor = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);if(musicAuthor == null) musicAuthor = "网络歌手";//文件路径String musicPath = "android.resource://" + getPackageName() + "/raw/m" + i;//时长String duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);//歌名,歌手,时长,专辑,图标,文件路径,sequence number of list in listviewMusic music = new Music(musicName, musicAuthor, Long.parseLong(duration), musicName + musicAuthor, musicPath);musicList.add(music);}}catch (Exception e){e.printStackTrace();}finally {if(retriever != null) retriever.release();}
}

到这里 requestPermissionByHand()就结束了,就是 授权+读文件

5.1.3、registerBroadCast();

注册广播: 这里采用的是 本地广播 + 动态注册

private void registerBroadCast() {localBroadcastManager = LocalBroadcastManager.getInstance(this);MusicReceiver musicReceiver = new MusicReceiver();IntentFilter intentFilter = new IntentFilter();intentFilter.addAction("com.xhy.musicRunning");localBroadcastManager.registerReceiver(musicReceiver, intentFilter);
}
class MusicReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {Bundle bundle = intent.getBundleExtra("bundle");int currentPosition = bundle.getInt("currentPosition");seekBar.setProgress(currentPosition);tv_now_time.setText(format(currentPosition));if (format(currentPosition).equals(format(seekBar.getMax())) && Flag == 1) {handleEnd();}}
}

ok,先到这里,后面再讲 MusicReceiver的操作。

5.1.4、startAndBindService()
private void startAndBindService() {Intent intent = new Intent(MainActivity.this, MusicService.class);musicServiceConnection = new MusicServiceConnection();startService(intent);bindService(intent, musicServiceConnection, BIND_AUTO_CREATE);
}
class MusicServiceConnection implements ServiceConnection {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {musicBinder = (MusicService.MusicBinder) service;isBind = true;}@Overridepublic void onServiceDisconnected(ComponentName name) {}
}

这就是很标准的服务绑定流程。

5.1.5、seekBar.setOnSeekBarChangeListener()

这种都比较好理解,不多讲。

seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {if (currentMusic == null) {ToastUtil.toast(MainActivity.this, "未播放歌曲");}}//主要看这个//当我们滑动或者点击进度条时,会跟随改变歌曲的进度。@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {int progress = seekBar.getProgress(); // progress就是代表当前进度条的数据tv_now_time.setText(format(progress)); //修改展示的当前时间(歌曲的进度)musicBinder.seekTo(progress);}
});

format() : 将 ms 转化成 mm:ss 的格式

private String format(long time) {int minute = 0;int second = 0;minute = (int) (time / (1000 * 60)) % 60;second = (int) (time / 1000) % 60;return String.format("%02d", minute) + ":" + String.format("%02d", second);
}
5.1.6、oprSeekBar():

刚开始,seekBar处于不可点击状态。本应用启动时是不会主动播放歌曲的,也就是处于 暂无歌曲状态。seekBar此时应处于不可用状态(因为有监听点击事件,会导致一些错误)。

private void oprSeekBar(Boolean clickable) { //禁止拖动seekBar.setClickable(clickable);seekBar.setEnabled(clickable);seekBar.setFocusable(clickable);
}

onCreate() 到这里就暂时先结束,我们要先去看服务。

6、MusicService

public class MusicService extends Service {//用来控制音乐的播放与暂停。系统自带的protected MediaPlayer mediaPlayer;//定时器protected Timer timer;//广播管理器//用的是 MainActivity中的public static LocalBroadcastManager localBroadcastManager; public MusicService() {}@Overridepublic void onCreate() {super.onCreate();mediaPlayer = new MediaPlayer();localBroadcastManager = MainActivity.localBroadcastManager;}private void createTimer() {if (timer == null) {timer = new Timer();TimerTask timerTask = new TimerTask() { //定时任务@Overridepublic void run() {//还没有播放器的时候,就直接退出。if(mediaPlayer == null) return;//当前进度, mediaPlayer 自带API,获取当前音频播放到哪里了int currentPosition = mediaPlayer.getCurrentPosition();//携带数据Bundle bundle=new Bundle();bundle.putInt("currentPosition",currentPosition);Intent intent = new Intent();intent.setAction("com.xhy.musicRunning");intent.setClassName("com.xhy.musicplayer","MainActivity&MusicReceiver");intent.putExtra("bundle",bundle);//发送广播localBroadcastManager.sendBroadcast(intent);}};timer.schedule(timerTask,1,1000); // 1ms后,每1000ms执行 一次 TimerTask;//总结下来就是,只要有 mediaPlay的存在,就把当前歌曲播放的具体时长 以广播的形式发送,由MainActivity进行捕获与响应}}@Overridepublic IBinder onBind(Intent intent) {return new MusicBinder();}//用来绑定服务,这样可以通过Activity 与服务进行交互了public class MusicBinder extends Binder {public void play(String url){//String pathUri uri= Uri.parse(url);try{//重置音乐播放器mediaPlayer.reset();//加载多媒体文件mediaPlayer=MediaPlayer.create(getApplicationContext(),uri);mediaPlayer.start();//播放音乐createTimer();//添加计时器}catch(Exception e){e.printStackTrace();}}//下面的暂停继续和退出方法全部调用的是MediaPlayer自带的方法public void pausePlay(){mediaPlayer.pause();//暂停播放音乐}public void continuePlay(){mediaPlayer.start();//继续播放音乐}public void seekTo(int progress){mediaPlayer.seekTo(progress);//设置音乐的播放位置}//播放下一首public void nextPlay(){//当前的下标加1,BaseActivity.currentOrder +=1;//确定下一首歌的坐标if(BaseActivity.currentOrder == BaseActivity.musicList.size()) BaseActivity.currentOrder = 0;//获取下一首歌的对象Music nextMusic = BaseActivity.musicList.get(BaseActivity.currentOrder);//播放play(nextMusic.getUrl());}//播放上一首public void lastPlay(){BaseActivity.currentOrder -=1;if(BaseActivity.currentOrder == -1) BaseActivity.currentOrder = 0;Music lastMusic = BaseActivity.musicList.get(BaseActivity.currentOrder);play(lastMusic.getUrl());}}@Overridepublic void onDestroy() { //当服务被销毁就 销毁 mediaPlayer,释放资源super.onDestroy();if(mediaPlayer==null) return;if(mediaPlayer.isPlaying()) mediaPlayer.stop();//停止播放音乐mediaPlayer.release();//释放占用的资源mediaPlayer=null;//将player置为空if(timer != null) timer = null;}
}

ok,此时我们回去看一下,广播接收器干了什么。

class MusicReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {Bundle bundle = intent.getBundleExtra("bundle");int currentPosition = bundle.getInt("currentPosition");seekBar.setProgress(currentPosition);//调整进度条tv_now_time.setText(format(currentPosition)); //设置当前的播放时间if (format(currentPosition).equals(format(seekBar.getMax())) && Flag == 1) {//如果进度条已经到头了handleEnd();}}
}
private void handleEnd() {//歌曲放完了,相当于触发一次下一首Flag = 0;//先暂停这一首,然后执行下一首btn_start.setImageResource(R.drawable.start);ToastUtil.toast(MainActivity.this, "即将播放下一首");//延迟2.5s,播放下一首new Handler().postDelayed(new Runnable() {@Overridepublic void run() {btn_next.performClick();Log.d("TestRecycler", "发送消息");//如果此时是在歌曲列表界面,发个消息if (MusicListActivity.musicHandler != null) {Message message = new Message();message.what = MusicListActivity.UPDATE_TEXT;MusicListActivity.musicHandler.sendMessage(message);}}}, 2500);
}

总结来说:MusicReceiver 就复杂监听音乐的播放,动态的去更新 界面上时间及进度条的显示。

if (format(currentPosition).equals(format(seekBar.getMax())) && Flag == 1) {//如果进度条已经到头了handleEnd();
}

==提示:==这里简单的提一下,为什么要判断 format 之后的 字符串 而不是直接比较 currentPositionseekBar.getMax()

因为我们接受的是广播,且广播一秒才发一次,再加上传播产生的时间,在 ms 时间级内, currentPositionseekBar.getMax()。大概不不会出现相等。所以这里比较的是格式化后的 s 级内。

MusicService就到这里

7、MusicListActivity

歌曲列表界面。这里采用的是 RecyclerView 布局去展示。

public class MusicListActivity extends BaseActivity {protected ImageButton btn_back;public static Handler musicHandler;public static final int UPDATE_TEXT = 1;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_music_list);RecyclerView recyclerView = findViewById(R.id.recycle_view);LinearLayoutManager layoutManager = new LinearLayoutManager(this);recyclerView.setLayoutManager(layoutManager);MusicAdapter musicAdapter = new MusicAdapter(musicList, currentOrder == -1 ? "-1":musicList.get(currentOrder).getId());musicAdapter.setOnItemClickListener(new OnItemClickListener() { //给我们的 item 设置点击事件,代表选中这首歌@Overridepublic void onItemClick(View view, int position) {Music music = musicList.get(position);if (music != null) {Intent intent = new Intent(MusicListActivity.this, MainActivity.class);currentOrder = position; //更新选中的小标,startActivity(intent); // 回到 MainActivity ,}}});recyclerView.setAdapter(musicAdapter);musicHandler = new Handler(new Handler.Callback() {@Overridepublic boolean handleMessage(@NonNull Message msg) {if (msg.what == UPDATE_TEXT){//刷新 recyclermusicAdapter.setCurrentId(musicList.get(currentOrder).getId());recyclerView.setAdapter(null);recyclerView.setAdapter(musicAdapter);}return true;}});btn_back = findViewById(R.id.btn_back);btn_back.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {finish();}});}
}

这里主要有两个部分需要注意。

1、

MusicAdapter musicAdapter = new MusicAdapter(musicList, currentOrder == -1 ? "-1":musicList.get(currentOrder).getId());

我们在这里传了当前正在播放歌曲的 id 。因为我们要对这个做特殊处理。MusicAdapter 做的大部分都是标准的流程化处理

public class MusicAdapter extends RecyclerView.Adapter<MusicAdapter.ViewHolder> {protected List<Music> myMusicList;protected  OnItemClickListener myItemListener;public String currentId;private static final String CHOOSE_COLOR = "#7FE67F";public  void setCurrentId(String id){currentId = id;}public MusicAdapter(List<Music> musicList, String currentId) {myMusicList = musicList;this.currentId = currentId;}public void setOnItemClickListener(OnItemClickListener listener){this.myItemListener = listener;}@NonNull@Overridepublic ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.music_list, parent, false);return new ViewHolder(view,myItemListener);}//在这里@Overridepublic void onBindViewHolder(@NonNull ViewHolder holder, int position) {Log.d("TestRecycler","会执行几次呢");Music music = myMusicList.get(position);holder.musicName.setText(music.getName());holder.musicAuthor.setText(music.getAuthor());//检测是否是正在播放的歌曲//对于正在播放的歌曲要加绿处理。if(currentId.equalsIgnoreCase(music.getId())){Log.d("TestRecycler","匹配成功--"+music.getName());holder.chooseFlag.setText("正在播放");holder.musicName.setTextColor(Color.parseColor(CHOOSE_COLOR));holder.musicAuthor.setTextColor(Color.parseColor(CHOOSE_COLOR));holder.point.setTextColor(Color.parseColor(CHOOSE_COLOR));holder.chooseFlag.setTextColor(Color.parseColor(CHOOSE_COLOR));}}@Overridepublic int getItemCount() {return myMusicList.size();}class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {TextView musicName;TextView musicAuthor;TextView point;TextView chooseFlag;public ViewHolder(View view, OnItemClickListener onItemClickListener) {super(view);myItemListener = onItemClickListener;view.setOnClickListener(this);musicName = view.findViewById(R.id.tv_list_name);musicAuthor = view.findViewById(R.id.tv_list_author);point = view.findViewById(R.id.point);chooseFlag  = view.findViewById(R.id.tv_choose);}@Overridepublic void onClick(View v) {myItemListener.onItemClick(v,getPosition());}}
}

2、

musicHandler = new Handler(new Handler.Callback() {@Overridepublic boolean handleMessage(@NonNull Message msg) {if (msg.what == UPDATE_TEXT){//刷新 recyclermusicAdapter.setCurrentId(musicList.get(currentOrder).getId());recyclerView.setAdapter(null);recyclerView.setAdapter(musicAdapter);}return true;}
});

不知道还记不记得,前面有个地方发了一个消息。当歌曲播放完成后,如果我们正处于 MusicListActivity界面。会发送一条消息。然后 MusicListActivity就会接受这条消息,然后刷新当前页面(主要就是为了更新 绿色的正在播放)。这里我先是用了notifyItemRangeChanged()去测试,但是发现如果一直待在这个界面,有绿色状态的会变的不唯一,也是DeBug很久,没解决,就用了这种 重置适配器 的暴力方法(大数据时不可取)。如果有别的方法,还请多多指教。

private void handleEnd() {//歌曲放完了,相当于触发一次下一首Flag = 0;//先暂停这一首,然后执行下一首btn_start.setImageResource(R.drawable.start);ToastUtil.toast(MainActivity.this, "即将播放下一首");//延迟2.5s,播放下一首new Handler().postDelayed(new Runnable() {@Overridepublic void run() {btn_next.performClick();Log.d("TestRecycler", "发送消息");//如果此时是在歌曲列表界面,发个消息if (MusicListActivity.musicHandler != null) {Message message = new Message();message.what = MusicListActivity.UPDATE_TEXT;MusicListActivity.musicHandler.sendMessage(message);}}}, 2500);
}

这个Activity功能较少。让我们继续回到MainActivity

8、onResume()

@Override
protected void onResume() {super.onResume();Intent intent = getIntent();//这个判断是为了区别时初始化还是从 MusicListActivity 返回来的。if (intent != null && currentOrder != -1) {//从歌曲列表返回来时,更新正在播放的音频对象currentMusic = musicList.get(currentOrder);//这个更新不会影响到播放,因为播放是 mediaPlayer 控制的//如果我们点击的是正在播放的歌曲,那么我们就不会进行任何操作//如果歌曲不一样,就会进行更新if (currentMusic != null && !CURRENT_ID.equalsIgnoreCase(currentMusic.getId())) {initMusicMessage();//更新展示界面btn_start.performClick(); //这个意思是 触发一次 btn_start的点击事件。后面再讲,这里主要是理清是否需要切歌的逻辑。}}
}
private void initMusicMessage() { //更新展示界面currentMusic = musicList.get(currentOrder);seekBar.setMax((int) currentMusic.getTime());seekBar.setProgress(0);tv_music_name.setText(currentMusic.getName());tv_music_author.setText(currentMusic.getAuthor());tv_all_time.setText(format(currentMusic.getTime()));tv_now_time.setText(R.string.default_music_time);
}

9、点击事件处理

坚持住,就要结束了!

btn_list : 点击后跳转到 歌曲列表。

case R.id.btn_list: //展示歌曲列表
if (IS_PERMISSION) {Intent intent = new Intent(this, MusicListActivity.class);startActivity(intent);
} else {ToastUtil.toast(MainActivity.this, "请先前往授权");
}
break;

btn_start: 情况最多的点击

case R.id.btn_start:
/**三种情况会触发。* 1、刚进入界面,还没有选择任何歌曲* 2、歌曲播放中,点击按钮* 3、选歌界面返回后,触发
*///1、刚进入界面,没有选择任何歌曲
if (currentOrder == -1) {startFirstMusic();//选中第一首歌进行播放break;
}
//如果二者不相等,说明发生了切歌
//什么时候不相等?还记的 onResume() 触发了一次点击事件不,就在这里
if (!CURRENT_ID.equalsIgnoreCase(currentMusic.getId())) { //在歌曲列表选择了不同的歌曲if (Flag == 0) { //如果是暂停装填,则修改一下图标btn_start.setImageResource(R.drawable.pause);}CURRENT_ID = currentMusic.getId();initMusicMessage();//初始化歌曲信息musicBinder.play(currentMusic.getUrl());//播放
} else {//相等就是单纯的暂停与播放if (Flag == 1) { //处于播放状态,点击后暂停btn_start.setImageResource(R.drawable.start);musicBinder.pausePlay();} else {btn_start.setImageResource(R.drawable.pause);//这个地方要判断下 是还没有播放,还是继续播放// play()是会从头开始重新播放的,所以不能乱用if (seekBar.getProgress() == 0) {musicBinder.play(currentMusic.getUrl());} else {musicBinder.continuePlay();}}Flag = Flag == 1 ? 0 : 1;
}
break;

btn_nextbtn_last 二者差不多

case R.id.btn_last:nextAndLast(false);break;
case R.id.btn_next:nextAndLast(true);break;
private void nextAndLast(Boolean nextFlag) {if (currentOrder == -1) { //与开始按钮一样,最开始的时候,点击三个中的任意一个,都会选中第一首歌进行播放startFirstMusic();return;}if (Flag == 0) { //如果此时处于暂停状态Flag = 1;  //更新状态btn_start.setImageResource(R.drawable.pause); // 更新下图标}if (nextFlag) {musicBinder.nextPlay(); //执行下一首} else {musicBinder.lastPlay(); //执行上一首}initMusicMessage(); //更新界面CURRENT_ID = currentMusic.getId(); //跟新 CURRNET_ID 的值,供后续使用
}

还有最后一个函数

private void startFirstMusic() {if (!IS_PERMISSION) { //如果没有授权,点击任何一个按钮,都会弹出提示,然后什么也不干ToastUtil.toast(MainActivity.this, "请先前往授权");return;}if (BaseActivity.musicList.isEmpty()) { //授权了,但是没有歌曲,也是弹出提示,然后啥也不干ToastUtil.toast(MainActivity.this, "暂无曲目");return;}//有歌曲就播放第一首currentOrder = 0;currentMusic = musicList.get(currentOrder);CURRENT_ID = currentMusic.getId();initMusicMessage();btn_start.setImageResource(R.drawable.pause);Flag = 1;musicBinder.play(musicList.get(currentOrder).getUrl());oprSeekBar(true)//设置我们的进度条可以进行点击、滑动。
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/408096.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Leetcode876. 链表的中间结点(双指针)

题目描述 给你单链表的头结点 head &#xff0c;请你找出并返回链表的中间结点。 如果有两个中间结点&#xff0c;则返回第二个中间结点。 示例&#xff1a; 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[3,4,5] 解释&#xff1a;链表只有一个中…

【蓝桥杯冲刺省一,省一看这些就够了-C++版本】蓝桥杯C++STL及相关练习题

蓝桥杯历年省赛真题 点击链接免费加入题单 STL map及其函数 map<key,value> 提供一对一的数据处理能力&#xff0c;由于这个特性&#xff0c;它完成有可能在我们处理一对一数据的时候&#xff0c;在编程上提供快速通道。map 中的第一个值称为关键字(key)&#xff0c;…

python 多进程 多线程 程序

这个纯粹为了增加理解&#xff0c;将很多比较好的资料进行归纳总结。 1、理论汇总 并发和并行 image.png 多进程和多线程 同步和异步 同步&#xff1a;所谓同步&#xff0c;就是在发出一个功能调用时&#xff0c;在没有得到结果之前&#xff0c;该调用就不会返回。 异步…

C语言刷题日记(附详解)(2)

一、有理数加法 输入格式&#xff1a; 输入在一行中按照a1/b1 a2/b2的格式给出两个分数形式的有理数&#xff0c;其中分子和分母全是整形范围内的正整数。 输出格式&#xff1a; 在一行中按照a/b的格式输出两个有理数的和。注意必须是该有理数的最简分数形式&#xff0c;若…

​14:00面试,14:06就出来了,问的问题有点变态。。。

从小厂出来&#xff0c;没想到在另一家公司又寄了。 到这家公司开始上班&#xff0c;加班是每天必不可少的&#xff0c;看在钱给的比较多的份上&#xff0c;就不太计较了。没想到5月一纸通知&#xff0c;所有人不准加班&#xff0c;加班费不仅没有了&#xff0c;薪资还要降40%…

Linux系统下的容器安全:深入解析与最佳实践

在云计算和微服务架构的推动下&#xff0c;容器技术因其高效、可移植和灵活的特点&#xff0c;已经成为现代软件开发和部署的首选方案。然而&#xff0c;容器的广泛应用也带来了新的安全挑战&#xff0c;尤其是在Linux系统下&#xff0c;容器安全的实现和维护变得尤为重要。本文…

如何使用python脚本爬取微信公众号文章?

1、什么是爬虫&#xff1f; 在座的各位可能经常听到一个词&#xff0c;叫“爬虫”&#xff0c;这是一种能够悄无声息地将网站数据下载至本地设备的程序。利用爬虫&#xff0c;您无需亲自访问特定网站&#xff0c;逐个点击并手动下载所需数据。相反&#xff0c;爬虫能够全自动地…

STM32——PWM波形输出

一、IC和OC 可以看到&#xff1a;定时器除了基本的定时中断功能&#xff0c;输入捕获、输出比较均是STM32定时器的功能 输入捕获IC&#xff08;Input Capture&#xff09; 输入捕获是一种用于测量外部信号脉冲宽度或频率的技术。它通过定时器模块捕获外部信号的特定事件&…

2024年AI编程新手必备工具,快速提升技能!

在当今这个技术日新月异的时代&#xff0c;AI编程已成为一个越来越重要的领域&#xff0c;吸引着众多新手和希望提升自己的中级开发者进入。 对于这些渴望在AI领域快速成长的人来说&#xff0c;选择合适的编程工具是至关重要的。 接下来&#xff0c;我们将深入探讨几款市场上…

Aria2安装和使用-Mac版

起因是需要网盘下载&#xff0c;无奈限速很烦&#xff0c;查找很多方案后&#xff0c;最终决定使用Aria2 Tampermonkey。 其中Aria2是一款开源轻量的下载软件&#xff0c;简单来说就是可以通过URL直接下载。 Tampermonkey则是一款插件&#xff0c;我这里是.crx结尾的谷歌插件…

抢单源码修正版,带教程,自动抓取订单,十几种语言可自动切换

亚马逊抢单源码自动抓取订单任务邀请英文,西班牙语可自动切换语言亲测修正版。带完整开源的前后台。 西班牙,英文&#xff0c;巴西&#xff0c;中文&#xff0c;德国&#xff0c;拉法兰西&#xff0c;荷兰&#xff0c;缅甸&#xff0c;Sverige&#xff0c;日本&#xff0c;Trk…

专利权和版权有什么区别?

专利权和版权有什么区别&#xff1f;

SD差点挂掉,后备军们兴奋入场,AI生图应用正在爆发?

前后不到一个月&#xff0c;两个开源生图模型相继上线。 首先是由称得上 SD 原班人马的黑森林实验室推出的 FLUX.1。黑森林实验室由 Stable Diffusion 的核心开发者 Robin Rombach 领衔创立&#xff0c;团队成员基本上都是 Stable Diffusion 3 的作者&#xff0c;其中三名元老…

内存管理篇-04伙伴系统

本小节有几个重要的知识点&#xff1a; 伙伴系统的思想伙伴系统的实现伙伴系统分配器&#xff1a;内存块的申请、释放过程伙伴算法和阶数 1.伙伴系统的思想 针对某个某个zone分区&#xff0c;&#xff08;1&#xff09;把物理地址相连的空闲页连接起来合成一个物理块&#xf…

数据结构——冒泡、选择、插入和希尔排序

目录 引言 冒泡排序 1.算法思想 2.算法步骤 3.代码实现 4.复杂度分析 选择排序 1.算法思想 2.算法步骤 3.代码实现 (1)优化前 (2)优化后 4.复杂度分析 插入排序 1.算法思想 2.算法步骤 3.代码实现 4.复杂度分析 希尔排序 1.算法思想 2.算法步骤 3.代码实…

tcp 网络通信及抓包工具的使用

tcp网络通信 本地回环&#xff08;Loopback&#xff09;的概念 本地回环地址是一个特殊的IP地址&#xff0c;用于指向计算机本身的网络接口。在IPv4中&#xff0c;最常见的本地回环地址是127.0.0.1&#xff0c;而在IPv6中则是::1。这个地址用于测试网络软件&#xff0c;确保网…

量化交易backtrader实践(四)_评价统计篇(1)_内置评价

背景 通过对基础的学习和不断深入的实践&#xff0c;当我们已经能够制作出快速获取数据&#xff0c;以及制作出多个股票 乘上多种策略进行回测的部分的时候&#xff0c;我们就会明显发现数据有点多了&#xff0c;比如10支股票都用了3种策略就得到30段数据&#xff0c;一页显示…

亲测好用,ChatGPT 3.5/4.0新手使用手册,最全论文指令手册~ 【2024年 更新】

本以为遥遥领先的GPT早就普及了&#xff0c;但小伙伴寻找使用的热度一直高居不下&#xff0c;其实现在很简单了&#xff01; 国产大模型快200家了&#xff0c;还有很多成熟的国内AI产品&#xff0c;跟官网一样使用&#xff0c;还更加好用~ ① 3.5 大多数场景是够用的&#xff…

Mix|使用VS2017CMake构建Qt工程 仿照MVS(仅用于学习)

MVS下载链接&#xff1a;https://www.hikrobotics.com/cn/machinevision/service/download/?module0 CMake工程构建参考&#xff1a;CMake|VS2017CMake3.8搭建Qt项目 文章目录 效果图整体结构实现代码最外层CMakeLists.txt代码实现及CMakeLists.txt搭建CMakeLists.txt搭建主函…

[创业之路-141] :产品经理 - NPDP概述

目录 一、产品经理以及主要职责 1.1 概述 1、市场调研与需求分析 2、产品规划与设计 3、项目管理与协调 4、产品推广与销售支持 5、产品运营与维护 6、其他职责 1.2 产品经理与项目经理的职责分工 1.2.1 职责区别 产品经理 项目经理 1.2.2 合作方式 二、什么是NP…