Android 播放器进度条改造实践:句级音频列表映射秒级时间轴

背景:我们正常的文章列表是句式连播,ui上进度条两侧是显示的当前播放是第几句以及总句数,以句为单位,在进行进度条滑动时也是设置到某一句播放。 这2点不同于传统音乐播放器,传统音乐播放器播放的是整首单个mp3文件,ui上显示的当前播放到多少秒,以及总时长共多少秒,进度条拖动也是精确到秒的。

有的句音频长有的短,所以进度条在动态上会有些突兀。

需要是让我改成与传统音乐播放器一样,播放以秒为单位。逐秒控制播放、当前和总共都以秒来显示。 以下是设计思路和关键代码 记录一下。

实现思路:

1、先把总秒数算出来。 如果数据能支持就让数据支持,在数据模型中加一个字段duration ,给出每一句的时长,然后累加计算,得出的总时长用来设置进度的长度和ui显示。

csharp 复制代码
//在获取数据中  进行累加时长
if (list.get(i) != null && list.get(i).getDuration() > 0) {
   map.put("duration", list.get(i).getDuration() + "");
// Log.i("TAG", "duration: "+ list.get(i).getDuration());
   long duration = list.get(i).getDuration();
   ConstantFd.durations.add(duration);
   ConstantFd.totalDuration = ConstantFd.totalDuration + duration;
// Log.i("TAG", "totalDuration: "+totalDuration);
}


//3个全局变量
public static List<Long> durations = new ArrayList<>(); //每句的时长列表   
public static long totalDuration = 0;//总时长
public static int currentPoi = 0; //当前播放句

如果数据上不支持,只能让播放器去加载音频列表再得出总时长了。

2、在播放服务中建一个Handler 每秒向UI发送进度,UI收到消息进行数据转换和展示

arduino 复制代码
private static long process = 0; //播放进度
    private static long max = 0; //最大进度
    private final Handler mHandler_tb = new Handler(new Handler.Callback() { //播放进度
        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case 0:
                    max = ConstantFd.totalDuration;
                    if(max<0){
                        max=0;
                    }else if (max > 10000){
                        max -= 1000;
                    }
                    long cur = apm.getMediaPlayer().getCurrentPosition(); //当前句子进度
                    process = cur + getPlayProcess(); //前几句的总时间
                    if(process<0){
                        process=0;
                    }
//                    Log.i("TAG", "process:  "+process);
//                    Log.i("TAG", "max:  "+max);
                    PoiToUIMsg.sendMsg(process, max); //发送进度到UI
                    if(ConstantFd.isplay){
                        process = process + 1000;
                        mHandler_tb.sendEmptyMessageDelayed(0, 1000);
                    }
                    fixedThreadPool.execute(command4); //保存播放进度
                    break;
            }
            return true;
        }
    });

private static long getPlayProcess(){ //获取播放进度
        if(ConstantFd.durations.size()==0 || mPosSomeTime == 0){
            return 0;
        }
        if(mPosSomeTime >= ConstantFd.durations.size()){
            mPosSomeTime = ConstantFd.durations.size()-1;
            ConstantFd.currentPoi = mPosSomeTime;
        }
        long pro = 0;
        for (int i = 0; i < mPosSomeTime; i++) {
            pro = pro + ConstantFd.durations.get(i);
        }
        return pro;
    }

//进度消息
public class PoiToUIMsg { //服务->UI
    long poi; //当前指针
    long max; //最大指针

    public long getPoi() {
        return poi;
    }

    public void setPoi(long poi) {
        this.poi = poi;
    }

    public long getMax() {
        return max;
    }

    public void setMax(long max) {
        this.max = max;
    }

