仿微信听书进度条

复制代码
    <com.example.simplemusic.widget.TimeSeekBar
        android:id="@+id/timeSeekBar"
        android:layout_width="match_parent"
        android:layout_height="30dp"
        android:max="180000"
        android:paddingHorizontal="16dp"
        android:progress="0"
        android:progressDrawable="@drawable/custom_seekbar_progress"
        android:thumb="@android:color/transparent" /> <!-- 隐藏默认 thumb -->

    <com.example.simplemusic.widget.FloatingThumbSeekBar
        android:id="@+id/timeSeekBar1"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:max="180000"
        android:progress="0"
        android:paddingHorizontal="16dp"/>

custom_seekbar_progress.xml

复制代码
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
        <shape>
            <size android:height="4dp"/> <!-- 比 thumb 矮很多 -->
            <solid android:color="#D0D0D0"/>
            <corners android:radius="2dp"/>
        </shape>
    </item>
    <item android:id="@android:id/secondaryProgress">
        <shape>
            <size android:height="4dp"/>
            <solid android:color="#90CAF9"/>
            <corners android:radius="2dp"/>
        </shape>
    </item>
    <item android:id="@android:id/progress">
        <shape>
            <size android:height="4dp"/>
            <solid android:color="#2196F3"/>
            <corners android:radius="2dp"/>
        </shape>
    </item>
</layer-list>

package com.haitun.music.widget;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.widget.SeekBar;

import androidx.appcompat.widget.AppCompatSeekBar;

/**
 * FloatingThumbSeekBar timeSeekBar = findViewById(R.id.timeSeekBar1);
 * <p>
 * // 假设你的歌曲总时长是 5 分 30 秒(即 330000 毫秒)
 * long totalDurationMs = 330000;
 * <p>
 * // 最重要的一步:设置总时长
 * timeSeekBar.setTotalDuration(totalDurationMs);
 * timeSeekBar.updateProgress(totalDurationMs);
 * // 设置一个监听器来获取滑动后的新位置
 * timeSeekBar.setOnSeekStopListener(newPositionMs -> {
 * // 在这里调用你的播放器方法来改变播放位置
 * // 你可以打印日志来验证
 * Log.d("SeekBar", "用户滑动到新的位置 (毫秒): " + newPositionMs);
 * });
 */
public class FloatingThumbSeekBar extends AppCompatSeekBar {

    private Paint trackPaint;
    private Paint progressPaint;
    private Paint thumbPaint;
    private Paint textPaint;
    private Paint borderPaint;

    // 进度条高度
    private float trackHeight = 5f;
    private float thumbHeight = 65f;
    private float thumbOffsetY = -0f;
    private float thumbPadding = 20f;

    private String progressText = "00:00 / 00:00";
    private long totalDuration = 0;

    // 添加自定义接口
    private OnSeekStopListener onSeekStopListener;

    public interface OnSeekStopListener {
        void onSeekStopped(long newPositionMs);
    }

    public void setOnSeekStopListener(OnSeekStopListener listener) {
        this.onSeekStopListener = listener;
    }

    public FloatingThumbSeekBar(Context context) {
        super(context);
        init();
        setupProgressListener();
    }

