android 进度条_Android仿酷狗音乐SeekBar(样式篇)

本文介绍如何在Android中自定义SeekBar的滑块样式,包括不同状态下的图案变化,并通过代码实现滑块在按下和释放时的动态效果。

直接上图,上代码

ca2e4699e710357ffe3f3a4a0ee67c60.png

d4ff25946f032fd010b1342799ee3003.png


难点:用户点击或者移动是SeekBar滑块是改变滑块的图案

先画两种不同状态的滑块Thumb

平时状态:一个直径为10dp大小的白色的圆

slider_thumb_normal.xml

<?xml version="1.0" encoding="utf-8"?>  android:shape="oval">          android:width="10dp"        android:height="10dp" />  
按下状态:一个直径为10dp大小的白色的圆,背景是半透明的直径为40dp的圆

slider_thumb_pressed.xml

<?xml version="1.0" encoding="utf-8"?>                android:gravity="center"        android:bottom="15dp"        android:top="15dp"        android:right="15dp"        android:left="15dp">                    android:shape="oval">                            android:width="10dp"                android:height="10dp" />                                        android:gravity="center">                    android:shape="oval">                            android:height="40dp"                android:width="40dp" />                        

画进度条

(不设置高度,由SeekBar自身控制,SeekBar控件

android:layout_height="wrap_content")play_seekbar_bg.xml

<?xml version="1.0" encoding="utf-8"?>    xmlns:android="http://schemas.android.com/apk/res/android">                                        android:height="2dp" />                                                android:id="@android:id/secondaryProgress">                                                        android:height="2dp" />                                                                                                                    android:height="2dp"/>                                                        

SeekBar样式xml片段

            android:layout_width="match_parent"        android:layout_height="wrap_content"        android:layout_margin="8dp">                    android:layout_gravity="center"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:gravity="center"            android:id="@+id/seekbar_slider_time"            android:textColor="@color/color_white"            android:textAppearance="?android:attr/textAppearanceMedium"            android:visibility="invisible" />                android:id="@+id/seekBar_layout"        android:layout_width="match_parent"        android:layout_height="40dp"        android:layout_marginStart="8dp"        android:layout_marginEnd="8dp"        android:gravity="center_vertical"        android:orientation="horizontal">                    android:id="@+id/tx_currentTime"            android:layout_width="40dp"            android:layout_height="wrap_content"            android:gravity="center"            android:textAppearance="?android:attr/textAppearanceSmall"            android:textColor="@color/color_white"/>                    android:id="@+id/seedBar"            android:layout_width="0dp"            android:layout_height="wrap_content"            android:layout_marginStart="-20dp"            android:layout_marginEnd="-20dp"            android:paddingStart="28dp"            android:paddingEnd="28dp"            android:background="@android:color/transparent"            android:gravity="center"            android:layout_weight="1"            android:splitTrack="false"            android:maxHeight="2dp"            android:progressDrawable="@drawable/play_seekbar_bg"            android:thumb="@drawable/slider_thumb_normal" />                    android:id="@+id/tx_maxTime"            android:layout_width="40dp"            android:layout_height="wrap_content"            android:gravity="center"            android:textAppearance="?android:attr/textAppearanceSmall"            android:textColor="@color/color_white"/>    

 SeekBar样式关键点 

  • android:maxHeight="2dp"——控制进度条高度

  • 设置SeekBar控件边际,以便在滑块变大是可覆盖左右两边的控件,而不会被遮住

android:layout_marginStart="-20dp"android:layout_marginEnd="-20dp"android:paddingStart="28dp"android:paddingEnd="28dp"
  • android:splitTrack="false"

    ——控制滑块覆盖在进度条的上面

  • android:background="@android:color/transparent"

    ——设置背景透明,去掉滑块变大时的周边光晕

  • android:progressDrawable="@drawable/play_seekbar_bg"

    ——默认进度条

  • android:thumb="@drawable/slider_thumb_normal"

    ——默认滑块

最关键的地方

使用SeekBar的setThumb方法动态设置滑块

seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {            @Override            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {                if(fromUser){                    Seekbar_slider_time.setText(updateCurrentTimeText(progress));                }                tx_currentTime.setText(updateCurrentTimeText(progress));                if(progress == seekBar.getMax()){                    pauseIcon.setLayoutParams(miss);                    playIcon.setLayoutParams(show);                }            }            @Override            public void onStartTrackingTouch(SeekBar seekBar) {                Log.d(TAG,"onStartTrackingTouch");                isUserPressThumb = true;                Seekbar_slider_time.setVisibility(View.VISIBLE);                //设置seekbarThumb相对位置可大于进度条15,保证thumb在变成40dp直径后可以滑动到进度条最末尾                seekBar.setThumbOffset(15);                seekBar.setThumb(Thumb_pressed);            }            @Override            public void onStopTrackingTouch(SeekBar seekBar) {                Log.d(TAG,"onStopTrackingTouch");                mi.seekTo(seekBar.getProgress());                seekBar.setThumbOffset(0);                seekBar.setThumb(Thumb_normal);                Seekbar_slider_time.setVisibility(View.INVISIBLE);                isUserPressThumb = false;            }        });
在用户开始按下滑块时onStartTrackingTouch

//设置seekbarThumb相对位置可大于进度条15,保证thumb在变成40dp直径后可以滑动到进度条最末尾
seekBar.setThumbOffset(15);

//改变滑块图案
seekBar.setThumb(Thumb_pressed);

在用户按下滑块结束后onStopTrackingTouch,恢复滑块及seekbar高度

seekBar.setThumbOffset(0);seekBar.setThumb(Thumb_normal);

 踩坑过程 

  • 使用selector的xml文件设置SeekBar的android:thumb属性设置滑块 
    play_seekbar_thumb.xml

<?xml version="1.0" encoding="UTF-8"?><selector xmlns:android="http://schemas.android.com/apk/res/android"       android:constantSize="false"       android:variablePadding="false">       <item android:state_focused="true" android:state_pressed="false" android:drawable="@drawable/slider_thumb_normal" />       <item android:state_focused="true" android:state_pressed="true" android:drawable="@drawable/slider_thumb_pressed" />       <item android:state_focused="false" android:state_pressed="true" android:drawable="@drawable/slider_thumb_pressed" />       <item android:drawable="@drawable/slider_thumb_normal" />selector>

坑:有些手机上按下或者移动滑块,滑块是变大了,但是由于SeekBar高度还是原来的,导致滑块被压扁成椭圆 

到这里就结束啦.

e6aa60a5f0fa5f67d7a22986bf439b9d.png

d4561c0d979c6a6a8d38b686891a62b6.png

package com.weishitech.qichechangtingyinyue.fragment.home; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.widget.ImageView; import android.widget.SeekBar; import android.widget.TextView; import androidx.lifecycle.ViewModelProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.bitmap.CircleCrop; import com.hfd.common.base.BaseActivity; import com.hfd.common.util.ToastUtil; import com.weishitech.qichechangtingyinyue.R; import com.weishitech.qichechangtingyinyue.bean.FavoriteManager; import com.weishitech.qichechangtingyinyue.bean.MusicBean; import com.weishitech.qichechangtingyinyue.bean.MusicCache; import com.weishitech.qichechangtingyinyue.bean.SharedMusicViewModel; import com.weishitech.qichechangtingyinyue.utils.MusicPlayerManager; import java.util.List; public class LyricsActivity extends BaseActivity { // UI Views TextView tv_back, tv_song_name, tv_sing_name, tv_start_time, tv_end_time; ImageView iv_sing_logo, iv_tj, iv_gd, iv_xh, iv_sys, iv_bf, iv_xys, iv_zjbf; SeekBar seekbar; private boolean isSingleLoop = false; // 单曲循环开关 private FavoriteManager favoriteManager; private boolean isCurrentSongFavorite = false; // 声明 ViewModel private SharedMusicViewModel sharedViewModel; // 播放管理器 private MusicPlayerManager playerManager; private boolean isBoundToCurrentSong = false; // 是否已绑定当前歌曲 // 数据接收字段 private String title, singer, coverUrl, musicUrl; private int songPosition,totalSongCount; // 进度更新 private final Handler handler = new Handler(); private final Runnable updateProgressRunnable = new Runnable() { @Override public void run() { if (playerManager != null && playerManager.isPlaying()) { int currentPosition = playerManager.getCurrentPosition(); int duration = playerManager.getDuration(); if (duration > 0) { seekbar.setMax(duration); seekbar.setProgress(currentPosition); // 更新时间文本(格式 mm:ss) tv_start_time.setText(formatTime(currentPosition)); tv_end_time.setText(formatTime(duration)); } } handler.postDelayed(this, 1000); // 每秒更新一次 } }; private List<MusicBean.DataBean> sourceList; @Override protected int setLayout() { return R.layout.activity_lyrics; } @Override protected void setView() { tv_back = fvbi(R.id.tv_back); iv_sing_logo = fvbi(R.id.iv_sing_logo); tv_song_name = fvbi(R.id.tv_song_name); tv_sing_name = fvbi(R.id.tv_sing_name); iv_tj = fvbi(R.id.iv_tj); iv_gd = fvbi(R.id.iv_gd); seekbar = fvbi(R.id.seekbar); tv_start_time = fvbi(R.id.tv_start_time); tv_end_time = fvbi(R.id.tv_end_time); iv_xh = fvbi(R.id.iv_xh); iv_sys = fvbi(R.id.iv_sys); iv_bf = fvbi(R.id.iv_bf); iv_xys = fvbi(R.id.iv_xys); iv_zjbf = fvbi(R.id.iv_zjbf); } @Override protected void setData() { Bundle bundle = getIntent().getExtras(); if (bundle != null) { title = bundle.getString("title"); singer = bundle.getString("singer"); coverUrl = bundle.getString("cover_url"); musicUrl = bundle.getString("music_url"); songPosition = bundle.getInt("position", -1); totalSongCount = bundle.getInt("total_count", 0); // 接收来源列表(关键!) if (bundle.containsKey("source_list")) { sourceList = (List<MusicBean.DataBean>) bundle.getSerializable("source_list"); totalSongCount = sourceList.size(); // 再次确认长度 } if (songPosition < 0 || songPosition >= totalSongCount) { ToastUtil.showShortToast("歌曲位置无效"); finish(); return; } tv_song_name.setText(title); tv_sing_name.setText(singer); Glide.with(this) .load(coverUrl) .transform(new CircleCrop()) .into(iv_sing_logo); } // 初始化播放器 //使用单例 playerManager = MusicPlayerManager.getInstance(); // 注册播放状态监听 setupPlaybackListener(); // 绑定当前歌曲(检查是否已在播放) bindToCurrentSong(); // 初始化收藏管理器 favoriteManager = FavoriteManager.getInstance(this); updateFavoriteState(); // 更新按钮状态 // 初始化 ViewModel sharedViewModel = new ViewModelProvider(this).get(SharedMusicViewModel.class); } @Override protected void setClick() { tv_back.setOnClickListener(v -> finish()); iv_sys.setOnClickListener(v -> { if (songPosition <= 0) { ToastUtil.showShortToast("当前已是第一首"); return; } playMusicAtPosition(songPosition - 1); }); iv_xh.setOnClickListener(v -> { isSingleLoop = !isSingleLoop; if (isSingleLoop) { ToastUtil.showShortToast("已开启单曲循环"); iv_xh.setImageResource(R.mipmap.img_xh); // 假设有这个资源 } else { ToastUtil.showShortToast("已关闭单曲循环"); iv_xh.setImageResource(R.mipmap.img_xh); } }); iv_xys.setOnClickListener(v -> { if (songPosition >= totalSongCount - 1) { ToastUtil.showShortToast("当前已是最后一首"); return; } playMusicAtPosition(songPosition + 1); }); iv_tj.setOnClickListener(v -> { if (title == null || musicUrl == null) { ToastUtil.showShortToast("歌曲信息不完整"); return; } MusicBean.DataBean currentSong = new MusicBean.DataBean(); currentSong.setTitle(title); currentSong.setSinger(singer); currentSong.setCover(coverUrl); currentSong.setMusic(musicUrl); currentSong.setSongTime(formatTime(playerManager.getDuration())); // 可选 if (isCurrentSongFavorite) { // 已收藏 → 取消收藏 favoriteManager.removeFromFavorites(musicUrl); iv_tj.setImageResource(R.mipmap.img_tj); // 未收藏图标 ToastUtil.showShortToast("已取消收藏"); } else { // 未收藏 → 添加收藏 favoriteManager.addToFavorites(currentSong); iv_tj.setImageResource(R.mipmap.img_tj_u); // 已收藏图标 ToastUtil.showShortToast("已添加到我喜欢"); } isCurrentSongFavorite = !isCurrentSongFavorite; //通知所有页面,收藏列表已改变 List<MusicBean.DataBean> newList = favoriteManager.getFavoriteList(); sharedViewModel.notifyFavoriteChanged(newList); }); // 播放/暂停按钮 iv_bf.setOnClickListener(v -> { if (!isBoundToCurrentSong) { ToastUtil.showShortToast("暂无歌曲信息"); return; } if (playerManager.isPlaying()) { playerManager.pause(); } else { playerManager.resume(); } updatePlayButtonIcon(); }); // SeekBar 拖动控制 seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) { tv_start_time.setText(formatTime(progress)); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { // 用户开始拖动 } @Override public void onStopTrackingTouch(SeekBar seekBar) { // 用户停止拖动,跳转到指定位置 if (playerManager != null && isBoundToCurrentSong) { playerManager.seekTo(seekbar.getProgress()); } } }); } private void bindToCurrentSong() { if (musicUrl == null || title == null) return; boolean isPlaying = playerManager.isPlaying(); int currentPos = playerManager.getCurrentPlayingPosition(); // 判断是否就是当前正在播放的歌曲 if (isPlaying && currentPos == songPosition) { // 是同一首歌 → 直接同步状态 isBoundToCurrentSong = true; updatePlayButtonIcon(); // 显示“暂停” startUpdatingProgress(); // 开始更新进度条 } else { // 不是当前播放的 → 准备播放这首 playerManager.play(this, songPosition, musicUrl); isBoundToCurrentSong = true; updatePlayButtonIcon(); // 默认播放中 startUpdatingProgress(); } } private void setupPlaybackListener() { playerManager.setOnPlaybackStateChangedListener(new MusicPlayerManager.OnPlaybackStateChangedListener() { @Override public void onPlaying(int position) { if (position == songPosition) { runOnUiThread(() -> { updatePlayButtonIcon(); startUpdatingProgress(); }); } else { // 播放的是其他位置的歌曲 → 当前页面应暂停状态 runOnUiThread(() -> { stopUpdatingProgress(); iv_bf.setImageResource(R.mipmap.img_gc_bf); // 显示播放图标 }); } } @Override public void onPaused() { runOnUiThread(() -> { updatePlayButtonIcon(); stopUpdatingProgress(); }); } @Override public void onCompletion() { runOnUiThread(() -> { // 停止当前进度更新 stopUpdatingProgress(); seekbar.setProgress(0); tv_start_time.setText("00:00"); if (isSingleLoop) { // 重新播放当前歌曲 playerManager.play(LyricsActivity.this, songPosition, musicUrl); startUpdatingProgress(); updatePlayButtonIcon(); } else { // 自动播放下一首 int nextPosition = songPosition + 1; if (nextPosition < totalSongCount) { playMusicAtPosition(nextPosition); } else { // 已到最后 → 可选:循环播放第一首 or 提示 ToastUtil.showShortToast("已播放完毕"); updatePlayButtonIcon(); // 显示为暂停状态 // 如果你想循环回第一首,取消下面注释 playMusicAtPosition(0); } } }); } @Override public void onError(String errorMsg) { runOnUiThread(() -> { ToastUtil.showShortToast("播放出错:" + errorMsg); updatePlayButtonIcon(); stopUpdatingProgress(); }); } }); } private void updatePlayButtonIcon() { if (playerManager.isPlaying()) { iv_bf.setImageResource(R.mipmap.img_gc_zt); // 暂停图标 } else { iv_bf.setImageResource(R.mipmap.img_gc_bf); // 播放图标 } } private void startUpdatingProgress() { stopUpdatingProgress(); // 防止重复 handler.postDelayed(updateProgressRunnable, 0); } private void stopUpdatingProgress() { handler.removeCallbacks(updateProgressRunnable); } @Override public void onResume() { super.onResume(); // 页面可见时尝试恢复状态 if (isBoundToCurrentSong) { updatePlayButtonIcon(); } } @Override protected void onPause() { super.onPause(); // 可选:不移除监听,让后台也能播放 } @Override protected void onDestroy() { stopUpdatingProgress(); // 注意:不要 release() playerManager,否则其他页面会中断 // playerManager.release(); ❌ 不要在这里释放 super.onDestroy(); } // 时间格式化工具 private String formatTime(int milliseconds) { int seconds = milliseconds / 1000; int minutes = seconds / 60; seconds = seconds % 60; return String.format("%02d:%02d", minutes, seconds); } private void playMusicAtPosition(int pos) { // 使用 MusicCache 判断边界(替代 musicDataList) if (pos < 0 || pos >= totalSongCount) { return; // 越界防护 } MusicBean.DataBean song; if (sourceList != null && pos < sourceList.size()) { song = sourceList.get(pos); } else { // 回退到主列表(兼容主列表播放) song = MusicCache.getMusicAt(pos); } // 播放指定位置的音乐 playerManager.play(this, pos, song.getMusic()); // 更新界面 updateUIForNewSong(song, pos); } /** * 播放新歌时更新界面和内部状态 */ private void updateUIForNewSong(MusicBean.DataBean song, int position) { // 更新成员变量 this.title = song.getTitle(); this.singer = song.getSinger(); this.coverUrl = song.getCover(); this.musicUrl = song.getMusic(); this.songPosition = position; // 更新 UI 显示 tv_song_name.setText(title); tv_sing_name.setText(singer); if (isDestroyed() || isFinishing()) return; Glide.with(this) .load(coverUrl) .transform(new CircleCrop()) .into(iv_sing_logo); // 标记已绑定当前歌曲 isBoundToCurrentSong = true; // 更新播放按钮为“正在播放” updatePlayButtonIcon(); // 开始进度条更新 startUpdatingProgress(); // 可选:Toast 提示 ToastUtil.showShortToast("播放: " + title); } private void updateFavoriteState() { if (musicUrl == null) { iv_tj.setImageResource(R.mipmap.img_tj); isCurrentSongFavorite = false; return; } isCurrentSongFavorite = favoriteManager.isFavorite(musicUrl); iv_tj.setImageResource(isCurrentSongFavorite ? R.mipmap.img_tj_u : R.mipmap.img_tj); } } 点击iv_zjbf按钮弹出类似酷狗音乐的最近播放列表弹框
最新发布
12-05
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值