安卓开发个人小作品(3) – 多功能音乐播放器[通俗易懂]

2022-09-13 11:36:21 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

这次介绍一个多功能音乐播放器,记得是大二那年寒假写的,实现的主要功能就是音乐播放,带进度条控制,扫描本地音乐,上一曲下一曲,播放类型(单曲循环,顺序播放,随机播放),APP主题换肤,背景图更换等,功能都比较基础,基本上如果你不会的话,跟着我的思路,应该都是能实现的,预计会在以后加入歌词的功能。

在开始前,先放一张最后的效果图吧,我个人喜欢的风格,简约,美观。

目录

1.实现扫描本地音乐

2.音乐的播放与控制

3.关联进度条seekbar,自定义seekbar

4.单曲循环,顺序播放,随机播放的实现

5.设置喜爱音乐

6.播放列表背景图设置与保存

7.实现APP主题换肤的功能

正文

1.实现扫描本地音乐

这里为了将每个系统里面存放的音乐抽象出来,也是为了方便管理,先定义一个音乐类Song,代码如下

代码语言:javascript复制
public class Song {
	/** * 歌手 */
	private String singer;
	/** * 歌曲名 */
	private String song;
	/** * 歌曲的地址 */
	private String path;
	/** * 歌曲长度 */
	private int duration;
	/** * 歌曲的大小 */
	private long size;

	public String getSinger() {
		return singer;
	}

	public void setSinger(String singer) {
		this.singer = singer;
	}

	public String getSong() {
		return song;
	}

	public void setSong(String song) {
		this.song = song;
	}

	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;
	}
}

然后我们再写一个工具类,这个工具类实现的功能就是扫描系统中的本地音乐,返回一个List<Song>集合,供我们使用,代码如下

代码语言:javascript复制
public class MusicUtils {
	/**
	 * 扫描系统里面的音频文件,返回一个list集合
	 */
	public static List<Song> getMusicData(Context context) {
		List<Song> list = new ArrayList<>();
		Cursor cursor = context.getContentResolver().query(
				MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null,
				MediaStore.Audio.AudioColumns.IS_MUSIC);
		if (cursor != null) {
			while (cursor.moveToNext()) {
				Song song = new Song();
				song.setSong( cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)));
				song.setSinger( cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)));
				song.setPath(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)));
				song.setDuration( cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)));
				song.setSize( cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)));
				if (song.getSize() > 1000 * 800) {//过滤掉短音频
					// 分离出歌曲名和歌手
					if (song.getSong().contains("-")) {
						String[] str = song.getSong().split("-");
						song.setSinger( str[0]);
						song.setSong( str[1]);
					}
					list.add(song);
				}
			}
			// 释放资源
			cursor.close();
		}
		return list;
	}

	//格式化时间
	public static String formatTime(int time) {
		if (time / 1000 % 60 < 10) {
			return time / 1000 / 60   ":0"   time / 1000 % 60;
		} else {
			return time / 1000 / 60   ":"   time / 1000 % 60;
		}
	}
}

然后,在布局里定义一个Listview,再给Listview写一个适配器,一般继承自BaseAdapter,adapter代码如下

代码语言:javascript复制
public class MyAdapter extends BaseAdapter {
	private Context context;
	private List<Song> list;
	private int position_flag = 0;

	public MyAdapter(MainActivity mainActivity, List<Song> list) {
		this.context = mainActivity;
		this.list = list;
	}

	@Override
	public int getCount() {
		return list.size();
	}

	@Override
	public Object getItem(int i) {
		return list.get(i);
	}

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

	@Override
	public View getView(int i, View view, ViewGroup viewGroup) {

		ViewHolder holder = null;
		if (view == null) {
			holder = new ViewHolder();
			// 引入布局
			view = View.inflate(context, R.layout.list_item, null);
			// 实例化对象
			holder.song = (TextView) view.findViewById(R.id.item_mymusic_song);
			holder.singer = (TextView) view
					.findViewById(R.id.item_mymusic_singer);
			holder.duration = (TextView) view
					.findViewById(R.id.item_mymusic_duration);
			holder.position = (TextView) view
					.findViewById(R.id.item_mymusic_postion);

			view.setTag(holder);
		} else {
			holder = (ViewHolder) view.getTag();
		}
		// 给控件赋值
		String string_song = list.get(i).getSong();
		if (string_song.length() >= 5
				&& string_song.substring(string_song.length() - 4,
						string_song.length()).equals(".mp3")) {
			holder.song.setText(string_song.substring(0,
					string_song.length() - 4).trim());
		} else {
			holder.song.setText(string_song.trim());
		}

		holder.singer.setText(list.get(i).getSinger().toString().trim());
		// 时间转换为时分秒
		int duration = list.get(i).getDuration();
		String time = MusicUtils.formatTime(duration);
		holder.duration.setText(time);

		return view;
	}

