Android -- 简易音乐播放器

Android -- 简易音乐播放器

复制代码
 播放器功能:
 * 1. 播放模式:单曲、列表循环、列表随机;
 * 2. 后台播放(单例模式);
 * 3. 多位置同步状态回调;
 
 处理模块:
 * 1. 提取文件信息:音频文件(.mp3) -> 对象类(AudioBean);
 * 2. 后台播放管理:VMPlayer(实现对音频的播放相关处理);
 * 3. UI显示及控制:歌曲列表 + 播放控制器;

效果:

模块一:处理音频文件(后台服务内)
java 复制代码
/**
* 同步指定文件夹下音频文件
* * @param autoPlay 是否自动播放
*/
private void flashAudioRes(boolean autoPlay) {
        Log.d(TAG, "同步音频中...");
        new Thread(() -> {
            try {
                List<AudioBean> audioItems = synLocalMusic2(FileUtils.getAudioDir());
                if (audioItems != null && !audioItems.isEmpty()) {
                    //排序
                    Collections.sort(audioItems, (o1, o2) -> o1.getDisplayName().compareTo(o2.getDisplayName()));

                    VMPlayer.getInstance().setPlayList(audioItems);
                    if(autoPlay){
                        Thread.sleep(1000);
                        VMPlayer.getInstance().resetIndex();
                        VMPlayer.getInstance().play();
                        VMPlayer.getInstance().notifyListChanged();
                    }
                } else {
                    //closeDialogSyn("本地无有效音频文件!", 3000);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }

    /**
     * 同步指定文件夹下音频文件:仅一层
     * @param dir 文件夹
     */
    private List<AudioBean> synLocalMusic2(String dir) {
        File root = new File(dir);
        if (root.exists()) {
            File[] files = root.listFiles();
            if (files == null || files.length < 1) {
                return null;
            }

            List<AudioBean> list = new ArrayList<>();
            MediaPlayer mediaPlayer = new MediaPlayer();
            int duration = 0;
            for (File f : files) {
                //筛选目标文件
                if (f.isFile() && f.getName().endsWith(".mp3")) {
                    try {
                        mediaPlayer.reset();
                        mediaPlayer.setDataSource(f.getPath());
                        mediaPlayer.prepare();
                        duration = mediaPlayer.getDuration();
                    } catch (IOException var5) {
                        var5.printStackTrace();
                    }
                    Log.v(TAG, "synLocalMusic: " + f.getName() + " - " + duration);
                    AudioBean bean = getAudioFileInfo(f.getPath(), f.length(), duration);
                    list.add(bean);
                }
            }

            if (mediaPlayer != null) {
                mediaPlayer.reset();
                mediaPlayer.release();
            }

            return list;
        }
        return null;
    }

    /**
     * 文件绝对路径,校验放在外面
     * 文件名格式:歌手 - 歌名.mp3
     */
    private AudioBean getAudioFileInfo(String path, long size, int duration) {
        AudioBean songsInfo = new AudioBean();
        //xxx/Music/歌手 - 歌名.mp3
        //filename
        String displayName = path.substring(path.lastIndexOf("/") + 1);//歌手 - 歌名.mp3
        String album = displayName.substring(0, displayName.lastIndexOf("."));//歌手 - 歌名

        String name;
        String artist;
        if (album.contains("-")) {
            artist = album.substring(0, album.lastIndexOf("-")).trim();//歌手
            name = album.substring(album.lastIndexOf("-") + 1).trim();//歌名
        } else {
            artist = name = album;
        }

        songsInfo.setName(name);
        songsInfo.setDisplayName(displayName);
        songsInfo.setArtist(artist);
        songsInfo.setDuration(duration);
        songsInfo.setSize(size);
        songsInfo.setPath(path);

        return songsInfo;
    }
java 复制代码
/**
 * Created by Administrator on 2024/11/24.
 * Usage: 简单自定义音频文件bean类
 */

public class AudioBean implements Serializable {
    private String name;//歌名
    private String displayName;//显示名(文件名去后缀)
    private String artist;//歌手名
    private String path;//文件路径
    private int duration;//时长
    private long size;//文件大小

    public AudioBean() {
    }

    public AudioBean(String path) {
        //
        this.path = path;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDisplayName() {
        return displayName;
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    public String getArtist() {
        return artist;
    }

    public void setArtist(String artist) {
        this.artist = artist;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public int getDuration() {
        return duration;
    }

    public void setDuration(int duration) {
        this.duration = duration;
    }

    public long getSize() {
        return size;
    }

    public void setSize(long size) {
        this.size = size;
    }

    @Override
    public String toString() {
        return "AudioBean{" +
                "name='" + name + '\'' +
                ", displayName='" + displayName + '\'' +
                ", artist='" + artist + '\'' +
                ", path='" + path + '\'' +
                ", duration=" + duration +
                ", size=" + size +
                '}';
    }
}
模块二:播放管理器
VMPlayer.java (主要类)
java 复制代码
import android.annotation.SuppressLint;
import android.content.Context;
import android.media.MediaPlayer;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;

import com.nepalese.harinetest.config.ShareDao;
import com.nepalese.harinetest.utils.MathUtil;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by Administrator on 2024/11/25.
 * Usage: virgo music player
 * 1. 播放模式:单曲、列表循环、列表随机;
 * 2. 后台播放(单例模式);
 * 3. 多位置同步状态回调;
 */
public class VMPlayer implements MediaPlayer.OnCompletionListener, VirgoPlayerCallback {
    private static final String TAG = "VMPlayer";
    private static final long INTERVAL_GET_PROGRESS = 500;//后台获取进度频率

    //播放器状态
    public static final int STATE_ERROR = -1; //错误状态:需要重置列表才能继续使用
    public static final int STATE_INITIAL = 0;//初始化状态
    public static final int STATE_PREPARED = 1;//播放列表/资源已设置
    public static final int STATE_PLAYING = 2;
    public static final int STATE_PAUSE = 3;

    //播放模式
    public static final int MODE_SINGLE = 0;//单曲循环
    public static final int MODE_LOOP = 1;//列表循环
    public static final int MODE_RANDOM = 2;//列表随机

    @SuppressLint("StaticFieldLeak")
    private static volatile VMPlayer instance;//单例
    private Context context;
    private MediaPlayer mediaPlayer;
    private List<AudioBean> beanList;//当前播放列表
    private List<iPlayBack> iPlayBacks;//已注册回调列表
    private AudioBean curBean;//当前在播放的音频

    private int curState;//当前播放状态
    private int curIndex;//当前播放索引
    private int curMode;//当前播放模式
    private int errTime;//播放器连续出错次数
    private int aimSeek;//播放前设置的进度

    public static VMPlayer getInstance() {
        if (instance == null) {
            synchronized (VMPlayer.class) {
                if (instance == null) {
                    instance = new VMPlayer();
                }
            }
        }
        return instance;
    }

    private VMPlayer() {
        beanList = new ArrayList<>();
        iPlayBacks = new ArrayList<>(5);//最多同时存在回调个数

        mediaPlayer = new MediaPlayer();
        mediaPlayer.setLooping(false);
        mediaPlayer.setOnCompletionListener(this);
    }

    public void init(Context context) {
        this.context = context;
        curState = STATE_INITIAL;
        curMode = ShareDao.getAudioMode(context);//记忆播放模式 默认列表循环 1
        curIndex = ShareDao.getAudioIndex(context);//记忆播放位置 0
        errTime = 0;
        aimSeek = 0;
        Log.d(TAG, "init: " + curIndex);
    }

    /**
     * 播放器是否可播放
     */
    private boolean isValid() {
        return curState >= STATE_PREPARED && !beanList.isEmpty();
    }

    public List<AudioBean> getBeanList() {
        return beanList;
    }

    //仅手动导入时调用
    public void resetIndex() {
        this.curIndex = 0;
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        if (curMode == MODE_SINGLE) {
            //单曲循环时自动重复播放
            mediaPlayer.seekTo(0);
            mediaPlayer.start();
        } else {
            notifyComplete();
        }
    }

    public void playOrPause() {
        if (curState == STATE_PLAYING) {
            pause();
        } else {
            play();
        }
    }

    /**
     * 播放|继续播放
     */
    @Override
    public void play() {
        if (isValid()) {
            if (curState == STATE_PAUSE) {
                //继续播放
                curState = STATE_PLAYING;
                mediaPlayer.start();
                notifyStateChanged(true);
            } else if (curState == STATE_PREPARED) {
                prepareAndPlay();
            }
            //正在播放...
        } else {
            Log.d(TAG, "play: " + curState + " - size: " + beanList.size());
            notifyError("未设置播放列表!");
        }
    }

    private void prepareAndPlay() {
        curState = STATE_PREPARED;
        if (curIndex < 0 || curIndex >= beanList.size()) {
            curIndex = 0;
        }

        ShareDao.setAudioIndex(context, curIndex);
        Log.d(TAG, "播放: " + curIndex);
        playResource(beanList.get(curIndex));
    }

    //播放资源
    private void playResource(AudioBean bean) {
        if (bean == null || TextUtils.isEmpty(bean.getPath())) {
            ++errTime;
            notifyStateChanged(false);
            if (errTime >= beanList.size()) {
                //需要重置列表才能继续使用
                curState = STATE_ERROR;
                notifyError("播放列表异常!");
            } else {
                //播放下一首
                curState = STATE_PREPARED;
                playNext();
            }
            return;
        }

        try {
            mediaPlayer.reset();
            mediaPlayer.setDataSource(bean.getPath());//本地文件、在线链接
            mediaPlayer.setOnPreparedListener(mp -> {
                notifySongChanged(bean);
                notifyStateChanged(true);
                curState = STATE_PLAYING;
                mediaPlayer.seekTo(aimSeek);
                mediaPlayer.start();
                errTime = 0;
                aimSeek = 0;
                curBean = bean;
            });
            mediaPlayer.prepareAsync();
            startTask();
        } catch (IOException e) {
            ++errTime;
            if (errTime >= beanList.size()) {
                //需要重置列表才能继续使用
                curState = STATE_ERROR;
            } else {
                //重置状态
                if (beanList.size() > 0) {
                    curState = STATE_PREPARED;
                } else {
                    curState = STATE_INITIAL;
                }
            }
            notifyStateChanged(false);
            notifyError("播放器出错!" + e.getMessage());
        }
    }

    /**
     * 播放当前列表指定位置
     *
     * @param index index
     */
    @Override
    public void play(int index) {
        if (isValid()) {
            curIndex = index;
            prepareAndPlay();
        } else {
            notifyError("未设置播放列表!");
        }
    }

    /**
     * 临时播放某个音频文件
     *
     * @param bean AudioBean
     */
    @Override
    public void play(AudioBean bean) {
        if (bean == null) {
            notifyError("指定歌曲为空!");
            return;
        }

        curState = STATE_PREPARED;
        playResource(bean);
    }

    /**
     * 更换播放列表
     *
     * @param list  新列表
     * @param index 开始位置,默认:0
     */
    @Override
    public void play(List<AudioBean> list, int index) {
        if (list == null || list.isEmpty()) {
            notifyError("新列表为空!");
            return;
        }

        curIndex = index;
        setPlayList(list);
        prepareAndPlay();
    }

    /**
     * 上一首
     */
    @Override
    public void playLast() {
        if (isValid()) {
            switch (curMode) {
                case MODE_SINGLE:
                    break;
                case MODE_LOOP:
                    if (curIndex > 0) {
                        --curIndex;
                    } else {
                        curIndex = beanList.size() - 1;
                    }
                    prepareAndPlay();
                    break;
                case MODE_RANDOM:
                    curIndex = MathUtil.getRandom(0, beanList.size(), curIndex);
                    prepareAndPlay();
                    break;
            }
        } else {
            notifyError("未设置播放列表!");
        }
    }

    /**
     * 下一首
     */
    @Override
    public void playNext() {
        if (isValid()) {
            switch (curMode) {
                case MODE_SINGLE:
                    break;
                case MODE_LOOP:
                    ++curIndex;
                    prepareAndPlay();
                    break;
                case MODE_RANDOM:
                    curIndex = MathUtil.getRandom(0, beanList.size(), curIndex);
                    prepareAndPlay();
                    break;
            }
        } else {
            notifyError("未设置播放列表!");
        }
    }

    /**
     * 暂停播放
     */
    @Override
    public void pause() {
        if (isPlaying()) {
            curState = STATE_PAUSE;
            mediaPlayer.pause();
            notifyStateChanged(false);
        }
    }

    /**
     * 跳转播放进度
     *
     * @param progress p
     */
    @Override
    public void seekTo(int progress) {
        if (isValid()) {
            if (curState > STATE_PREPARED) {
                aimSeek = 0;
                mediaPlayer.seekTo(progress);
            } else {
                aimSeek = progress;
            }
        }
    }

    /**
     * 设置播放列表
     *
     * @param beans b
     */
    @Override
    public void setPlayList(List<AudioBean> beans) {
        if (beans == null || beans.isEmpty()) {
            notifyError("新列表为空!");
            return;
        }
        Log.d(TAG, "setPlayList: " + beans.size());

        curState = STATE_PREPARED;
        beanList.clear();
        beanList.addAll(beans);
        curBean = beanList.get(curIndex);
    }

    /**
     * 设置播放模式,外部校验
     *
     * @param mode m
     */
    @Override
    public void setPlayMode(int mode) {
        if (mode == this.curMode) {
            return;
        }
        this.curMode = mode;
        ShareDao.setAudioMode(context, mode);
        Log.d(TAG, "setPlayMode: " + curMode);
    }

    /**
     * 是否正在播放
     */
    @Override
    public boolean isPlaying() {
        return isValid() && mediaPlayer.isPlaying();
    }

    /**
     * 当前播放进度
     */
    @Override
    public int getCurProgress() {
        return mediaPlayer.getCurrentPosition();
    }

    /**
     * 当前播放器状态
     */
    @Override
    public int getCurState() {
        return curState;
    }

    @Override
    public int getCurMode() {
        return curMode;
    }

    /**
     * 获取当前播放音频信息
     * 可空
     */
    @Override
    public AudioBean getCurMusic() {
        if (isValid()) {
            return curBean;
        }
        return null;
    }

    /**
     * 注销播放器
     */
    @Override
    public void releasePlayer() {
        stopTask();
        if (iPlayBacks != null) {
            iPlayBacks.clear();
            iPlayBacks = null;
        }
        if (beanList != null) {
            beanList.clear();
            beanList = null;
        }
        try {
            if (mediaPlayer != null) {
                //stop 可能会有异常
                if (mediaPlayer.isPlaying()) {
                    mediaPlayer.stop();
                }

                mediaPlayer.reset();
                mediaPlayer.release();
                mediaPlayer = null;
            }
        } catch (Exception e) {
            //
        } finally {
            if (mediaPlayer != null) {
                mediaPlayer.reset();
                mediaPlayer.release();
                mediaPlayer = null;
            }
        }
        instance = null;
        curState = STATE_INITIAL;
    }

    /**
     * 注册播放器回调
     */
    @Override
    public void registerCallback(iPlayBack callback) {
        iPlayBacks.add(callback);
    }

    /**
     * 注销播放器回调
     */
    @Override
    public void unregisterCallback(iPlayBack callback) {
        iPlayBacks.remove(callback);
    }

    @Override
    public void removeCallbacks() {
        iPlayBacks.clear();
    }

    public void notifyListChanged() {
        if (iPlayBacks != null) {
            for (iPlayBack callBack : iPlayBacks) {
                callBack.onListChange();
            }
        }
    }

    private void notifySongChanged(AudioBean bean) {
        if (iPlayBacks != null) {
            for (iPlayBack callBack : iPlayBacks) {
                callBack.onChangeSong(bean);
            }
        }
    }

    private void notifyStateChanged(boolean isPlaying) {
        if (iPlayBacks != null) {
            for (iPlayBack callback : iPlayBacks) {
                callback.onPlayStateChanged(isPlaying);
            }
        }
    }

    private void notifyComplete() {
        if (iPlayBacks != null) {
            for (iPlayBack callback : iPlayBacks) {
                callback.onPlayCompleted();
            }
        }
    }

    private void notifyProcessChanged(int process) {
        if (iPlayBacks != null) {
            for (iPlayBack callback : iPlayBacks) {
                callback.onProcessChanged(process);
            }
        }
    }

    private void notifyError(String msg) {
        if (iPlayBacks != null) {
            for (iPlayBack callback : iPlayBacks) {
                callback.onPlayError(curState, msg);
            }
        }
    }

    
    private final Handler handler = new Handler(msg -> false);

    private final Runnable getProcessTask = new Runnable() {
        @Override
        public void run() {
            handler.postDelayed(getProcessTask, INTERVAL_GET_PROGRESS);
            try {
                if (isPlaying()) {
                    notifyProcessChanged(getCurProgress());
                }
            } catch (Throwable ignored) {
            }
        }
    };

    private void startTask() {
        stopTask();
        handler.post(getProcessTask);
    }

    private void stopTask() {
        handler.removeCallbacks(getProcessTask);
    }

}
VirgoPlayerCallback.java (功能接口)
java 复制代码
/**
 * Created by Administrator on 2024/11/24.
 * Usage: 音乐播放器公开接口
 */
public interface VirgoPlayerCallback {
    //播放|继续播放
    void play();

    //播放当前列表指定位置
    void play(int index);

    //临时播放某个音频文件
    void play(AudioBean bean);

    //更换播放列表
    void play(List<AudioBean> beanList, int index);

    //上一首
    void playLast();

    //下一首
    void playNext();

    //暂停播放
    void pause();

    //跳转播放进度
    void seekTo(int progress);

    //设置播放列表
    void setPlayList(List<AudioBean> beans);

    //设置播放模式
    void setPlayMode(int mode);

    //是否正在播放
    boolean isPlaying();

    //当前播放进度
    int getCurProgress();

    //当前播放器状态
    int getCurState();

    //当前播放模式
    int getCurMode();

    //获取当前播放音频信息
    AudioBean getCurMusic();

    //注销播放器
    void releasePlayer();

    void registerCallback(iPlayBack callback);

    void unregisterCallback(iPlayBack callback);

    void removeCallbacks();
}
iPlayBack.java(播放状态回调接口)
java 复制代码
public interface iPlayBack {
    //歌单变化
    void onListChange();

    void onChangeSong(@NonNull AudioBean bean);

    //播放结束时调用
    void onPlayCompleted();

    //播放状态变化时调用:播放|暂停
    void onPlayStateChanged(boolean isPlaying);

    //播放进度变化时调用
    void onProcessChanged(int process);

    //播放出错时调用
    void onPlayError(int state, String error);
}
模块三:播放控件+歌曲列表
VirgoSimplePlayer.java (简单音乐播放器控件)
java 复制代码
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;

import com.nepalese.harinetest.R;
import com.nepalese.harinetest.utils.ConvertUtil;

/**
 * Created by Administrator on 2024/11/24.
 * Usage: 简单音乐播放器控件
 */

public class VirgoSimplePlayer extends RelativeLayout {
    private static final String TAG = "VirgoSimplePlayer";

    private SeekBar musicSeekbar;
    private TextView musicName, musicCur, musicAll;
    private ImageButton musicLast, musicPlay, musicNext, musicMode;
    private VMPlayer vmPlayer;

    public VirgoSimplePlayer(Context context) {
        this(context, null);
    }

    public VirgoSimplePlayer(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public VirgoSimplePlayer(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        LayoutInflater.from(context).inflate(R.layout.layout_simple_virgo_player, this, true);
        init();
    }

    private void init() {
        initUI();
        initData();
        setListener();
    }

    private void initUI() {
        musicSeekbar = findViewById(R.id.music_seekbar);
        musicName = findViewById(R.id.music_tv_name);
        musicCur = findViewById(R.id.music_cur);
        musicAll = findViewById(R.id.music_all);

        musicLast = findViewById(R.id.music_btn_last);
        musicPlay = findViewById(R.id.music_btn_paly);
        musicNext = findViewById(R.id.music_btn_next);
        musicMode = findViewById(R.id.music_btn_mode);

        musicName.setSelected(true);
    }

    private void initData() {
        vmPlayer = VMPlayer.getInstance();
    }

    private void setListener() {
        musicLast.setOnClickListener(v -> vmPlayer.playLast());

        musicNext.setOnClickListener(v -> vmPlayer.playNext());

        musicPlay.setOnClickListener(v -> vmPlayer.playOrPause());

        musicMode.setOnClickListener(v -> changPlayMode());

        musicSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {

            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                //拖动进度条控制播放进度
                vmPlayer.seekTo(seekBar.getProgress());
            }
        });
    }

    private void changPlayMode() {
        int curMode = vmPlayer.getCurMode();
        curMode++;
        if (curMode > VMPlayer.MODE_RANDOM) {
            curMode = VMPlayer.MODE_SINGLE;
        }
        vmPlayer.setPlayMode(curMode);
        updateModeImg(curMode);
    }

    private void updateModeImg(int curMode) {
        switch (curMode) {
            case VMPlayer.MODE_SINGLE:
                musicMode.setImageResource(R.mipmap.icon_single);
                break;
            case VMPlayer.MODE_LOOP:
                musicMode.setImageResource(R.mipmap.icon_order);
                break;
            case VMPlayer.MODE_RANDOM:
                musicMode.setImageResource(R.mipmap.icon_random);
                break;
        }
    }

    public void notifyStateChanged(boolean isPlaying) {
        if (isPlaying) {
            musicPlay.setImageResource(R.mipmap.icon_playing);
        } else {
            musicPlay.setImageResource(R.mipmap.icon_pause);
        }
    }

    public void notifyProcessChanged(int process) {
        musicSeekbar.setProgress(process);
        musicCur.setText(ConvertUtil.formatTime(process));
    }

    public void notifySongChanged(String name, int duration) {
        musicName.setText(name);
        musicSeekbar.setMax(duration);
        musicAll.setText(ConvertUtil.formatTime(duration));
    }

    public void synInfo() {
        //重新进入时,如果在播放,则需同步一下歌曲信息
        if (vmPlayer.isPlaying()) {
            AudioBean bean = vmPlayer.getCurMusic();
            notifySongChanged(bean.getName() + " - " + bean.getArtist(), bean.getDuration());
            notifyStateChanged(true);
        }else{
            //自动播放
            vmPlayer.play();
        }
        //同步播放模式
        updateModeImg(vmPlayer.getCurMode());
    }
}
layout_simple_virgo_player.xml
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:padding="15dp"
    android:orientation="horizontal"
    android:background="@drawable/bg_card_red">

    <ImageView
        android:layout_width="@dimen/player_img_size"
        android:layout_height="@dimen/player_img_size"
        android:src="@mipmap/img_cover_default"
        android:scaleType="centerCrop"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_marginStart="10dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:orientation="vertical">

            <TextView
                android:id="@+id/music_tv_name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="left"
                android:singleLine="true"
                android:ellipsize="marquee"
                android:marqueeRepeatLimit="marquee_forever"
                android:text="歌名"
                android:textSize="@dimen/text_size_14"
                android:textColor="@color/black"
                android:paddingStart="15dp"/>

            <SeekBar
                android:id="@+id/music_seekbar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:progressTint="@color/black"
                android:thumbTint="@color/color_QYH"
                android:layout_marginTop="3dp"
                android:progress="0"/>
        </LinearLayout>

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_marginLeft="5dp"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/music_cur"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="00"
                    android:textColor="@color/white"
                    android:textSize="@dimen/text_size_12"/>

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="/"
                    android:textColor="@color/white"
                    android:textSize="@dimen/text_size_12"/>

                <TextView
                    android:id="@+id/music_all"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="00"
                    android:textColor="@color/white"
                    android:textSize="@dimen/text_size_12"/>
            </LinearLayout>

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:gravity="center"
                android:orientation="horizontal">

                <ImageButton
                    android:id="@+id/music_btn_last"
                    android:layout_width="@dimen/player_icon_size_small"
                    android:layout_height="@dimen/player_icon_size_small"
                    android:layout_margin="@dimen/player_icon_margin"
                    android:background="@drawable/img_button_transprant"
                    android:src="@mipmap/icon_last"
                    android:scaleType="centerCrop"/>

                <ImageButton
                    android:id="@+id/music_btn_paly"
                    android:layout_width="@dimen/player_icon_size_big"
                    android:layout_height="@dimen/player_icon_size_big"
                    android:background="@drawable/img_button_transprant"
                    android:src="@mipmap/icon_pause"
                    android:scaleType="centerCrop"/>

                <ImageButton
                    android:id="@+id/music_btn_next"
                    android:layout_width="@dimen/player_icon_size_small"
                    android:layout_height="@dimen/player_icon_size_small"
                    android:layout_margin="@dimen/player_icon_margin"
                    android:background="@drawable/img_button_transprant"
                    android:src="@mipmap/icon_next"
                    android:scaleType="centerCrop"/>
            </LinearLayout>

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_alignParentEnd="true">

                <ImageButton
                    android:id="@+id/music_btn_mode"
                    android:layout_width="@dimen/player_icon_size_small"
                    android:layout_height="@dimen/player_icon_size_small"
                    android:layout_margin="@dimen/player_icon_margin"
                    android:background="@drawable/img_button_transprant"
                    android:src="@mipmap/icon_order"
                    android:scaleType="centerCrop"/>
            </LinearLayout>
        </RelativeLayout>
    </LinearLayout>
</LinearLayout>
xml 复制代码
<!--============dimens.xml=============-->
<dimen name="player_icon_size_big">42dp</dimen>
<dimen name="player_icon_size_small">30dp</dimen>
<dimen name="player_icon_margin">10dp</dimen>
<dimen name="player_layout_padding">10dp</dimen>
<dimen name="player_img_size">85dp</dimen>

<!--text size sp-->
<dimen name="text_size_10">10sp</dimen>
<dimen name="text_size_12">12sp</dimen>
<dimen name="text_size_14">14sp</dimen>
<dimen name="text_size_16">16sp</dimen>
<dimen name="text_size_18">18sp</dimen>
<dimen name="text_size_20">20sp</dimen>
<dimen name="text_size_22">22sp</dimen>
<dimen name="text_size_24">24sp</dimen>
<dimen name="text_size_32">32sp</dimen>
<dimen name="text_size_50">50sp</dimen>

<dimen name="padding_1">1dp</dimen>
<dimen name="padding_2">2dp</dimen>
<dimen name="padding_3">3dp</dimen>
<dimen name="padding_5">5dp</dimen>
<dimen name="padding_10">10dp</dimen>
<dimen name="padding_15">15dp</dimen>

<dimen name="margin_1">1dp</dimen>
<dimen name="margin_3">3dp</dimen>
<dimen name="margin_5">5dp</dimen>
<dimen name="margin_10">10dp</dimen>
<dimen name="margin_15">15dp</dimen>
ListView_LocalSong_Adapter.java(自定义列表适配器)
java 复制代码
import android.content.Context;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.nepalese.harinetest.R;

import java.util.List;

/**
 * @author nepalese on 2024/11/24
 * @usage
 */
public class ListView_LocalSong_Adapter extends BaseAdapter {
    private Context context;
    private LayoutInflater inflater;
    private List<AudioBean> data;
    private interListenerSongList listener;

    public ListView_LocalSong_Adapter(Context context, interListenerSongList listener, List<AudioBean> list) {
        this.context = context;
        this.inflater = LayoutInflater.from(context);
        this.listener = listener;
        this.data = list;
    }

    @Override
    public int getCount() {
        return data == null ? 0 : data.size();
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    static class Holder {
        private TextView tvOrder, tvName, tvArtist;
        private LinearLayout root;
        private ImageButton ibList;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Holder holder;
        if (convertView == null) {
            holder = new Holder();
            convertView = inflater.inflate(R.layout.layout_song_list_local, null);

            holder.root = convertView.findViewById(R.id.layout_list_root);
            holder.tvOrder = convertView.findViewById(R.id.tv_order);
            holder.tvName = convertView.findViewById(R.id.tvLocalName);
            holder.tvArtist = convertView.findViewById(R.id.tvLocalArtist);
            holder.ibList = convertView.findViewById(R.id.ibLocalSongList);
            convertView.setTag(holder);
        } else {
            holder = (Holder) convertView.getTag();
        }

        holder.tvOrder.setText(String.valueOf(position + 1));
        holder.tvName.setText(data.get(position).getName());
        holder.tvArtist.setText(data.get(position).getArtist());
        if (position % 2 == 0) {
            holder.root.setBackgroundColor(Color.parseColor("#4D03A9F4"));
        }else{
            holder.root.setBackgroundColor(Color.TRANSPARENT);
        }

        if (VMPlayer.getInstance().getCurState() >= VMPlayer.STATE_PREPARED && VMPlayer.getInstance().getCurMusic().getDisplayName().equals(data.get(position).getDisplayName())) {
            holder.tvName.setTextColor(Color.RED);
        } else {
            holder.tvName.setTextColor(Color.BLACK);
        }

        //内部项点击监听
//        holder.ibList.setOnClickListener(v -> {
//            listener.onItemClick(v);
//        });
        holder.ibList.setTag(position);

        return convertView;
    }

    public interface interListenerSongList {
        void onItemClick(View view);
    }
}
layout_song_list_local.xml
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/layout_list_root"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="@dimen/padding_5"
    android:gravity="center_vertical"
    android:descendantFocusability="blocksDescendants"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/tv_order"
        android:layout_margin="@dimen/margin_5"
        android:layout_width="35dp"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:textSize="@dimen/text_size_18"
        android:textColor="@color/black" />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tvLocalName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:singleLine="true"
            android:ellipsize="end"
            android:layout_marginBottom="@dimen/margin_3"
            android:textColor="@color/black"
            android:textSize="@dimen/text_size_18"/>

        <TextView
            android:id="@+id/tvLocalArtist"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:singleLine="true"
            android:ellipsize="end"
            android:textColor="@color/gray"
            android:textSize="@dimen/text_size_14"/>
    </LinearLayout>

    <ImageButton
        android:id="@+id/ibLocalSongList"
        android:layout_width="@dimen/icon_30"
        android:layout_height="@dimen/icon_30"
        android:src="@mipmap/icon_list_gray"
        android:scaleType="fitCenter"
        android:padding="@dimen/padding_2"
        android:focusable="false"
        android:background="@drawable/selector_button_transparent"/>
</LinearLayout>
前端使用
java 复制代码
public class AudioPlayActivity extends AppCompatActivity implements ListView_LocalSong_Adapter.interListenerSongList, iPlayBack {
    private static final String TAG = "AudioPlayActivity";

    private Context context;
    private VirgoSimplePlayer simplePlayer;
    private ListView listView;
    private ListView_LocalSong_Adapter adapter;
    private final List<AudioBean> audioList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_audio_play);
        context = getApplicationContext();

        init();
    }

    private void init() {
        initUI();
        initData();
        setListener();
    }

    private void initUI() {
        simplePlayer = findViewById(R.id.simplePlayer);
        listView = findViewById(R.id.listviewAudio);
    }

    private void initData() {
        VMPlayer.getInstance().registerCallback(this);
        simplePlayer.synInfo();
        audioList.addAll(VMPlayer.getInstance().getBeanList());
        adapter = new ListView_LocalSong_Adapter(context, this, audioList);
        listView.setAdapter(adapter);
    }

    private void setListener() {
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                VMPlayer.getInstance().play(position);
            }
        });
    }

    @Override
    protected void onDestroy() {
        release();
        super.onDestroy();
    }

    private void release() {
        VMPlayer.getInstance().unregisterCallback(this);
    }

    @Override
    public void onItemClick(View view) {
        //
    }

    @Override
    public void onListChange() {
        audioList.clear();
        audioList.addAll(VMPlayer.getInstance().getBeanList());
        updateListView();
    }

    //
    @Override
    public void onChangeSong(@NonNull AudioBean bean) {
        if (simplePlayer != null) {
            simplePlayer.notifySongChanged(bean.getName() + " - " + bean.getArtist(), bean.getDuration());
        }//刷新列表
        updateListView();
    }

    @Override
    public void onPlayCompleted() {
        //自动播放下一首
        VMPlayer.getInstance().playNext();
    }

    @Override
    public void onPlayStateChanged(boolean isPlaying) {
        if (simplePlayer != null) {
            simplePlayer.notifyStateChanged(isPlaying);
        }
    }

    @Override
    public void onProcessChanged(int process) {
        if (simplePlayer != null) {
            simplePlayer.notifyProcessChanged(process);
        }
    }

    @Override
    public void onPlayError(int state, String error) {
//        Toast.makeText(context, error, Toast.LENGTH_LONG).show();
        Log.d(TAG, "onPlayError: " + error);
    }

    private final int MSG_UPDATE_LIST = 1;

    private void updateListView(){
        handler.sendEmptyMessage(MSG_UPDATE_LIST);
    }

    private final Handler handler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(@NonNull Message msg) {
            if(msg.what == MSG_UPDATE_LIST){
                if (adapter != null) {
                    adapter.notifyDataSetChanged();
                }
            }
            return false;
        }
    });
}
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".AudioPlayActivity">

    <ListView
        android:id="@+id/listviewAudio"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:divider="@color/white"
        android:dividerHeight="0dp" />

    <com.nepalese.harinetest.musicplayer.VirgoSimplePlayer
        android:id="@+id/simplePlayer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>
相关推荐
selt7911 小时前
Redisson之RedissonLock源码完全解析
android·java·javascript
Nautiluss2 小时前
一起玩XVF3800麦克风阵列(八)
大数据·人工智能·嵌入式硬件·github·音频·语音识别
Yao_YongChao2 小时前
Android MVI处理副作用(Side Effect)
android·mvi·mvi副作用
非凡ghost3 小时前
JRiver Media Center(媒体管理软件)
android·学习·智能手机·媒体·软件需求
席卷全城3 小时前
Android 推箱子实现(引流文章)
android
齊家治國平天下3 小时前
Android 14 系统中 Tombstone 深度分析与解决指南
android·crash·系统服务·tombstone·android 14
maycho1235 小时前
MATLAB环境下基于双向长短时记忆网络的时间序列预测探索
android
思成不止于此6 小时前
【MySQL 零基础入门】MySQL 函数精讲(二):日期函数与流程控制函数篇
android·数据库·笔记·sql·学习·mysql
brave_zhao6 小时前
达梦数据库(DM8)支持全文索引功能,但并不直接兼容 MySQL 的 FULLTEXT 索引语法
android·adb
sheji34166 小时前
【开题答辩全过程】以 基于Android的网上订餐系统为例,包含答辩的问题和答案
android