这个是实习的时候做的了,当时看了半本第一行代码就去上班,Java 都没学过,最后实习走的时候还做了挺多东西,这个音频播放器就是其一,现在写个博客纪念一下上班两年了,虽然还是菜狗程序员!
前言
实习的时候很菜,一直追求使用开源库,这个播放器也用到了几个有名的库,也不算强凑的吧。
- Glide
- ButterKnife
- BaseRecyclerViewAdapterHelper
效果图我也不想放了,很 low,但能用,就自己记录下,如果希望借鉴的话继续看吧!
代码
- AudioPlayerActivity
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CircleCrop;
import com.bumptech.glide.request.RequestOptions;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
/**
* 低仿网易云播放器
* @author :Silence
* @date:2019/5/21 16:53
*/
@SuppressWarnings("unused")
@SuppressLint("NonConstantResourceId")
public class AudioPlayerActivity extends AppCompatActivity {
/**
* 列表循环播放模式
*/
public static final int PLAY_MOD_LOOP = 0;
/**
* 单曲循环播放模式
*/
public static final int PLAY_MOD_SINGLE = 1;
/**
* 随机播放模式
*/
public static final int PLAY_MOD_RANDOM = 2;
/**
* 退出 activity
*/
@OnClick(R.id.play_audio_back)
public void backClick(){ finish(); }
/**
* 标题
*/
@BindView(R.id.play_audio_title)
TextView textTitle;
/**
* 中心图片 ImageView,可以获取到音乐的封面放这,靠Glide加载,及属性动画转起来
*/
@BindView(R.id.play_audio_center)
ImageView imageCenter;
/**
* 上一首
*/
@BindView(R.id.last_audio)
ImageView lastAudio;
@OnClick(R.id.last_audio)
public void last(){
switch (playbackMode){
case PLAY_MOD_LOOP:
case PLAY_MOD_SINGLE:
nowPosition--;
if (nowPosition < 0) {
//跳转循环
nowPosition = mediaItems.size() - 1;
}
//播放
startPlay();
//选中歌曲变色
audioAdapter.setSelect(nowPosition);
audioAdapter.notifyDataSetChanged();
break;
case PLAY_MOD_RANDOM:
nowPosition = (int) (Math.random()*mediaItems.size());
startPlay();
//选中歌曲变色
audioAdapter.setSelect(nowPosition);
audioAdapter.notifyDataSetChanged();
break;
default:
}
}
/**
* 播放或者暂停
*/
@BindView(R.id.startOrPause)
ImageView startOrPause;
@OnClick(R.id.startOrPause)
public void pause(){
if (mediaPlayer.isPlaying()) {
//中间图片暂停旋转
mAnimator.pause();
mediaPlayer.pause();
startOrPause.setImageResource(R.drawable.ic_pic_pause);
} else {
//中间图片继续旋转
mAnimator.resume();
mediaPlayer.start();
startOrPause.setImageResource(R.drawable.ic_pic_play);
}
}
/**
* 下一首
*/
@BindView(R.id.next_audio)
ImageView nextAudio;
@OnClick(R.id.next_audio)
public void next(){
switch (playbackMode){
case PLAY_MOD_LOOP:
case PLAY_MOD_SINGLE:
nowPosition++;
//跳转循环
if (nowPosition > (mediaItems.size() - 1)) {
nowPosition = 0;
}
//播放
startPlay();
//选中歌曲变色
audioAdapter.setSelect(nowPosition);
audioAdapter.notifyDataSetChanged();
break;
case PLAY_MOD_RANDOM:
//随机
nowPosition = (int) (Math.random()*mediaItems.size());
startPlay();
//选中歌曲变色
audioAdapter.setSelect(nowPosition);
audioAdapter.notifyDataSetChanged();
break;
default:
}
}
/**
* 进度条
*/
@BindView(R.id.seekBar)
SeekBar seekBar;
/**
* 歌曲时间
*/
@BindView(R.id.audio_time)
TextView textTime;
/**
* 播放模式,点击来回切换
*/
@BindView(R.id.playMode)
ImageView playMode;
@OnClick(R.id.playMode)
public void playMode(){
switch (playbackMode){
case PLAY_MOD_LOOP:
playbackMode = 1;
playMode.setImageResource(R.drawable.pic_playmod_signle);
break;
case PLAY_MOD_SINGLE:
playbackMode = 2;
playMode.setImageResource(R.drawable.pic_playmod_random);
break;
case PLAY_MOD_RANDOM:
playbackMode = 0;
playMode.setImageResource(R.drawable.pic_playmod_loop);
break;
default:
}
}
/**
* 列表相关,仅文字列表,但和音频对应
*/
@BindView(R.id.audioList)
RecyclerView audioList;
AudioPlayListAdapter audioAdapter;
List<String> data = new ArrayList<>();
/**
* 是否是网络资源
*/
boolean isNet = true;
/**
* 音频链接
*/
String link;
/**
* 列表中正在播放的位置
*/
private int nowPosition = 0;
/**
* 播放模式
*/
private int playbackMode = 0;
/**
* 音频列表
*/
List<Audio> mediaItems = new ArrayList<>();
/**
* 音频播放器
*/
MediaPlayer mediaPlayer;
/**
* 封面图片
*/
Bitmap albumBitmap;
/**
* 旋转动画
*/
ObjectAnimator mAnimator;
/**
* 处理 seekBar进度的重置和更新,要注意内存泄漏
*/
@SuppressLint("HandlerLeak")
Handler handler = new Handler(){
@SuppressLint("SetTextI18n")
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 0) {
//得到当前进度
int currentPosition = mediaPlayer.getCurrentPosition();
int duration = mediaPlayer.getDuration();
seekBar.setMax(duration);
//设置seekBar的进度
seekBar.setProgress(currentPosition);
textTime.setText(formatSeconds(currentPosition / 1000)
+ "/" + formatSeconds(duration / 1000));
//定时更新,1秒
removeMessages(0);
sendEmptyMessageDelayed(0, 1000);
}
}
};
/**
* 转换秒值为字符串
* @param seconds 秒
* @return 时分秒字符串
*/
private String formatSeconds(long seconds) {
int temp;
StringBuilder sb = new StringBuilder();
if (seconds > 3600) {
//时
temp = (int) (seconds / 3600);
sb.append((seconds / 3600) < 10 ? "0" + temp + ":" : temp + ":");
}
//分
temp = (int) (seconds % 3600 / 60);
sb.append((temp < 10) ? "0" + temp + ":" : "" + temp + ":");
//秒
temp = (int) (seconds % 3600 % 60);
sb.append((temp < 10) ? "0" + temp : "" + temp);
return sb.toString();
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.component_audio);
//全屏
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
ButterKnife.bind(this);
init();
//获取intent数据
getIntentData();
//启动播放
startPlay();
//设置歌曲列表适配器
initAdapter();
}
private void init(){
mediaPlayer = new MediaPlayer();
//将 seekBar和播放器联系起来
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
if (b) { mediaPlayer.seekTo(i); } }
@Override
public void onStartTrackingTouch(SeekBar seekBar) { }
@Override
public void onStopTrackingTouch(SeekBar seekBar) { }
});
}
/**
* 旋转图片
*/
private void rotateImage() {
//旋转动画
mAnimator = ObjectAnimator.ofFloat(imageCenter, "rotation", 0.0f, 360.0f);
//设定转一圈的时间
mAnimator.setDuration(20000);
//设定无限循环
mAnimator.setRepeatCount(Animation.INFINITE);
// 循环模式
mAnimator.setRepeatMode(ObjectAnimator.RESTART);
// 匀速
mAnimator.setInterpolator(new LinearInterpolator());
//开始动画
mAnimator.start();
}
private void getIntentData(){
Intent intent = getIntent();
isNet = intent.getBooleanExtra("isNet",false);
if(isNet) {
//网络链接
link = intent.getStringExtra("link");
}else {
if(null != intent.getExtras()) {
//noinspection unchecked
mediaItems = (List<Audio>) intent.getExtras().getSerializable("audioList");
}
//传入播放音乐在列表中位置
nowPosition = intent.getIntExtra("position", 0);
}
}
private void initAdapter() {
audioList.setHasFixedSize(true);
audioList.setLayoutManager(new LinearLayoutManager(this));
//从音频列表中获取音频文件列表
for (Audio audio : mediaItems){
data.add(audio.getName());
}
//创建列表适配器
audioAdapter = new AudioPlayListAdapter(R.layout.item_text_singleline,data);
//选中项
audioAdapter.setSelect(nowPosition);
//列表为空时布局
audioAdapter.setEmptyView(R.layout.view_empty_file, (ViewGroup) audioList.getParent());
//单击列表项切换音频
audioAdapter.setOnItemClickListener((adapter, view, position) -> {
audioAdapter.setSelect(nowPosition);
audioAdapter.notifyDataSetChanged();
nowPosition = position;
startPlay();
});
audioList.setAdapter(audioAdapter);
}
private void startPlay() {
// 释放之前音频
if (mediaPlayer != null) {
mediaPlayer.reset();
}
try {
assert mediaPlayer != null;
mediaPlayer.setDataSource(AudioPlayerActivity.this, Uri.parse(link));
mediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
if (isNet){
//解码
mediaPlayer.setOnPreparedListener(mediaPlayer -> {
mediaPlayer.start();
//加载默认图片
albumBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
Glide.with(AudioPlayerActivity.this).load(albumBitmap)
.apply(RequestOptions.bitmapTransform(new CircleCrop()))
.skipMemoryCache(true) // 不使用内存缓存
.diskCacheStrategy(DiskCacheStrategy.NONE) // 不使用磁盘缓存
.into(imageCenter);
handler.sendEmptyMessage(0);
});
//播放完成监听
mediaPlayer.setOnCompletionListener(mediaPlayer -> finish());
mediaPlayer.setOnErrorListener((mediaPlayer, i, i1) -> {
Toast.makeText(AudioPlayerActivity.this,"播放出错了",Toast.LENGTH_SHORT).show();
finish();
return true;
});
//网络资源关闭上下曲和播放模式
lastAudio.setVisibility(View.GONE);
nextAudio.setVisibility(View.GONE);
playMode.setVisibility(View.GONE);
}else{
//设置标题为音频文件名
textTitle.setText(mediaItems.get(nowPosition).getName());
//解码
mediaPlayer.setOnPreparedListener(mediaPlayer -> {
mediaPlayer.start();
//获取专辑封面图片
albumBitmap = MediaUtiles.getArtwork(AudioPlayerActivity.this,
mediaItems.get(nowPosition).getAudioId(),
mediaItems.get(nowPosition).getAlbumId(),true,false);
Glide.with(AudioPlayerActivity.this).load(albumBitmap)
.apply(RequestOptions.bitmapTransform(new CircleCrop()))
.skipMemoryCache(true) // 不使用内存缓存
.diskCacheStrategy(DiskCacheStrategy.NONE) // 不使用磁盘缓存
.into(imageCenter);
handler.sendEmptyMessage(0);
});
//播放完成监听
mediaPlayer.setOnCompletionListener(mediaPlayer -> next());
mediaPlayer.setOnErrorListener((mediaPlayer, i, i1) -> {
Toast.makeText(AudioPlayerActivity.this,"播放出错了",Toast.LENGTH_SHORT).show();
next();
return true;
});
//本地播放有循环模式
mediaPlayer.setLooping((playbackMode == PLAY_MOD_LOOP));
}
//旋转中间图片
rotateImage();
}
@Override
protected void onPause() {
super.onPause();
mediaPlayer.pause();
}
@Override
protected void onDestroy() {
mediaPlayer.stop();
mediaPlayer.release();
handler.removeCallbacksAndMessages(null);
super.onDestroy();
}
}
这里就是一个 activity,记得在 manifest 里面注册。最烦那些放代码不给引入库的,这个全给了吧,还有几个用到的类,下面写。
- XML 布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/whitesmoke"
android:orientation="vertical">
<RelativeLayout
android:background="@color/gray"
android:layout_width="match_parent"
android:layout_height="40dp">
<ImageView
android:id="@+id/play_audio_back"
android:layout_alignParentStart="true"
android:layout_marginStart="10dp"
android:scaleType="centerCrop"
android:src="@drawable/ic_pic_back"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
<TextView
android:id="@+id/play_audio_title"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:gravity="center"
android:text="音频播放" />
</RelativeLayout>
<ImageView
android:id="@+id/play_audio_center"
android:layout_centerInParent="true"
android:src="@drawable/pic_audio_default"
android:layout_width="200dp"
android:layout_height="200dp" />
<LinearLayout
android:background="@color/colorGray"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:gravity="center"
android:layout_height="50dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent">
<ImageView
android:id="@+id/last_audio"
android:layout_width="40dp"
android:layout_margin="5dp"
android:padding="5dp"
android:layout_height="match_parent"
android:src="@drawable/ic_pic_previous"/>
<ImageView
android:id="@+id/startOrPause"
android:layout_width="40dp"
android:layout_margin="5dp"
android:layout_height="match_parent"
android:src="@drawable/ic_pic_pause"/>
<ImageView
android:id="@+id/next_audio"
android:layout_width="40dp"
android:padding="5dp"
android:layout_margin="5dp"
android:layout_height="match_parent"
android:src="@drawable/ic_pic_next"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:gravity="center"
android:layout_marginEnd="3dp"
android:layout_height="match_parent">
<SeekBar
android:id="@+id/seekBar"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent" />
<TextView
android:id="@+id/audio_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#ffffff"
android:textSize="18sp"
android:text="00:00/00:00"/>
</LinearLayout>
<ImageView
android:id="@+id/playMode"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:src="@drawable/pic_playmod_loop"/>
</LinearLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/audioList"
android:layout_alignParentEnd="true"
android:layout_marginTop="40dp"
android:layout_marginBottom="50dp"
android:layout_width="200dp"
android:layout_height="match_parent"/>
</RelativeLayout>
图标、颜色、toolbar如果要用的话,自己改改吧,原谅我之前的菜!
- Audio
public class Audio implements Serializable {
private String name;
private Long duration;
private Long size;
private String address;
private String artist;
private Long audioId;
private Long albumId;
public Audio(String name, Long duration, Long size, String address, String artist) {
this.name = name;
this.duration = duration;
this.size = size;
this.address = address;
this.artist = artist;
this.audioId = -1L;
this.albumId = -1L;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getDuration() {
return duration;
}
public void setDuration(Long duration) {
this.duration = duration;
}
public Long getSize() {
return size;
}
public void setSize(Long size) {
this.size = size;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getArtist() {
return artist;
}
public void setArtist(String artist) {
this.artist = artist;
}
public Long getAudioId() {
return audioId;
}
public void setAudioId(Long audioId) {
this.audioId = audioId;
}
public Long getAlbumId() {
return albumId;
}
public void setAlbumId(Long albumId) {
this.albumId = albumId;
}
}
这个是本地音频的类,用的 Serializable,偷懒了,虽然我当时都不知道序列化有什么用,更不要说泛型了。
- 适配器
import android.graphics.Color;
import android.support.annotation.Nullable;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.BaseViewHolder;
import java.util.List;
public class AudioPlayListAdapter extends BaseQuickAdapter<String, BaseViewHolder> {
private int select = 0;
public AudioPlayListAdapter(int layoutResId, @Nullable List<String> data) {
super(layoutResId, data);
}
@Override
protected void convert(BaseViewHolder helper, String item) {
helper.setText(R.id.item_text_singleline, item);
//标记选中项
if (helper.getLayoutPosition() == select){
helper.setTextColor(R.id.item_text_singleline, Color.parseColor("#4caf50"));
}else {
helper.setTextColor(R.id.item_text_singleline, Color.parseColor("#8A8A8A"));
}
}
public void setSelect(int select) {
this.select = select;
}
}
这里用到了 BARVH 里面的类,item_text_singleline 这个可以用 android 自带的单行布局,省的写了。
- 获取音频封面工具类
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class MediaUtiles {
private static final Uri albumArtUri = Uri.parse("content://media/external/audio/albumart");
/**
* 获取默认专辑图片
*/
public static Bitmap getDefaultArtwork(Context context, boolean small) {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredConfig = Bitmap.Config.RGB_565;
if(small){ //返回小图片
return BitmapFactory.decodeResource(context.getResources(), R.drawable.pic_audio_default);
}
return BitmapFactory.decodeResource(context.getResources(), R.drawable.pic_audio_default);
}
/**
* 从文件当中获取专辑封面位图
*/
private static Bitmap getArtworkFromFile(Context context, long songid, long albumid){
Bitmap bm = null;
if(albumid < 0 && songid < 0) {
throw new IllegalArgumentException("Must specify an album or a song id");
}
try {
BitmapFactory.Options options = new BitmapFactory.Options();
FileDescriptor fd = null;
if(albumid < 0){
Uri uri = Uri.parse("content://media/external/audio/media/"
+ songid + "/albumart");
ParcelFileDescriptor pfd = null;
try {
pfd = context.getContentResolver().openFileDescriptor(uri, "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if(pfd != null) {
fd = pfd.getFileDescriptor();
}
} else {
Uri uri = ContentUris.withAppendedId(albumArtUri, albumid);
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
if(pfd != null) {
fd = pfd.getFileDescriptor();
}
}
options.inSampleSize = 1;
// 只进行大小判断
options.inJustDecodeBounds = true;
// 调用此方法得到options得到图片大小
BitmapFactory.decodeFileDescriptor(fd, null, options);
// 我们的目标是在800pixel的画面上显示
// 所以需要调用computeSampleSize得到图片缩放的比例
options.inSampleSize = 100;
// 我们得到了缩放的比例,现在开始正式读入Bitmap数据
options.inJustDecodeBounds = false;
options.inDither = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
//根据options参数,减少所需要的内存
bm = BitmapFactory.decodeFileDescriptor(fd, null, options);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return bm;
}
/**
* 获取专辑封面位图对象
*/
public static Bitmap getArtwork(Context context, long song_id, long album_id, boolean allowdefalut, boolean small){
if(album_id < 0) {
if(song_id < 0) {
Bitmap bm = getArtworkFromFile(context, song_id, -1);
if(bm != null) {
return bm;
}
}
if(allowdefalut) {
return getDefaultArtwork(context, small);
}
return null;
}
ContentResolver res = context.getContentResolver();
Uri uri = ContentUris.withAppendedId(albumArtUri, album_id);
if(uri != null) {
InputStream in = null;
try {
in = res.openInputStream(uri);
BitmapFactory.Options options = new BitmapFactory.Options();
//先制定原始大小
options.inSampleSize = 1;
//只进行大小判断
options.inJustDecodeBounds = true;
//调用此方法得到options得到图片的大小
BitmapFactory.decodeStream(in, null, options);
/** 我们的目标是在你N pixel的画面上显示。 所以需要调用computeSampleSize得到图片缩放的比例 **/
/** 这里的target为800是根据默认专辑图片大小决定的,800只是测试数字但是试验后发现完美的结合 **/
if(small){
options.inSampleSize = computeSampleSize(options, 40);
} else{
options.inSampleSize = computeSampleSize(options, 600);
}
// 我们得到了缩放比例,现在开始正式读入Bitmap数据
options.inJustDecodeBounds = false;
options.inDither = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
in = res.openInputStream(uri);
return BitmapFactory.decodeStream(in, null, options);
} catch (FileNotFoundException e) {
Bitmap bm = getArtworkFromFile(context, song_id, album_id);
if(bm != null) {
if(bm.getConfig() == null) {
bm = bm.copy(Bitmap.Config.RGB_565, false);
if(bm == null && allowdefalut) {
return getDefaultArtwork(context, small);
}
}
} else if(allowdefalut) {
bm = getDefaultArtwork(context, small);
}
return bm;
} finally {
try {
if(in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
/**
* 对图片进行合适的缩放
*/
public static int computeSampleSize(BitmapFactory.Options options, int target) {
int w = options.outWidth;
int h = options.outHeight;
int candidateW = w / target;
int candidateH = h / target;
int candidate = Math.max(candidateW, candidateH);
if(candidate == 0) {
return 1;
}
if(candidate > 1) {
if((w > target) && (w / candidate) < target) {
candidate -= 1;
}
}
if(candidate > 1) {
if((h > target) && (h / candidate) < target) {
candidate -= 1;
}
}
return candidate;
}
}
这里实际上很多音乐都会把海报存到文件里面,获取到像 MP3 就可能拿到里面的封面。
结语
实际上这些代码写的不是很好,ButterKnife 现在用的也不多了,音频播放应该放到 service 里面去的,数据的序列化,布局的适配等都要提高。但是我也是想记录下自己当时有多菜,勉励吧!
end