	class ViewHolder {
		TextView song;// 歌曲名
		TextView singer;// 歌手
		TextView duration;// 时长
		TextView position;// 序号
	}

}

adapter里面的列表项布局代码如下

代码语言:javascript复制
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="60dp"
    android:orientation="horizontal">

    <TextView
        android:id="@ id/item_mymusic_postion"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_alignBottom="@ id/item_mymusic_singer"
        android:layout_gravity="center_vertical"
        android:gravity="center"
        android:layout_margin="10dp"
        android:text="1"
        android:textSize="18sp" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="60dp">

        <TextView
            android:id="@ id/item_mymusic_song"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:ellipsize="end"
            android:maxLines="1"
            android:layout_marginRight="10dp"
            android:layout_marginTop="5dp"
            android:text="歌曲名"
            android:textSize="18sp" />

        <TextView
            android:id="@ id/item_mymusic_singer"
            android:layout_width="wrap_content"
            android:layout_height="30dp"
            android:layout_alignParentBottom="true"
            android:layout_alignParentLeft="true"
            android:layout_toLeftOf="@ id/item_mymusic_duration"
            android:gravity="bottom"
            android:text="歌手"
            android:ellipsize="end"
            android:maxLines="1"
            android:layout_marginBottom="5dp"
            android:textSize="16sp" />

        <TextView
            android:id="@ id/item_mymusic_duration"
            android:layout_width="wrap_content"
            android:layout_height="30dp"
            android:layout_alignParentBottom="true"
            android:layout_alignParentRight="true"
            android:gravity="bottom"
            android:layout_marginRight="5dp"
            android:layout_marginBottom="5dp"
            android:text="歌曲时间"
            android:textSize="16sp" />

    </RelativeLayout>


</LinearLayout>

然后在Listview所在的activity里,调用工具类获取音乐集合,构造适配器,给Listview设置适配器,即可在Listview中显示本地所有的音乐啦,关键代码就三行,如下

代码语言:javascript复制
List<Song> list = MusicUtils.getMusicData(MainActivity.this);
MyAdapter adapter = new MyAdapter(MainActivity.this, list);
listview.setAdapter(adapter);

好了,到现在为止,你已经实现了,显示手机里所有的音乐,但是还不能播放,怎么播放,接着往下看

2.音乐的播放与控制

实现音乐播放,需要用到的类为MediaPlayer,为了方便,封装一个播放音乐的方法,如下

代码语言:javascript复制
private void musicplay(int position) {
        try {
            mplayer.reset();
            mplayer.setDataSource(list.get(position).getPath());
            mplayer.prepare();
            mplayer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

传入的position为,播放的音乐的位置,即序号。

然后给listview设置点击事件

代码语言:javascript复制
listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> parent, View view,
                                    int position, long id) {
                musicplay(currentposition);
            }
        });

这样我们只是实现了简单的播放,点击Listview对应的条目,即可播放对应的音乐

我们下一步就是实现,音乐播放的控制,即暂停,下一曲,上一曲的实现

首先是暂停,在播放按钮的点击时间中,我们通常的需求是这样的,如果当前音乐正在播放,那么点击,暂停音乐,再点击,即可再次接着上次的继续播放,所以在播放按钮的点击事件中,需要根据不同情况处理,同时为了直观,需要准备两张图片,播放的时候一张,暂停的时候一张,播放按钮的点击事件如下

代码语言:javascript复制
imageView_play.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                if (mplayer.isPlaying()) {
                    mplayer.pause();
                    imageview.clearAnimation();
                } else {
                    mplayer.start();
                    // thread = new Thread(new SeekBarThread());
                    // thread.start();
                    imageview.startAnimation(AnimationUtils.loadAnimation(
                            MainActivity.this, R.anim.imageview_rotate));
                }
            }
        });

由于为了界面体验良好,我这里还设置了,当音乐播放的时候,左侧图片的旋转效果,代码已经在上面的点击事件中,效果图如下

左侧imageview的动画代码如下

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="20000"
    android:fromDegrees="0"
    android:interpolator="@android:anim/linear_interpolator"
    android:pivotX="50%"
    android:pivotY="50%"
    android:repeatCount="-1"
    android:repeatMode="restart"
    android:toDegrees="360" />

