新建工程(Android Studio)
略。
添加、编辑资源文件
- 编辑
res
->values
->strings.xml
文件,内容如下:
<resources>
<string name="app_name">MusicBox</string>
<string name="title_string">本地音乐</string>
<string name="menu_about">关于</string>
<string name="menu_exit">退出</string>
<string name="btn_confirm">确认</string>
<string name="btn_cancel">取消</string>
<string name="str_warning">警告</string>
<string name="menu_detail">文件详情</string>
<string name="menu_play">开始播放</string>
<string name="str_playing">歌曲已经在播放了</string>
</resources>
- 在
AndroidManifest.xml
文件中添加读取外部储存卡的权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
- 添加menu资源目录,并在其中新建一个
menu_item.xml
文件,作为选项菜单
的布局文件。
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/item_about"
android:alphabeticShortcut="r"
android:icon="@drawable/ic_launcher_foreground"
android:title="@string/menu_about"/>
<item
android:id="@+id/item_exit"
android:alphabeticShortcut="d"
android:icon="@drawable/ic_launcher_foreground"
android:title="@string/menu_exit"/>
</menu>
- 更改程序图标。(此过程简单,略)
添加、编辑布局文件
- 编辑
activity_main.xml
(主界面布局文件),内容如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#dfebe8"
android:orientation="vertical">
<TextView
android:id="@+id/music_list_title"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginBottom="6dp"
android:text="@string/title_string"
android:textSize="16dp"
android:paddingLeft="10dp"
android:gravity="center_vertical"
android:background="#e9faf6"/>
<ListView
android:id="@+id/music_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="70dp"
android:orientation="horizontal"
android:background="#e9faf6">
<ImageView
android:id="@+id/music_thumb"
android:layout_width="70dp"
android:layout_height="70dp"
android:padding="4dp"
android:src="@drawable/gnote"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/music_name"
android:layout_width="match_parent"
android:layout_height="25dp"
android:textSize="14sp"
android:textColor="#0b9900"
android:paddingLeft="5dp"
android:gravity="bottom"
android:singleLine="true"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="45dp"
android:orientation="horizontal">
<android.support.v7.widget.AppCompatSeekBar
android:id="@+id/music_seek_bar"
android:layout_width="0dp"
android:layout_height="15dp"
android:layout_weight="1"
android:layout_marginTop="10dp"/>
<ImageView
android:id="@+id/btn_previous"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/previous"/>
<ImageView
android:id="@+id/btn_play"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/play"/>
<ImageView
android:id="@+id/btn_next"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/next"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
- 添加
item_layout.xml
作为ListView
每个item
的布局文件:
<?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="70dp"
android:padding="5dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/rand_icon"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="2dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginLeft="6dp"
android:orientation="vertical">
<TextView
android:id="@+id/item_music_name"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.5"
android:textSize="18sp"
android:gravity="center_vertical"
android:singleLine="true"/>
<TextView
android:id="@+id/item_music_singer"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:textSize="16sp"
android:gravity="center_vertical"
android:singleLine="true"/>
</LinearLayout>
</LinearLayout>
添加音乐信息类
在工程中新建一个类MusicInfo
用于记录音乐文件的信息:
package com.zys.musicbox;
public class MusicInfo {
private String music_title;
private String music_name;
private String music_path;
private String music_artist;
private int music_duration;
//getter and setter
}
添加MusicUtils类
在工程中新建一个类MusicUtils
用于操作音乐文件信息(此例中只有一个方法,利用ContentResolver
类读取音乐信息):
package com.zys.musicbox;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.provider.MediaStore;
import java.util.ArrayList;
import java.util.List;
public class MusicUtils {
/*
* 用于获取本地Music目录下的所有音乐信息,并封装成List后返回
* 需要一个Context对象
* */
public static List<MusicInfo> ResolveMusicToList(Context context){
String selection = MediaStore.Audio.Media.IS_MUSIC + "!=0";
String sortOrder = MediaStore.MediaColumns.DISPLAY_NAME+"";
List<MusicInfo> musicList = new ArrayList<MusicInfo>();
String[] projection = {
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.DATA,
MediaStore.Audio.Media.DURATION
};
ContentResolver contentResolver = context.getContentResolver();
Cursor cursor = contentResolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection,selection,null,sortOrder);
if (cursor != null){
for (cursor.moveToFirst(); cursor.isAfterLast() != true; cursor.moveToNext()){
MusicInfo musicInfo = new MusicInfo();
musicInfo.setMusic_title(cursor.getString(0));
musicInfo.setMusic_artist(cursor.getString(1));
musicInfo.setMusic_name(cursor.getString(2));
musicInfo.setMusic_path(cursor.getString(3));
musicInfo.setMusic_duration(Integer.parseInt(cursor.getString(4)));
musicList.add(musicInfo);
}
}
return musicList;
}
}
添加ListView适配器类
在工程中新建一个类ListAdapter
作为主界面中ListView的适配器类。
package com.zys.musicbox;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
class ViewHolder{
public ImageView itemIcon;
public TextView itemMusicName;
public TextView itemMusicSinger;
public int defaultTextColor;
View itemView;
public ViewHolder(View itemView) {
if (itemView == null){
throw new IllegalArgumentException("item View can not be null!");
}
this.itemView = itemView;
itemIcon = itemView.findViewById(R.id.rand_icon);
itemMusicName = itemView.findViewById(R.id.item_music_name);
itemMusicSinger = itemView.findViewById(R.id.item_music_singer);
defaultTextColor = itemMusicName.getCurrentTextColor();
}
}
public class ListAdapter extends BaseAdapter {
private List<MusicInfo> musicList;
private LayoutInflater layoutInflater;
private Context context;
private int currentPos = -1;
private ViewHolder holder = null;
public ListAdapter(Context context,List<MusicInfo> musicList) {
this.musicList = musicList;
this.context = context;
layoutInflater = LayoutInflater.from(context);
}
public void setFocusItemPos(int pos){
currentPos = pos;
notifyDataSetChanged();
}
@Override
public int getCount() {
return musicList.size();
}
@Override
public Object getItem(int position) {
return musicList.get(position).getMusic_title();
}
@Override
public long getItemId(int position) {
return position;
}
public void remove(int index){
musicList.remove(index);
}
public void refreshDataSet(){
notifyDataSetChanged();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null){
convertView = layoutInflater.inflate(R.layout.item_layout,null);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
}
else {
holder = (ViewHolder)convertView.getTag();
}
//如果是正在播放的音乐 就改变图片、字体颜色
if (position == currentPos){
holder.itemIcon.setImageBitmap(BitmapFactory.decodeResource(
context.getResources(),R.drawable.arrow));
holder.itemMusicName.setTextColor(Color.RED);
holder.itemMusicSinger.setTextColor(Color.RED);
}
//否则使用默认图片、字体颜色
else{
holder.itemIcon.setImageBitmap(BitmapFactory.decodeResource(
context.getResources(),R.drawable.music));
holder.itemMusicName.setTextColor(holder.defaultTextColor);
holder.itemMusicSinger.setTextColor(holder.defaultTextColor);
}
holder.itemMusicName.setText(musicList.get(position).getMusic_title());
holder.itemMusicSinger.setText(musicList.get(position).getMusic_artist());
return convertView;
}
}
添加接口MyBinderInterface
在工程中新建一个接口MyBinderInterface
作为调用Service中方法的媒介、中间人:
package com.zys.musicbox;
public interface MyBinderInterface {
//暂停
void Pause();
//恢复
void Resume();
//播放
void Play();
//播放下一首
void PlayNext();
//播放上一首
void PlayPrev();
//释放
void Release();
//是否正在播
boolean isPlaying();
//获取时长
int getDuration();
//当前位置
int getCurrentPosition();
//拖动位置
void seekTo(int length);
//获取当前索引
int getCurrentIndex();
//设置当前索引
void setCurrentIndex(int currentIdx);
}
MusicService服务类
package com.zys.musicbox;
import android.app.Service;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import java.io.IOException;
import java.util.List;
public class MusicService extends Service {
private MediaPlayer mPlayer;
private int seekLength = 0;
private int currentIndex = -1;
private List<MusicInfo> musicList;
@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}
@Override
public void onCreate() {
super.onCreate();
InitPlayer();
//通过工具类MusicUtils获取音乐信息列表
musicList = MusicUtils.ResolveMusicToList(getApplicationContext());
}
private void InitPlayer() {
mPlayer = new MediaPlayer();
mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
}
private class MyBinder extends Binder implements MyBinderInterface{
@Override
public void Pause() {
if (mPlayer.isPlaying()){
mPlayer.pause();
seekLength = mPlayer.getCurrentPosition();
}
}
@Override
public void Resume() {
mPlayer.seekTo(seekLength);
mPlayer.start();
}
@Override
public void Play() {
mPlayer.reset();
Uri path = Uri.parse(musicList.get(currentIndex).getMusic_path());
try {
mPlayer.setDataSource(String.valueOf(path));
mPlayer.prepare();
} catch (IOException e) {
e.printStackTrace();
}
mPlayer.seekTo(seekLength);
mPlayer.start();
}
@Override
public void PlayNext() {
currentIndex += 1;
if (currentIndex >= musicList.size()){
currentIndex = 0;
}
seekLength = 0;
Play();
}
@Override
public void PlayPrev() {
currentIndex -= 1;
if (currentIndex <= 0){
currentIndex = musicList.size() - 1;
}
seekLength = 0;
Play();
}
@Override
public void Release() {
mPlayer.reset();
mPlayer.stop();
mPlayer.release();
}
@Override
public boolean isPlaying() {
return mPlayer.isPlaying();
}
@Override
public int getDuration() {
return mPlayer.getDuration();
}
@Override
public int getCurrentPosition() {
return mPlayer.getCurrentPosition();
}
@Override
public void seekTo(int length) {
seekLength = length;
mPlayer.seekTo(length);
}
@Override
public int getCurrentIndex() {
return currentIndex;
}
@Override
public void setCurrentIndex(int currentIdx) {
currentIndex = currentIdx;
}
}
}
注册服务
修改AndroidManifest.xml
文件,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zys.musicbox">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 注册服务 -->
<service android:name=".MusicService"></service>
</application>
</manifest>
主要是注册服务那一行代码。
编辑MainActivity.java文件
package com.zys.musicbox;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private List<MusicInfo> musicList = new ArrayList<MusicInfo>();
private ListAdapter mListAdapter;
private ListView mListView;
//记录长按的列表项坐标
private int currentSel;
//按钮
private ImageView btnPrevious;
private ImageView btnNext;
private ImageView btnPlay;
//文本
private TextView listTitle;
private TextView playingName;
//进度条
private SeekBar musicSeekBar;
//自定义Binder对象 用于调用服务中的方法
private MyBinderInterface myBinder;
//自定义服务连接对象
private MyServiceConnection conn;
//是否正在播放
private boolean isPlaying = false;
private Handler handler = new Handler();
//更新线程用于更新进度条
private Runnable updateThread = new Runnable() {
@Override
public void run() {
if (myBinder != null){
try {
if (myBinder.isPlaying()){
int duration = myBinder.getDuration();
int currentPos = myBinder.getCurrentPosition();
musicSeekBar.setMax(duration);
musicSeekBar.setProgress(currentPos);
int prg_sec = currentPos/1000;
int max_sec = duration/1000;
if (prg_sec == max_sec){
myBinder.PlayNext();
updateState();
}
}
}catch (Exception e){
e.printStackTrace();
}
}
handler.post(updateThread);
}
};
//定义服务连接
private class MyServiceConnection implements ServiceConnection{
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
myBinder = (MyBinderInterface)service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
//更新播放状态
private void updateState() {
int index = myBinder.getCurrentIndex();
mListAdapter.setFocusItemPos(index);
String currentMusicName = musicList.get(index).getMusic_title();
playingName.setText(currentMusicName);
btnPlay.setImageBitmap(BitmapFactory.decodeResource(getResources(),R.drawable.pause));
isPlaying = true;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//通过工具类MusicUtils获取音乐信息列表
musicList = MusicUtils.ResolveMusicToList(getApplicationContext());
//获取视图
initView();
//设置列表标题
String title = getResources().getString(R.string.title_string).toString();
title += "(总数:"+ musicList.size() + ")";
listTitle.setText(title);
//为mListView注册上下文菜单
registerForContextMenu(mListView);
conn = new MyServiceConnection();
//绑定服务
bindService(new Intent(this,MusicService.class),conn, Context.BIND_AUTO_CREATE);
handler.post(updateThread);
}
//初始化视图
private void initView(){
listTitle = (TextView)findViewById(R.id.music_list_title);
playingName = (TextView)findViewById(R.id.music_name);
musicSeekBar = (SeekBar)findViewById(R.id.music_seek_bar);
btnPrevious = (ImageView)findViewById(R.id.btn_previous);
btnNext = (ImageView)findViewById(R.id.btn_next);
btnPlay = (ImageView)findViewById(R.id.btn_play);
mListView = (ListView)findViewById(R.id.music_list);
mListAdapter = new ListAdapter(MainActivity.this,musicList);
mListView.setAdapter(mListAdapter);
setListener();
}
//设置监听事件
private void setListener(){
mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
mListView.showContextMenu();
currentSel = position;
return true;
}
});
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
myBinder.setCurrentIndex(position);
myBinder.Play();
mListAdapter.setFocusItemPos(position);
updateState();
}
});
btnPrevious.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (isPlaying == true){
myBinder.PlayPrev();
updateState();
}
}
});
btnNext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (isPlaying == true){
myBinder.PlayNext();
updateState();
}
}
});
btnPlay.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (isPlaying == true){
btnPlay.setImageBitmap(BitmapFactory.decodeResource(
getResources(),R.drawable.play));
isPlaying = false;
myBinder.Pause();
return;
}
if (isPlaying == false){
if (myBinder.getCurrentIndex() == -1){
myBinder.setCurrentIndex(0);
mListAdapter.setFocusItemPos(0);
myBinder.Play();
updateState();
}
btnPlay.setImageBitmap(BitmapFactory.decodeResource(
getResources(),R.drawable.pause));
isPlaying = true;
myBinder.Resume();
}
}
});
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) {
if (myBinder != null){
try {
myBinder.seekTo(seekBar.getProgress());
}catch (Exception e){
e.printStackTrace();
}
}
}
});
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo){
menu.add(0,0,0,R.string.menu_detail);
menu.add(0,1,1,R.string.menu_play);
super.onCreateContextMenu(menu,view,menuInfo);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()){
case 0:
StringBuilder msgBuilder = new StringBuilder();
msgBuilder.append("文件名:" + musicList.get(currentSel).getMusic_name() + "\n");
msgBuilder.append("路 径:" + musicList.get(currentSel).getMusic_path() + "\n");
msgBuilder.append("时 长:" + musicList.get(currentSel).getMusic_duration()/1000 + " s\n");
String title = "文件详情";
new AlertDialog.Builder(MainActivity.this)
.setIcon(R.drawable.note)
.setTitle(title)
.setMessage(msgBuilder.toString())
.setPositiveButton(R.string.btn_confirm, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}}).create().show();
break;
case 1:
//不处于播放状态 或者 选择的歌曲和正在播放的歌曲不是同一首 则更新状态且播放
if (isPlaying == false || currentSel != myBinder.getCurrentIndex()){
myBinder.setCurrentIndex(currentSel);
updateState();
myBinder.Play();
}
//提示选择的歌曲已经在播放了
else{
Toast.makeText(MainActivity.this,R.string.str_playing,Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
return super.onContextItemSelected(item);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
boolean retValue = super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.menu_item,menu);
return retValue;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.item_about){
StringBuilder msgBuilder = new StringBuilder();
msgBuilder.append("MusicBox V1.0.0\n");
msgBuilder.append("作者:Leo_Elegant\n");
msgBuilder.append("(C) 2019 ......");
String title = "关于";
new AlertDialog.Builder(MainActivity.this)
.setIcon(R.drawable.note)
.setTitle(title)
.setMessage(msgBuilder.toString())
.setPositiveButton(R.string.btn_confirm, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}}).create().show();
}
if (item.getItemId() == R.id.item_exit){
onBackPressed();
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
String title = "提示";
new AlertDialog.Builder(MainActivity.this)
.setIcon(R.drawable.note)
.setTitle(title)
.setMessage("确定要退出吗?")
.setPositiveButton(R.string.btn_confirm, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
myBinder.Release();
finish();
}
})
.setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
}).create().show();
}
}
测试、运行
- 先
冷启动
模拟器,并上传几首音乐到SD
卡的Music
目录下。 - 在启动程序后会闪退,给程序设好权限。
- ==先打开模拟器自带的音乐播放软件,帮助我们索引音乐文件并存入数据库==,之后我们的程序在读取的到。
- 启动界面,并点击播放按钮:
- 下一首:
- 选项菜单
- 选项菜单“关于”
- 长按歌曲弹出上下文菜单
- 上下文菜单“文件详情”
注:此例子部分内容为《移动软件开发》课程老师布置实验。
老师博客:http://www.wanlizhong.com/