android 实现简易音乐播放器

音乐App

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

1、简介

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

播放界面如下:

歌曲列表界面如下:

项目结构如下:

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

2、Music: 音频对象

java 复制代码
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主要用来存放一些全局都要访问的东西。

java 复制代码
public class BaseActivity extends AppCompatActivity {

    //用来存放音频对象。
    public static List<Music> musicList = null;
    
    //用来标志 当前播放的是第几首歌, 值代表在 musicList 中的下标。
    public static int currentOrder = -1;
    
    //不多解释,就看成一个解析音频文件的工具即可
    protected MediaMetadataRetriever retriever;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        retriever = new MediaMetadataRetriever();
    }

    @SuppressLint("Range")
    protected void initMusicList() {
        //此处是有代码的,后面再具体讲解
    }
       
}

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

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

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">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:ignore="UselessParent">

        <LinearLayout
            android:id="@+id/title"
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="70sp"
            android:layout_alignParentTop="true"
            >
            <TextView
                android: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"/>
            <ImageButton
                android: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>


        <ImageButton
            android: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"/>

        <LinearLayout
            android: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">
            <TextView
                android: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"/>

            <TextView
                android: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>

        <SeekBar
            android:id="@+id/seekBar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="60sp"
            android:layout_below="@+id/music_message"
            />

        <RelativeLayout
            android:layout_below="@+id/seekBar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <TextView
                android: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"/>

            <TextView
                android: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>



        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="80sp"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="20sp"
            android:orientation="horizontal">

            <ImageButton
                android: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" />

            <ImageButton
                android: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" />

            <ImageButton
                android: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 。代码很长,分模块讲解。

属性:

java 复制代码
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()

java 复制代码
protected void onCreate(Bundle savedInstanceState) {
        ...
        //省略一些属性赋值。
        //获取权限
        requestPermissionByHand();
        //注册广播
        registerBroadCast();
        //绑定服务
        startAndBindService();//启动服务

        //进度条
        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                if (currentMusic == null) {
                    ToastUtil.toast(MainActivity.this, "未播放歌曲");
                }
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                int progress = seekBar.getProgress();
                tv_now_time.setText(format(progress));
                musicBinder.seekTo(progress);
            }
        });

        oprSeekBar(false);//刚开始不允许操作

    }
5.1.1、requestPermissionByHand(): 因为要读取音频文件,第一步肯定要先进行授权。代码就是很标准的权限获取流程。
java 复制代码
  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();
        }
    }
    
    @Override
    public 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自己不知道,必须告诉它一声)。

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

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

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

5.1.2 initMusicList(): 初始化音频数据
java 复制代码
@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 listview
                    Music 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 listview
            Music 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();

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

java 复制代码
private void registerBroadCast() {
    localBroadcastManager = LocalBroadcastManager.getInstance(this);
    MusicReceiver musicReceiver = new MusicReceiver();
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction("com.xhy.musicRunning");
    localBroadcastManager.registerReceiver(musicReceiver, intentFilter);
}
java 复制代码
class MusicReceiver extends BroadcastReceiver {
    @Override
    public 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()
java 复制代码
private void startAndBindService() {
    Intent intent = new Intent(MainActivity.this, MusicService.class);
    musicServiceConnection = new MusicServiceConnection();
    startService(intent);
    bindService(intent, musicServiceConnection, BIND_AUTO_CREATE);
}
java 复制代码
class MusicServiceConnection implements ServiceConnection {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        musicBinder = (MusicService.MusicBinder) service;
        isBind = true;
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {}
}

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

5.1.5、seekBar.setOnSeekBarChangeListener()

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

java 复制代码
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    }

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

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

java 复制代码
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此时应处于不可用状态(因为有监听点击事件,会导致一些错误)。

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

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

6、MusicService

java 复制代码
public class MusicService extends Service {

    //用来控制音乐的播放与暂停。系统自带的
    protected MediaPlayer mediaPlayer;
    
    //定时器
    protected Timer timer;
    
    //广播管理器
    //用的是 MainActivity中的
    public static LocalBroadcastManager localBroadcastManager; 