扯了点其他的,下面来实现上一曲和下一曲的效果,我们也可以和播放一个,分别写一个对应的方法

上一曲方法代码如下

代码语言:javascript复制
// 上一曲
    private void frontMusic() {
        currentposition--;
        if (currentposition < 0) {
            currentposition = list.size() - 1;
        }
        musicplay(currentposition);
    }

其中,currentposition是记录的当前音乐播放序号,这里有一点需要考虑的是,当前播放音乐的序号为0的时候,进行–操作之后那么会变成负数,所以,这里根据逻辑,处理为播放列表最后一曲,即设置序号为list.size()-1,形成一个环形。

相信你看了上一曲的方法,那么下一曲也很简单了,下一曲方法代码如下

代码语言:javascript复制
// 下一曲
    private void nextMusic() {
        currentposition  ;
        if (currentposition > list.size() - 1) {
            currentposition = 0;
        }
        musicplay(currentposition);
    }

同样我们也需要处理播放歌曲到最后一曲的时候,设置为播放列表第一首歌曲。

3.关联进度条seekbar,自定义seekbar

关联进度条的方法也很简单,这里将更新seekbar的方法重新开了一个线程,专门处理更新,代码如下

代码语言:javascript复制
// 自定义的线程,用于下方seekbar的刷新
    class SeekBarThread implements Runnable {

        @Override
        public void run() {
            while (!ischanging && mplayer.isPlaying()) {
                // 将SeekBar位置设置到当前播放位置
                seekBar.setProgress(mplayer.getCurrentPosition());

                try {
                    // 每500毫秒更新一次位置
                    Thread.sleep(500);
                    // 播放进度

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

其中,ischanging用于判断当前的seekbar是否处于滑动状态,然后在音乐播放的地方,也就是刚才封装的musicplay方法中,更改为如下代码

代码语言:javascript复制
private void musicplay(int position) {
        seekBar.setMax(list.get(position).getDuration());
        imageview.startAnimation(AnimationUtils.loadAnimation(
                MainActivity.this, R.anim.imageview_rotate));
        try {
            mplayer.reset();
            mplayer.setDataSource(list.get(position).getPath());
            mplayer.prepare();
            mplayer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
        thread = new Thread(new SeekBarThread());
        thread.start();
    }

当然,不要忘了先设置seekbar的最大刻度值,也就是上面代码中setMax方法。

至此,你的音乐播放就已经和seekbar进度条关联起来了,但是你可能会发现系统默认的进度条很丑,不符合你的审美,那么我们就需要更改seekbar的样式,也就是自定义seekbar。

自定义seekbar,需要在布局中设置progressDrawable和thumb,分别对应进度条的背景和进度条的指示小图标,我这里进度条的背景采用的是drawable,代码如下

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >

    <item android:id="@ id/background">
        <shape>
            <solid 
                android:color="#DCDCDC"/>
        </shape>
    </item>
    <item android:id="@ id/secondaryProgress">
        <clip>
            <shape>
                <solid android:color="@color/blue" />
            </shape>
        </clip>
    </item>
    <item android:id="@ id/progress">
        <clip>
            <shape>
                <solid android:color="@color/blue" />
            </shape>
        </clip>
    </item>

</layer-list>

而thumb,我这里使用的就是一张图片,可以在我的项目源代码中找到,图片长下面这个样子

当然你也可以采用自己的图片,来实现炫酷的效果哦!

4.单曲循环,顺序播放,随机播放的实现

实现这个效果,首先我哦们定义一个变量,用于记录当前的播放类型是哪种,如下

代码语言:javascript复制
// 用于判断当前的播放顺序,0->单曲循环,1->顺序播放,2->随机播放
private int play_style = 0;

然后在我们的更改播放类型的按钮点击事件中,更改它的值,点击事件代码如下

代码语言:javascript复制
imageview_playstyle.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                play_style  ;
                if (play_style > 2) {
                    play_style = 0;
                }

                switch (play_style) {
                    case 0:
                        imageview_playstyle.setImageResource(R.mipmap.cicle);
                        Toast.makeText(MainActivity.this, "单曲循环",
                                Toast.LENGTH_SHORT).show();
                        break;
                    case 1:
                        imageview_playstyle.setImageResource(R.mipmap.ordered);
                        Toast.makeText(MainActivity.this, "顺序播放",
                                Toast.LENGTH_SHORT).show();
                        break;
                    case 2:
                        imageview_playstyle.setImageResource(R.mipmap.unordered);
                        Toast.makeText(MainActivity.this, "随机播放",
                                Toast.LENGTH_SHORT).show();
                        break;

                }
            }
        });

逻辑比较简单,应该都能看懂,然后就是怎么根据这个变量来实现对应的效果,核心方法就是MediaPLayer的setOnCompeleteListener,代码如下

代码语言:javascript复制
// 监听mediaplayer播放完毕时调用
        mplayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {

            @Override
            public void onCompletion(MediaPlayer mp) {
                // TODO Auto-generated method stub
                switch (play_style) {
                    case 0:
                        musicplay(currentposition);
                        break;
                    case 1:
                        nextMusic();
                        break;
                    case 2:
                        random_nextMusic();
                        break;
                    default:

                        break;
                }
            }
        });

下一曲的代码上面已经给出了,下面是随机播放下一曲的代码,思想很简单,就是生成一个随机数,再设置为currentpositon,然后调用musicplay方法即可

代码语言:javascript复制
// 随机播放下一曲
    private void random_nextMusic() {
        currentposition = currentposition   random.nextInt(list.size() - 1);
        currentposition %= list.size();
        musicplay(currentposition);
    }

5.设置喜爱音乐

喜爱音乐的设置,我这里处理的比较简单, 当长按列表项的时候,弹出对话框,用于设置喜爱音乐,效果如下

然后,用sharepreference记录下喜爱音乐的序号值,当要播放喜爱音乐的时候,直接取到该序号值,然后调用musicplay方法播放序号值对应的音乐即可。主要就是sharepreference的使用,代码很简单,就不贴了

6.播放列表背景图设置与保存

设置播放列表背景也就是调用一下,listview.setBackground即可,但是我们如果不进行保存的话,下次进入APP的时候,背景图可能又恢复为初始的,那么我们就需要保存列表ode背景图,这里也采用sharepreference来保存,首先用Base64将图片转换为String,然后保存起来,下次进入APP的时候,再取出来,用Base64将String转为drawable对象,在设置上去即可。相关代码如下。

代码语言:javascript复制
// 使用sharedPreferences保存listview背景图片
    private void saveDrawable(Drawable drawable) {
        SharedPreferences.Editor editor = sharedPreferences.edit();
        BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
        Bitmap bitmap = bitmapDrawable.getBitmap();
        // Bitmap bitmap = BitmapFactory.decodeResource(getResources(), id);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.JPEG, 50, baos);
        String imageBase64 = new String(Base64.encodeToString(
                baos.toByteArray(), Base64.DEFAULT));
        editor.putString("listbg", imageBase64);
        editor.commit();
    }

    // 加载用sharedPreferences保存的图片
    private Drawable loadDrawable() {
        String temp = sharedPreferences.getString("listbg", "");
        ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decode(
                temp.getBytes(), Base64.DEFAULT));
        return Drawable.createFromStream(bais, "");
    }