    public FloatingThumbSeekBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
        setupProgressListener();
    }

    private void init() {
        setThumb(null);

        trackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        trackPaint.setColor(Color.LTGRAY);

        progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        progressPaint.setColor(Color.WHITE);

        thumbPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        thumbPaint.setColor(Color.WHITE);

        borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        borderPaint.setColor(Color.GRAY);
        borderPaint.setStyle(Paint.Style.STROKE);
        borderPaint.setStrokeWidth(2f);

        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(Color.BLACK);
        textPaint.setTextSize(35f);
        textPaint.setTextAlign(Paint.Align.CENTER);
    }

    private void setupProgressListener() {
        setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if (totalDuration > 0) {
                    long currentMs = (long) (progress * 1.0f / getMax() * totalDuration);
                    progressText = formatTime(currentMs, totalDuration);
                    invalidate();
                }
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                // 滑动停止时,调用外部监听器
                if (onSeekStopListener != null) {
                    long newPositionMs = (long) (seekBar.getProgress() * 1.0f / getMax() * totalDuration);
                    onSeekStopListener.onSeekStopped(newPositionMs);
                }
            }
        });
    }

    /**
     * 将毫秒转换为 时分秒 格式
     */
    private String formatTime(long currentMs, long totalMs) {
        long totalSeconds = totalMs / 1000;
        long totalMinutes = (totalSeconds / 60) % 60;
        long totalHours = totalSeconds / 3600;

        String totalTimeStr;
        if (totalHours > 0) {
            totalTimeStr = String.format("%02d:%02d:%02d", totalHours, totalMinutes, totalSeconds % 60);
        } else {
            totalTimeStr = String.format("%02d:%02d", totalMinutes, totalSeconds % 60);
        }

        long currentSeconds = currentMs / 1000;
        long currentMinutes = (currentSeconds / 60) % 60;
        long currentHours = currentSeconds / 3600;

        String currentTimeStr;
        if (totalHours > 0) {
            currentTimeStr = String.format("%02d:%02d:%02d", currentHours, currentMinutes, currentSeconds % 60);
        } else {
            currentTimeStr = String.format("%02d:%02d", currentMinutes, currentSeconds % 60);
        }

        return String.format("%s / %s", currentTimeStr, totalTimeStr);
    }

    @Override
    public void drawableStateChanged() {
        // 不调用 super,防止状态切换触发 ripple
    }

    @Override
    protected synchronized void onDraw(Canvas canvas) {
        float centerY = getHeight() / 2f;
        float startX = getPaddingLeft();
        float endX = getWidth() - getPaddingRight();

        float trackTop = centerY - trackHeight / 2;
        float trackBottom = centerY + trackHeight / 2;
        canvas.drawRect(startX, trackTop, endX, trackBottom, trackPaint);

        float progressEndX = startX + (getProgress() * 1f / getMax()) * (endX - startX);
        canvas.drawRect(startX, trackTop, progressEndX, trackBottom, progressPaint);

        float scale = getProgress() * 1f / getMax();
        float thumbCenterX = startX + scale * (endX - startX);

        float textWidth = textPaint.measureText(progressText);
        float thumbWidth = textWidth + (thumbPadding * 2);

        RectF rect = new RectF(
                thumbCenterX - thumbWidth / 2,
                centerY - thumbHeight / 2 + thumbOffsetY,
                thumbCenterX + thumbWidth / 2,
                centerY + thumbHeight / 2 + thumbOffsetY
        );

        canvas.drawRoundRect(rect, thumbHeight / 2, thumbHeight / 2, borderPaint);
        thumbPaint.setStyle(Paint.Style.FILL);
        canvas.drawRoundRect(rect, thumbHeight / 2, thumbHeight / 2, thumbPaint);

        Paint.FontMetrics fm = textPaint.getFontMetrics();
        float textBaseY = rect.centerY() - (fm.ascent + fm.descent) / 2;
        canvas.drawText(progressText, rect.centerX(), textBaseY, textPaint);
    }

    /**
     * 设置总时长
     */
    public void setTotalDuration(long totalDurationMs) {
        this.totalDuration = totalDurationMs;
        progressText = formatTime(0, totalDurationMs);
        invalidate();
    }

    /**
     * 更新进度条
     */
    public void updateProgress(long currentMs) {
        if (totalDuration <= 0) return;
        int progress = (int) (currentMs * 1.0f / totalDuration * getMax());
        setProgress(progress);
    }

    // Existing style setter methods
    public void setTrackHeight(float height) {
        this.trackHeight = height;
        invalidate();
    }

    public void setTrackColor(int color) {
        trackPaint.setColor(color);
        invalidate();
    }

    public void setProgressColor(int color) {
        progressPaint.setColor(color);
        invalidate();
    }

    public void setTextSize(float size) {
        textPaint.setTextSize(size);
        invalidate();
    }
}


        FloatingThumbSeekBar timeSeekBar = findViewById(R.id.timeSeekBar1);

// 假设你的歌曲总时长是 5 分 30 秒(即 330000 毫秒)
        long totalDurationMs = 330000;

// 最重要的一步:设置总时长
        timeSeekBar.setTotalDuration(totalDurationMs);
        timeSeekBar.updateProgress(totalDurationMs);
// 设置一个监听器来获取滑动后的新位置
        timeSeekBar.setOnSeekStopListener(newPositionMs -> {
            // 在这里调用你的播放器方法来改变播放位置
            // 你可以打印日志来验证
            Log.d("SeekBar", "用户滑动到新的位置 (毫秒): " + newPositionMs);
        });

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/splash"
    android:orientation="vertical"
    android:paddingTop="40dp">

    <com.haitun.music.widget.TimeSeekBar
        android:id="@+id/timeSeekBar"
        android:layout_width="match_parent"
        android:layout_height="30dp"
        android:max="180000"
        android:paddingHorizontal="16dp"
        android:progress="0"
        android:progressDrawable="@drawable/custom_seekbar_progress"
        android:thumb="@android:color/transparent" /> <!-- 隐藏默认 thumb -->

    <com.haitun.music.widget.FloatingThumbSeekBar
        android:id="@+id/timeSeekBar1"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:max="180000"
        android:progress="0"
        android:paddingHorizontal="16dp"/>

</LinearLayout>
相关推荐
QN1幻化引擎2 天前
SFA 信号场注意力:用8KB参数换248x KV Cache压缩,边缘设备也能跑长序列
人工智能·算法·gitee·gitlab
J14818529145 天前
md文件图片在其他地方不能预览
gitee
Mark White7 天前
使用 Codex 自动完成 Gitee 仓库全托管
gitee
hkj88088 天前
vscode提交gitee远程仓库出现Permission denied (publickey)解决方法
ide·vscode·gitee
雪的季节9 天前
ffmpeg源码国内gitee下载
ffmpeg·gitee
御坂嘀喵11 天前
Speed Tools:一套低侵入的 Android 插件化 + 动态换肤 + 字体切换框架
android·gitee
风信子IT人17 天前
【5天实战】从零构建AI-Native组织:飞书+Bot+Gitee全链路自动化实战指南—Day 5:完整场景实操验证
gitee·自动化·飞书·ai-native·bot开发
zakariyaa3317 天前
Android 绘制调度机制
android·gitee
m0_526119401 个月前
ssh key生成,gitee配置ssh
运维·gitee·ssh
我最爱吃鱼香茄子1 个月前
【保姆级】Git配置用户名和邮箱(全局/局部)解决提交记录不识别、贡献图不亮问题
git·gitee·github·小白教程·git配置·程序员开发工具