    public static void sendMsg(long poi, long max){
        PoiToUIMsg msg = new PoiToUIMsg();
        msg.setPoi(poi);
        msg.setMax(max);
        EventBus.getDefault().post(msg); //发送指针
    }
}
ini 复制代码
    @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
    public void receiveProcess(PoiToUIMsg msg) { //接收进度
//        Log.i("TAG", "process: "+msg.getPoi());
//        Log.i("TAG", "getMax: "+msg.getMax());
        mBinding.seekbar1.setMax((int)msg.getMax());
        mBinding.seekbar1.setProgress((int)msg.getPoi());
        if(msg.getPoi()<=msg.getMax()){
            mBinding.tvNum1.setText(ConstantFd.FormatMiss(msg.getPoi()));
        }else {
            mBinding.tvNum1.setText(ConstantFd.FormatMiss(msg.getMax()));
        }
        mBinding.tvNum2.setText(ConstantFd.FormatMiss(msg.getMax()));
    }

// 格式化 录音时长为 时:分:秒
    public static String FormatMiss(long miss) {
        String mm,ss;
        if(((miss / 1000) % 3600) / 60 < 10){
            mm = "0"+((miss / 1000) % 3600) / 60;
        }else {
            mm = ""+((miss / 1000) % 3600) / 60;
        }
        if((miss / 1000) % 60 < 10){
            ss = "0"+(miss / 1000) % 60;
        }else {
            ss = ""+(miss / 1000) % 60;
        }
        return  mm + ":" + ss;
    }

3、播放动态中,需要把秒和句进行转换 ,下面是Fragment中的代码,滑动进度条时发送进度消息,ui页面和服务都会接收到。

java 复制代码
mBinding.seekbar1.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) {//进度条滑动
                Log.e("TAG", "进度条,当前值:" + seekBar.getProgress());
                mBinding.ivReadBf.setImageResource(R.drawable.icon_pause);//图标改为播放
                PoiToPlayMsg.sendMsg(seekBar.getProgress()); //发送指针
                mBinding.tvNum1.setText(ConstantFd.FormatMiss(seekBar.getProgress()));
            }
        });

@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
    public void receiveProcess(PoiToPlayMsg msg) { //接收滑动的进度
        seekToSpecificPoi(msg.getPoi());
    }

    private void seekToSpecificPoi(long totalPositionMs) { //播放第几秒
        Log.i("TAG", " totalPositionMs: "+totalPositionMs);
        Log.i("TAG", " totalDuration: "+ConstantFd.totalDuration);
        int mPosSomeTime = 0;
        if (totalPositionMs >= ConstantFd.totalDuration) {
            mPosSomeTime = ConstantFd.durations.size() - 1;
        }else {
            long accumulatedDuration = 0l;
            for (int i = 0; i < ConstantFd.durations.size(); i++) {
                long duration = ConstantFd.durations.get(i);
                if (totalPositionMs < accumulatedDuration + duration) {
                    long relativePositionMs = totalPositionMs - accumulatedDuration;
                    mPosSomeTime = i;
                    //Log.i("TAG", " mPosSomeTime: "+mPosSomeTime);
                    //Log.i("TAG", " relativePositionMs: "+relativePositionMs);
                    break;
                }
                accumulatedDuration += duration;
            }
        }

        readAdapter.mPosSomeTime = mPosSomeTime;//播放指针 高亮显示
        if(isResumed()){
            readAdapter.notifyDataSetChanged(); //列表UI更新
            mBinding.recyclerView.scrollToPosition(readAdapter.mPosSomeTime); //列表滚动
        }
    }

public class PoiToPlayMsg { 
    long poi; //当前指针

    public long getPoi() {
        return poi;
    }

    public void setPoi(long poi) {
        this.poi = poi;
    }


    public static void sendMsg(long poi){
        PoiToPlayMsg msg = new PoiToPlayMsg();
        msg.setPoi(poi);
        EventBus.getDefault().post(msg); //发送指针
    }
}

4、服务中的代码 根据秒计算播放指针,以及连续播放时根据指针计算秒

ini 复制代码
 @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
    public void receiveProcess(PoiToPlayMsg msg) { //接收滑动的进度
        if (readType.equals(ConstantFd.tbtl)) {
            setPlayPoi(msg.getPoi());
        }
    }

    private void setPlayPoi(long poi){ //播放指定位置
        Log.i("TAG", "滑动的 poi: "+poi);
        seekToSpecificPosition(poi);
        ConstantFd.isplay = true;
        apm.playOrPause(true);
    }