7.实现APP主题换肤的功能

实现主题效果,有很多种方法,我这里采用的是自定义属性的方法,首先我们在values下新建一个文件attrs,内容如下

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="theme_color" format="color" />
    <attr name="popupwindow_bg" format="reference"/>
    <attr name="dialogactivity_bg" format="reference"/>
    <attr name="btn_submit_bg" format="reference"/>
    <attr name="seekbar_progress_bg" format="reference"/>
    <attr name="play_image" format="reference" />
    <attr name="next_image" format="reference" />
    <attr name="front_image" format="reference" />
    <attr name="thumb_image" format="reference" />
    <attr name="indicate_image" format="reference" />
</resources>

这里每一个attr属性代表了哪些内容需要根据主题不同而更换,比如popupwindow_bg,即弹出窗口的背景色等等,然后在styles文件文件中指定各个主题下,这些值分别对应哪个具体的值,styles中相关代码如下

代码语言:javascript复制
<style name="Theme_blue">
        <item name="theme_color">@color/blue</item>
        <item name="popupwindow_bg">@drawable/popupwindow_bg</item>
        <item name="dialogactivity_bg">@drawable/dialogactivity_bg</item>
        <item name="btn_submit_bg">@drawable/btn_submit_bg</item>
        <item name="seekbar_progress_bg">@drawable/seekbar_progress_bg</item>
        <item name="play_image">@mipmap/play</item>
        <item name="next_image">@mipmap/next</item>
        <item name="front_image">@mipmap/front</item>
        <item name="thumb_image">@mipmap/seekbar_thumb</item>
        <item name="indicate_image">@mipmap/play_small</item>
    </style>

    <style name="Theme_purple">
        <item name="theme_color">@color/purple</item>
        <item name="popupwindow_bg">@drawable/popupwindow_bg_purple</item>
        <item name="dialogactivity_bg">@drawable/dialogactivity_bg_purple</item>
        <item name="btn_submit_bg">@drawable/btn_submit_bg_purple</item>
        <item name="seekbar_progress_bg">@drawable/seekbar_progress_bg_purple</item>
        <item name="play_image">@mipmap/play_purple</item>
        <item name="next_image">@mipmap/next_purple</item>
        <item name="front_image">@mipmap/front_purple</item>
        <item name="thumb_image">@mipmap/seekbar_thumb_purple</item>
        <item name="indicate_image">@mipmap/play_small_purple</item>
    </style>

    <style name="Theme_green">
        <item name="theme_color">@color/green</item>
        <item name="popupwindow_bg">@drawable/popupwindow_bg_green</item>
        <item name="dialogactivity_bg">@drawable/dialogactivity_bg_green</item>
        <item name="btn_submit_bg">@drawable/btn_submit_bg_green</item>
        <item name="seekbar_progress_bg">@drawable/seekbar_progress_bg_green</item>
        <item name="play_image">@mipmap/play_green</item>
        <item name="next_image">@mipmap/next_green</item>
        <item name="front_image">@mipmap/front_green</item>
        <item name="thumb_image">@mipmap/seekbar_thumb_green</item>
        <item name="indicate_image">@mipmap/play_small_green</item>
    </style>

    <style name="Theme_red">
        <item name="theme_color">@color/red</item>
        <item name="popupwindow_bg">@drawable/popupwindow_bg_red</item>
        <item name="dialogactivity_bg">@drawable/dialogactivity_bg_red</item>
        <item name="btn_submit_bg">@drawable/btn_submit_bg_red</item>
        <item name="seekbar_progress_bg">@drawable/seekbar_progress_bg_red</item>
        <item name="play_image">@mipmap/play_red</item>
        <item name="next_image">@mipmap/next_red</item>
        <item name="front_image">@mipmap/front_red</item>
        <item name="thumb_image">@mipmap/seekbar_thumb_red</item>
        <item name="indicate_image">@mipmap/play_small_red</item>
    </style>