    public MusicService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mediaPlayer = new MediaPlayer();
        localBroadcastManager = MainActivity.localBroadcastManager;
    }

    private void createTimer() {
        if (timer == null) {
            timer = new Timer();
            TimerTask timerTask = new TimerTask() { //定时任务
                @Override
                public 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进行捕获与响应
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return new MusicBinder();
    }

    //用来绑定服务,这样可以通过Activity 与服务进行交互了
    public class MusicBinder extends Binder {
        public void play(String url){//String path
            Uri 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());
        }
    }

    @Override
    public 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,此时我们回去看一下,广播接收器干了什么。

java 复制代码
class MusicReceiver extends BroadcastReceiver {
    @Override
    public 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();
        }
    }
}
java 复制代码
private void handleEnd() {
    //歌曲放完了,相当于触发一次下一首
    Flag = 0;//先暂停这一首,然后执行下一首
    btn_start.setImageResource(R.drawable.start);
    ToastUtil.toast(MainActivity.this, "即将播放下一首");
    //延迟2.5s,播放下一首
    new Handler().postDelayed(new Runnable() {
        @Override
        public 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 就复杂监听音乐的播放,动态的去更新 界面上时间及进度条的显示。

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

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

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

MusicService就到这里

7、MusicListActivity

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

java 复制代码
public class MusicListActivity extends BaseActivity {
    protected ImageButton btn_back;
    public static Handler musicHandler;
    public static final int UPDATE_TEXT = 1;

    @Override
    protected 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 设置点击事件,代表选中这首歌
            @Override
            public 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() {
            @Override
            public boolean handleMessage(@NonNull Message msg) {
                if (msg.what == UPDATE_TEXT){
                    //刷新 recycler
                    musicAdapter.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() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
    }
}

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

1、

java 复制代码
MusicAdapter musicAdapter = new MusicAdapter(musicList, currentOrder == -1 ? "-1":musicList.get(currentOrder).getId());

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

java 复制代码
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
    @Override
    public 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);
    }

    //在这里
    @Override
    public 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));
        }
    }

    @Override
    public 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);
        }
        
        @Override
        public void onClick(View v) {
            myItemListener.onItemClick(v,getPosition());
        }
    }
}

2、

java 复制代码
musicHandler = new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(@NonNull Message msg) {
        if (msg.what == UPDATE_TEXT){
            //刷新 recycler
            musicAdapter.setCurrentId(musicList.get(currentOrder).getId());
            recyclerView.setAdapter(null);
            recyclerView.setAdapter(musicAdapter);
        }
        return true;
    }
});

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

java 复制代码
private void handleEnd() {
    //歌曲放完了,相当于触发一次下一首
    Flag = 0;//先暂停这一首,然后执行下一首
    btn_start.setImageResource(R.drawable.start);
    ToastUtil.toast(MainActivity.this, "即将播放下一首");
    //延迟2.5s,播放下一首
    new Handler().postDelayed(new Runnable() {
        @Override
        public 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()

java 复制代码
@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的点击事件。后面再讲,这里主要是理清是否需要切歌的逻辑。
        }
    }
}
java 复制代码
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 : 点击后跳转到 歌曲列表。

java 复制代码
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: 情况最多的点击

java 复制代码
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 二者差不多

java 复制代码
case R.id.btn_last:
                nextAndLast(false);
                break;
case R.id.btn_next:
                nextAndLast(true);
                break;
java 复制代码
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 的值,供后续使用
}

还有最后一个函数

java 复制代码
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)//设置我们的进度条可以进行点击、滑动。
}
相关推荐
黄林晴3 小时前
如何判断手机是否是纯血鸿蒙系统
android
火柴就是我3 小时前
flutter 之真手势冲突处理
android·flutter
法的空间4 小时前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
循环不息优化不止4 小时前
深入解析安卓 Handle 机制
android
恋猫de小郭4 小时前
Android 将强制应用使用主题图标,你怎么看?
android·前端·flutter
jctech4 小时前
这才是2025年的插件化!ComboLite 2.0:为Compose开发者带来极致“爽”感
android·开源
用户2018792831674 小时前
为何Handler的postDelayed不适合精准定时任务?
android
叽哥4 小时前
Kotlin学习第 8 课:Kotlin 进阶特性:简化代码与提升效率
android·java·kotlin
Cui晨4 小时前
Android RecyclerView展示List<View> Adapter的数据源使用View
android
氦客4 小时前
Android Doze低电耗休眠模式 与 WorkManager
android·suspend·休眠模式·workmanager·doze·低功耗模式·state_doze