private void seekToSpecificPosition(long totalPositionMs) { //需要播放第几秒
        Log.i("TAG", " totalPositionMs: "+totalPositionMs);
        Log.i("TAG", " totalDuration: "+ConstantFd.totalDuration);
        mHandler_tb.removeMessages(0);
        apm.playOrPause(false);
        if (totalPositionMs >= ConstantFd.totalDuration) {
            mPosSomeTime = ConstantFd.durations.size() - 1;
            ConstantFd.currentPoi = mPosSomeTime;
            playStart();
            return;
        }
        process = totalPositionMs;
        long accumulatedDuration = 0l;
        for (int i = 0; i < ConstantFd.durations.size(); i++) {
            long duration = ConstantFd.durations.get(i);
            if (totalPositionMs < accumulatedDuration + duration) {
                long relativePositionMs = totalPositionMs - accumulatedDuration;
                mPosSomeTime = i;
                ConstantFd.currentPoi = mPosSomeTime;
                Log.i("TAG", " mPosSomeTime: "+mPosSomeTime);
                Log.i("TAG", " relativePositionMs: "+relativePositionMs);
                playStart();//先加载播放到当前句
                apm.getMediaPlayer().seekTo(relativePositionMs);//然后设置到秒
                break;
            }
            accumulatedDuration += duration;
        }
        mHandler_tb.sendEmptyMessage(0);
    }

 private void playStart() {
        apm.setAudioFile2(ConstantFd.ycList.get(mPosSomeTime).get("read")); //在线
        apm.start();
    }

@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
    public void receiveEventBus_Poi(PlayPoiMsg msg) { //接收指针 播这一句
        mHandler_tb.removeMessages(0);
        mPosSomeTime = msg.getPoi();
        ConstantFd.currentPoi = mPosSomeTime;
        process = 0;
        for (int i = 0; i < mPosSomeTime; i++) {
            long duration = ConstantFd.durations.get(i);
            process = process+duration; //计算已播放的时长
        }
        ConstantFd.isplay = true;
        apm.playOrPause(true);
        playStart();
        mHandler_tb.sendEmptyMessage(0);
    }

总结:

秒-句转换算法(核心)

有两个类似的遍历逻辑,但作用不同:

  • 服务层 seekToSpecificPosition :把秒拆成 句索引 + 句内偏移,然后 seekTo(offset)
  • UI 层 seekToSpecificPoi:只算句索引,用于列表高亮和滚动
ini 复制代码
总时长 = [句0: 3000ms] + [句1: 2500ms] + [句2: 4000ms] = 9500ms

用户拖动到 5500ms:
- 句0累计 3000ms,5500 > 3000,继续
- 句1累计 5500ms,5500 < 5500?不,是 <= 还是 < 要注意边界
- 实际应落在句1内偏移 2500ms(如果 5500-3000=2500,正好等于句1时长)

边界:如果拖动到 9500ms(末尾),应直接定位到最后一句。

3 个全局静态是老项目遗留写法,不推荐新项目使用!!!

相关推荐
我命由我123455 小时前
Bugly - Bugly 基本使用( App 质量追踪平台)
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
宋哥转AI6 小时前
Spring AI Graph:从0到Supervisor(一)RAG子图+Supervisor路由踩坑全记录
java·agent
Mahir086 小时前
MyBatis 深度解密:从执行流程到底层原理全解
java·后端·面试·mybatis
菜菜的顾清寒6 小时前
力扣hot100(37)栈-有效的括号
java·开发语言
罗超驿6 小时前
9.LeetCode 209. 长度最小的子数组 | 滑动窗口专题详解
java·算法·leetcode·面试
孟林洁6 小时前
Java转AI应用开发速成(3)—— 第一个 SpringAI 聊天应用
java·spring boot·后端·ai·机器人
Simon523146 小时前
Spring AOP 五大通知类型
java·前端·spring
早睡身体真不戳6 小时前
【无标题】
java·服务器·windows
布吉岛的石头6 小时前
Java 程序员第 38 阶段:Embedding 向量缓存实战,减少重复向量化计算开销
java·缓存·embedding