项目中用到了MediaPlayer播放音频,趁这两天比较闲,试着写了一个音频播放器,还有很多不完善,仅当练手。实现了播放、暂停、退出,上下切歌,扫描音频文件并展示,点击音频列表可播放,打开指定音频文件播放的功能,不过功能之间并没有做特殊处理,所以多个功能之间使用会出现崩溃(笑哭)。下面是效果图:
已知问题:
1. 目前打开目录查找文件功能在PAD(安卓5.1)上可以,手机上(安卓8.0)上会崩溃。
2. 这个布局主要是针对手头上的PAD,对手机布局没有优化,同时布局也比较丑,但是这次Demo主要以功能练习为主,所以布局上也不再优化。
3. 因此Demo仅用于MediaPlayer学习,目前所做内容已超过预期。加之下周需要述职,没有精力花费在此,故代码逻辑并不严谨,封装也不好, 留待后期优化。
可优化:
后期打算增加播放时当前播放歌曲左侧列表高亮。
UI布局优化(不可能的,就是如此粗犷的人)
下面贴代码:
MainActivity.java,基本功能都是在此完成。
package com.example.qxb_810.audioplaydemo;
import android.Manifest;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.provider.MediaStore;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import com.example.qxb_810.bean.ScanFile;
import com.example.qxb_810.utils.GetPathFromUri4kitkat;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import butterknife.BindView;
import butterknife.ButterKnife;
/**
* author XXX
* email [email protected]
* create 2018/9/5 19:08
* desc 音乐播放 -- mediaPlayer的使用
*/
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@BindView(R.id.tv_play)
TextView tvPlay;
@BindView(R.id.tv_resume)
TextView tvResume;
@BindView(R.id.tv_stop)
TextView tvStop;
@BindView(R.id.sb_progress)
SeekBar sbProgress;
@BindView(R.id.tv_currprogress)
TextView tvCurrprogress;
@BindView(R.id.tv_maxprogress)
TextView tvMaxprogress;
@BindView(R.id.iv_pic)
ImageView ivPic;
@BindView(R.id.tv_find_file)
TextView tvFindFile;
@BindView(R.id.tv_scan_file)
TextView tvScanFile;
@BindView(R.id.tv_last)
TextView tvLast;
@BindView(R.id.tv_next)
TextView tvNext;
@BindView(R.id.tv_title)
TextView tvTitle;
@BindView(R.id.tv_artist)
TextView tvArtist;
@BindView(R.id.rv_file_list)
RecyclerView rvFileList;
private MediaPlayer mMediaPlayer;
private int mDuration = 0;
private boolean isMusicLoadSuccess = false;
private boolean isPlayPause = false;
private String mPath;
private int mCurrPostion = 0;
private MyAdapter myAdapter;
private ObjectAnimator animator;
private List fileList = new ArrayList();
private Set filePathSet = new HashSet();
private List fileNameList = new ArrayList();
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 123) {
sbProgress.setProgress(mMediaPlayer.getCurrentPosition());
tvCurrprogress.setText(formatDuring(mMediaPlayer.getCurrentPosition()));
mHandler.sendEmptyMessage(123);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
mPath = null;
try {
requestPermissions();
initMediaPlayer();
initRecyclerView();
initEvent();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 动态申请权限 ---- API23后需要动态申请权限
*/
private void requestPermissions() {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
// 读写存储权限 和联网权限
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.INTERNET}, 1);
}
}
/**
* 初始化RecyclerView
*/
private void initRecyclerView() {
rvFileList.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
myAdapter = new MyAdapter(fileList, this);
rvFileList.setAdapter(myAdapter);
}
/**
* 初始化MediaPlayer
*
* @throws IOException
*/
private void initMediaPlayer() throws IOException {
mMediaPlayer = new MediaPlayer();
if (mPath != null && !mPath.equals("")) {
mMediaPlayer.setLooping(false); // 设置单曲循环
mMediaPlayer.setDataSource(mPath);
mMediaPlayer.prepareAsync(); // 异步加载音频
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
// 异步加载结束后再回调
isMusicLoadSuccess = true;
mDuration = mMediaPlayer.getDuration();
tvMaxprogress.setText(formatDuring(mDuration));
sbProgress.setMax(mDuration); // 设置进度条最大值
}
});
}
animator = ObjectAnimator.ofFloat(ivPic, "rotation", 360.0f);
animator.setDuration(10000); // 设置转速,越大越慢
animator.setInterpolator(new LinearInterpolator());
animator.setRepeatCount(-1);
ivPic.setImageBitmap(null);
if (getAudioPic() != null) {
ivPic.setImageBitmap(getAudioPic());
}
ivPic.setBackgroundResource(R.drawable.shape_default_pic);
}
/**
* 初始化事件
*/
private void initEvent() {
tvPlay.setOnClickListener(this);
tvResume.setOnClickListener(this);
tvStop.setOnClickListener(this);
tvFindFile.setOnClickListener(this);
tvScanFile.setOnClickListener(this);
tvLast.setOnClickListener(this);
tvNext.setOnClickListener(this);
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
changeMusic(++mCurrPostion);
}
});
sbProgress.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
tvCurrprogress.setText(formatDuring(seekBar.getProgress()));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
mMediaPlayer.seekTo(seekBar.getProgress());
}
});
}
/**
* 获取音乐文件图片
*
* @return
*/
public Bitmap getAudioPic() {
try {
Uri selectedAudio = Uri.parse(mPath);
MediaMetadataRetriever myRetriever = new MediaMetadataRetriever();
myRetriever.setDataSource(this, selectedAudio); // the URI of audio file
byte[] artwork;
artwork = myRetriever.getEmbeddedPicture();
if (artwork != null) {
return getCircleBitmap(BitmapFactory.decodeByteArray(artwork, 0, artwork.length));
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取圆形图片
*
* @param bitmap
* @return
*/
private Bitmap getCircleBitmap(Bitmap bitmap) {
Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
bitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
Paint paint = new Paint();
paint.setAntiAlias(true);
canvas.drawCircle(bitmap.getWidth() / 2, bitmap.getHeight() / 2, bitmap.getHeight() / 2, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmap, rect, rect, paint);//将图片画出来
return output;
}
/**
* 扫描音乐文件信息
*
* @return
*/
public void scanAudioFile() {
fileList.clear();
filePathSet.clear();
fileNameList.clear();
Cursor cursor = this.getContentResolver().query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null,
MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
for (int i = 0; i < cursor.getCount(); i++) {
cursor.moveToNext();
int id = (int) cursor.getLong(cursor
.getColumnIndex(MediaStore.Audio.Media._ID)); //音乐id
String title = cursor.getString((cursor
.getColumnIndex(MediaStore.Audio.Media.TITLE))); // 音乐标题
String artist = cursor.getString(cursor
.getColumnIndex(MediaStore.Audio.Media.ARTIST)); // 艺术家
String album = cursor.getString(cursor
.getColumnIndex(MediaStore.Audio.Media.ALBUM)); //专辑
String displayName = cursor.getString(cursor
.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME));
int albumId = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID));
int duration = (int) cursor.getLong(cursor
.getColumnIndex(MediaStore.Audio.Media.DURATION)); // 时长
long size = cursor.getLong(cursor
.getColumnIndex(MediaStore.Audio.Media.SIZE)); // 文件大小
String url = cursor.getString(cursor
.getColumnIndex(MediaStore.Audio.Media.DATA)); // 文件路径
int isMusic = cursor.getInt(cursor
.getColumnIndex(MediaStore.Audio.Media.IS_MUSIC)); // 是否为音乐
if (1 == isMusic && duration > 1000 * 60 && !filePathSet.contains(url)) { // 大于1min的音乐文件且未添加过
ScanFile file = new ScanFile(id, title, artist, album, displayName, url, albumId, duration, size, isMusic);
filePathSet.add(file.getUrl());
fileNameList.add(file.getTitle());
fileList.add(file);
}
}
myAdapter.notifyDataSetChanged();
Log.e("list", fileList.toString());
}
/**
* 切换歌曲
*
* @param postion
*/
private void changeMusic(int postion) {
if (fileList.size() > 0) {
mCurrPostion = 0;
sbProgress.setProgress(0);
mHandler.removeMessages(123);
// 播放结束
mMediaPlayer.stop();
if (postion >= fileList.size()) {
postion = 0;
} else if (postion <= 0) {
postion = fileList.size() - 1;
}
tvArtist.setText("演唱者: " + fileList.get(postion).getArtist());
tvTitle.setText("歌曲名: " + fileList.get(postion).getTitle());
ScanFile scanFile = fileList.get(mCurrPostion = postion);
mPath = scanFile.getUrl();
try {
initMediaPlayer();
} catch (IOException e) {
e.printStackTrace();
}
} else {
Toast.makeText(getApplicationContext(), "暂无音乐,请扫描...", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv_play:
if (isMusicLoadSuccess) {
if (mMediaPlayer.isPlaying()) {
Toast.makeText(getApplicationContext(), "播放中...", Toast.LENGTH_SHORT).show();
} else {
if (fileList.size() == 0) {
Toast.makeText(getApplicationContext(), "暂无音乐,请扫描...", Toast.LENGTH_SHORT).show();
} else {
mMediaPlayer.start();
animator.start();
mHandler.sendEmptyMessage(123);
}
}
} else {
Toast.makeText(getApplicationContext(), fileList.size() > 0 ? "加载中..." : "暂无音乐,请扫描...", Toast.LENGTH_SHORT).show();
}
break;
case R.id.tv_resume:
if (!isPlayPause) {
if (mMediaPlayer.isPlaying()) {
isPlayPause = true;
mDuration = mMediaPlayer.getCurrentPosition();
mMediaPlayer.pause();
animator.pause();
tvResume.setText("继续");
} else {
Toast.makeText(getApplicationContext(), "还未播放...", Toast.LENGTH_SHORT).show();
}
} else {
isPlayPause = false;
mMediaPlayer.start();
animator.resume();
tvResume.setText("暂停");
}
break;
case R.id.tv_stop:
mDuration = 0;
sbProgress.setProgress(0);
mMediaPlayer.reset();
mMediaPlayer.stop();
animator.cancel();
mHandler.removeMessages(123);
break;
case R.id.tv_find_file:
File parentFlie = new File(Environment.getExternalStorageDirectory().getAbsolutePath());
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setDataAndType(Uri.fromFile(parentFlie), "*/*");
// intent.setDataAndType(FileProvider.getUriForFile(getApplicationContext(), "com.example.qxb_810.fileprovider", parentFlie), "*/*"); // 设置打开文件类型,这么没设置类型
// intent.setType(“image/*”);
//intent.setType(“audio/*”); //选择音频
//intent.setType(“video/*”); //选择视频 (mp4 3gp 是android支持的视频格式)
//intent.setType(“video/*;image/*”);//同时选择视频和图片
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, 111);
break;
case R.id.tv_scan_file:
Toast.makeText(getApplicationContext(), "扫描中...", Toast.LENGTH_SHORT).show();
scanAudioFile();
Toast.makeText(getApplicationContext(), "扫描结束,共扫描到" + fileList.size() + "个音频...", Toast.LENGTH_SHORT).show();
break;
case R.id.tv_last:
changeMusic(--mCurrPostion);
break;
case R.id.tv_next:
changeMusic(++mCurrPostion);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK) {//是否选择,没选择就不会继续
Uri uri = data.getData();//得到uri,后面就是将uri转化成file的过程。
mPath = GetPathFromUri4kitkat.getPath(this, uri);
try {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.release();
animator.cancel();
isMusicLoadSuccess = false;
initMediaPlayer();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 换算时间
*
* @param time 时间
* @return
*/
public static String formatDuring(long time) {
long minutes = (time % (1000 * 60 * 60)) / (1000 * 60);
long seconds = (time % (1000 * 60)) / 1000;
String min = minutes < 10 ? "0" + minutes : minutes + "";
String sec = seconds < 10 ? "0" + seconds : seconds + "";
return min + ":" + sec;
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeMessages(123);
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.release();
}
}
public class MyAdapter extends RecyclerView.Adapter {
private List list = new ArrayList<>();
private Context context;
public MyAdapter(List list, Context context) {
this.list = list;
this.context = context;
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.simple_adapter_item, parent, false);
MyViewHolder viewHolder = new MyViewHolder(view);
return viewHolder;
}
@Override
public void onBindViewHolder(final MyViewHolder holder, final int position) {
holder.tvTitle.setText(list.get(position).getTitle());
holder.llContent.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
changeMusic(position);
mHandler.sendEmptyMessage(123);
mMediaPlayer.start();
}
});
}
@Override
public int getItemCount() {
return list.size();
}
class MyViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.tv_title)
TextView tvTitle;
@BindView(R.id.ll_content)
LinearLayout llContent;
public MyViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
}
}
MainActivity布局,没错,如此丑陋的布局就是下面这个文件:
GetPathFromUri4kitkat类,这个是从网上找的,因为需要转相应路径
package com.example.qxb_810.utils;
import android.annotation.SuppressLint;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
public class GetPathFromUri4kitkat {
/**
* 专为Android4.4设计的从Uri获取文件绝对路径,以前的方法已不好使
*/
@SuppressLint("NewApi")
public static String getPath(final Context context, final Uri uri) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
// TODO handle non-primary volumes
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[]{split[1]};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {column};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
}
ScanFile — bean类
package com.example.qxb_810.bean;
/**
* author XXX
* email [email protected]
* create 2018/9/6 14:23
* desc 音乐bean
*/
public class ScanFile {
private int id;
private String title;
private String artist;
private String album;
private String displayName;
private String url;
private int albumId;
private int duration;
private long size;
private int isMusic;
public ScanFile() {
}
public ScanFile(int id, String title, String artist, String album, String displayName, String url, int albumId, int duration, long size, int isMusic) {
this.id = id;
this.title = title;
this.artist = artist;
this.album = album;
this.displayName = displayName;
this.url = url;
this.albumId = albumId;
this.duration = duration;
this.size = size;
this.isMusic = isMusic;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getArtist() {
return artist;
}
public void setArtist(String artist) {
this.artist = artist;
}
public String getAlbum() {
return album;
}
public void setAlbum(String album) {
this.album = album;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getAlbumId() {
return albumId;
}
public void setAlbumId(int albumId) {
this.albumId = albumId;
}
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;
}
public int getIsMusic() {
return isMusic;
}
public void setIsMusic(int isMusic) {
this.isMusic = isMusic;
}
@Override
public String toString() {
return "ScanFile{" +
"id=" + id +
", title='" + title + '\'' +
", artist='" + artist + '\'' +
", album='" + album + '\'' +
", displayName='" + displayName + '\'' +
", url='" + url + '\'' +
", albumId=" + albumId +
", duration=" + duration +
", size=" + size +
", isMusic=" + isMusic +
'}';
}
}
接下来就是一些简单的布局文件
shape_default_pic
simple_adapter_item
OK,项目大致就是这些文件。作为第一个做出来的稍微像样点的小Demo,代码里面仍有很多不足需要改进,等下周述职结束后再进行优化。