可以很清楚的看到,我设置了四个主题,每个主题中,我都对attrs中定义的属性进行了具体的赋值,然后怎么使用呢,举个例子,比如我现在需要让popupwindow的背景色随主题改变而更换,那么在popupwindow的布局中,设置其background属性为如下即可

代码语言:javascript复制
android:background="?attr/popupwindow_bg"

其他属性的使用方法同理,然后我们如何来让用户设置主题呢,可以写一个dialog,也可popupwindow,不过我这里为了学习一下样式为dialog的activity,便采用了这种方式,最后效果如下

看上去就像一个dialog,其实是一个activity,然后在这里根据用户的选择,来设置不同的主题,然后拿到主题的类型之后,在代码中根据这个值去判断应该显示哪个主题,相关代码如下

代码语言:javascript复制
// 主题设置
string_theme = sharedPreferences.getString("theme_select", "blue");
if (string_theme.equals("blue")) {
      setTheme(R.style.Theme_blue);
} else if (string_theme.equals("purple")) {
      setTheme(R.style.Theme_purple);
} else if (string_theme.equals("green")) {
      setTheme(R.style.Theme_green);
} else {
      setTheme(R.style.Theme_red);
}
setContentView(R.layout.activity_main);

记住一定要在setContentView方法之前调用,具体细节各方面由于代码比较散,不方便贴,可以去源码里看我是怎么设置的,最终四个主题下的主界面效果如下

当然这个APP里,还有很多其他的细节,诸如,控制当前播放的列表项为不同颜色,顶部显示歌曲名字的彩色TextView等,这些可以直接去看源码,实现的方法也不难,欢迎访问源码!!

源码下载

源码下载

由于考虑到大家可能没有积分,我把源码重新传到了百度云,这样大家可以免费下载学习,链接和提取码如下:

链接: https://pan.baidu.com/s/1KNxJvsE6XTIi3JkEBgCNgw 提取码: 4xhi

任何问题,也可以加我vx交流:hqq_0711

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/153231.html原文链接:https://javaforall.cn

0 人点赞