目录

小项目开发——Android 音乐播放器

一、题目

音乐播放器.

要求Activity编程、ListView编程、SeekBar编程、ExoPlayer编程(播放暂停停止上一首下一首),音乐文件放在assets/music目录下,界面自拟.

◼ 期望最终效果:

在这里插入图片描述


二、实际最终效果

◼ 分别对应activity_music_list.xmlactivity_my_music_player.xml的视图.

◼ 点击列表任何一个元素都可以直接跳转到音乐播放界面.

在这里插入图片描述


三、模块分析

在这里插入图片描述

◼ 从题目所期望的效果来看,需要实现的主要分为3大模块:音乐列表进度条功能按钮.

◼ 还有2大可自定义模块:状态栏导航栏.

但为了尽可能实现像市面上的大部分音乐播放器的界面,我在不改变题目原有主功能的基础上进行了重新设计,即设计了2个 Activity(MusicListActivity.javaMyMusicPlayerActivity.java)及其对应 Layout(activity_music_list.xmlactivity_my_music_player.xml),分别控制 音乐列表音乐播放,而不是将它们写在一起.


四、思维导图

◼ 基于以上分析决定分以下4大模块来进行编程:状态栏导航栏音乐列表音乐播放.

◼ 并以 LayoutActivity 两大块 进行阐述.

在这里插入图片描述


五、Layout

1. 自定义 Theme

Path:res/values/themes.xml,添加自定义主题样式,

<style name="Theme.MyMusic" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
    <!-- Primary brand color. -->
    <item name="colorPrimary">#B22196F3</item>
    <item name="colorPrimaryVariant">#8B19ADD2</item>
    <item name="colorOnPrimary">#FFFFFF</item>
    <!-- Status bar color. -->
    <item name="android:statusBarColor">#B91976D2</item>
</style>

◼ 并应用在了音乐列表和音乐播放的 Activity 中(即状态栏、导航栏)——AndroidManifest.xml.

android:theme="@style/Theme.MyMusic"

2. 导航栏 LOGO

music_list.xml:将 LOGO 资源图片(icon_music_list.png)修改为 白色 以适配导航栏的背景色.

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <bitmap android:src="@drawable/icon_music_list" android:tint="@color/white" />
    </item>
</layer-list>

注:LOGO 图标 见 ———> icon_music_list.png

3. 音乐列表布局

◼ 文件名:activity_music_list.xml

◼ 采用 约束布局ConstrainLayout

◼ 布局idcl_music_list

◼ 只有一个 空白的 占满整个屏幕的ListView组件:

在这里插入图片描述

4. 音乐播放布局

◼ 文件名:activity_my_music_player.xml

◼ 采用 约束布局ConstrainLayout

◼ 布局idcl_music_player

注:剩下没贴图的ImageButton同右下角的ibtn_play,只是图标、位置、放缩倍率不同.

音乐封面可去网易云官网复制.

按钮图标 见 ———> 播放按钮图标

5. 设置APP图标及名字

android:icon="@drawable/headphone"
android:label="MyMusicPlayer"

注:APP 图标见 ———> headphone.png


六、Activity

1. 音乐列表 Activity

◼ 文件名:MusicListActivity.java

◼ 变量声明:

Intent mIntent; // 与 MyMusicPlayerActivity 进行通信
ListView mMusicListLv; // 音乐列表框
List<String> mMusicList; // 音乐列表
ArrayAdapter<String> mArrayAdapter; // 适配器
⑴ 列表元素点击监听器
AdapterView.OnItemClickListener mListenerLv = (parent, view, position, id) -> {
    mIntent.putExtra("selectedIndex", position); // 传选中音乐下标
    startActivity(mIntent); // 跳转至 MyMusicPlayerActivity
}; // end mLvListener
⑵ 获取音乐名
public void getMusics() {
    try {
        String[] musicFileNames = getAssets().list("musics");
        for (int i = 0; i < musicFileNames.length; ++i) {
            musicFileNames[i] = musicFileNames[i].split("\\.")[0]; // 以“.”分割字符串得到不含后缀的音乐名
            mMusicList.add(musicFileNames[i]);
        } // end for
        mIntent.putExtra("musicArray", musicFileNames); // 将整个音乐列表传给 MyMusicPlayerActivity
    } catch (IOException e) {
        throw new RuntimeException(e);
    } // end catch
} // end getMusics

2. 音乐播放 Activity

◼ 文件名:MyMusicPlayerActivity.java

◼ 变量声明:

private static String[] sMusicArray; // 音乐数组
private static int sMusicIndex; // 音乐下标
private int mPlayMode; // 音乐播放模式
private long mStartPos, mDurationPos; // 进度条起始位置、终止(最大)位置
Intent mIntent; // 与 MusicListActivity 进行通信
Timer mTimer; // 定时器
ExoPlayer mExoPlayer; // 音乐播放器
MediaItem mediaItem; // 媒体资源
TextView mMusicTitleTv; // 音乐名和作者文本
TextView mStartTimeTv, mDurationTimeTv; // 播放起始和终止时间文本
ImageButton mPlayModeIbtn, mPreviousIbtn, mPlayStateIbtn, mNextIbtn, mStopIbtn; // 音乐按钮
ImageView mMusicCoverIv; // 音乐封面
Bitmap mBitmap; // Bitmap 对象
ObjectAnimator mAnimator; // 设置动画
SeekBar mMusicProgressSb; // 音乐进度条
⑴ 获取音乐列表信息
public void getIntentMsg() {
    sMusicIndex = mIntent.getIntExtra("selectedIndex", 0); // 默认第一首音乐
    sMusicArray = mIntent.getStringArrayExtra("musicArray");
} // end getIntentMsg
⑵ 音乐封面圆形剪裁和旋转动画
API 说明
circularCutting Bitmap对象进行圆形剪裁
addAnimation ImageView对象添加动画
public Bitmap circularCutting(Bitmap bitmap) {
    mBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(mBitmap);
    Paint paint = new Paint();
    paint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
    canvas.drawCircle(bitmap.getWidth() / 2f, bitmap.getHeight() / 2f, bitmap.getWidth() / 2f, paint);
    return mBitmap;
} // end circularCutting

◼ 首先,使用Bitmap.createBitmap()ARGB_8888一种32位颜色深度的色彩模式创建一个与原始bitmap大小相同且高质量的位图对象mBitmap.

◼ 然后,先使用Canvas对象将该位图对象绘制成一个圆形,再使用Paint对象的setShader()方法设置圆形填充色,通过BitmapShader对象将原始 bitmap作为填充纹理.

◼ 最后,将处理后的位图对象返回.

public void addAnimation(ImageView iv) {
    mAnimator = ObjectAnimator.ofFloat(iv, "rotation", 0f, 360f); // 360°旋转
    mAnimator.setDuration(40000); // 毫秒
    mAnimator.setRepeatCount(ObjectAnimator.INFINITE); // 动画无限循环
    mAnimator.setInterpolator(new LinearInterpolator()); // 线性插值器
    mAnimator.start(); // 启动动画
} // end addAnimation

◼ 首先使用ObjectAnimator.ofFloat()方法创建一个ObjectAnimator对象,将其绑定到ImageView对象的rotation属性上,设置动画的起始值和结束值,以及动画的持续时间和重复次数.

◼ 然后,使用setInterpolator()方法设置动画插值器为线性插值器.

◼ 最后调用start()方法启动动画.

⑶ 设置音乐播放相关资源
API 说明
mPlayerListener 音乐播放器监听器
initExoPlayer 初始化音乐播放器(ExoPlayer对象)
updateMusicPlayer 更新音乐播放器
updateMusicLayout 更新音乐播放器页面
Player.Listener mPlayerListener = new Player.Listener() {
    @Override
    public void onPlaybackStateChanged(int playbackState) {
        if (playbackState == ExoPlayer.STATE_READY) { // 播放器准备好了
            mExoPlayer.play();
            mPlayStateIbtn.setImageResource(R.drawable.pause);
            mTimer.schedule(new ProgressUpdate(), 0, 500);
        } // end if
    } // end onPlaybackStateChanged
}; // end mPlayerListener

public void initExoPlayer() {
    mExoPlayer = new ExoPlayer.Builder(MyMusicPlayerActivity.this).build();
    /* 一次性将所有音乐资源添加到音乐播放器中 */
    for (String musicName : sMusicArray) {
        mediaItem = MediaItem.fromUri("asset:///musics/" + musicName + ".mp3");
        mExoPlayer.addMediaItem(mediaItem);
    } // end for
    updateMusicPlayer(sMusicIndex);
    mExoPlayer.setRepeatMode(ExoPlayer.REPEAT_MODE_ALL); // 默认列表循环
    mPlayMode = mExoPlayer.getRepeatMode();
    mExoPlayer.addListener(mPlayerListener);
} // end initExoPlayer

◼ 对于 asset 文件夹里的资源,可以以asset:///path形式得到资源的URI.

◼ 这里一次性将 MusicListActivity 传来的所有音乐资源添加到音乐播放器中,以便后续直接通过 索引位置 进行相关操作.

public void updateMusicPlayer(int index) {
    updateMusicLayout(index); // 更新页面
    mExoPlayer.seekTo(index, 0);
    mExoPlayer.prepare();
} // end updateExoPlayer

public void updateMusicLayout(int index) {
    mMusicTitleTv.setText(sMusicArray[index]);
    try {
        InputStream inputStream = getAssets().open("music_images/" + sMusicArray[index] + ".jpg");
        mBitmap = BitmapFactory.decodeStream(inputStream);
        mMusicCoverIv.setImageBitmap(circularCutting(mBitmap));
        inputStream.close();
    } catch (IOException e) {
        throw new RuntimeException(e);
    } // end catch
} // end updateMusicLayout
⑷ 进度条
SeekBar.OnSeekBarChangeListener mListenerSb = new SeekBar.OnSeekBarChangeListener() {
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        /* 从用户拖动到的位置开始播放 */
        if (fromUser) {
            mExoPlayer.seekTo(sMusicIndex, progress);
        }
        /* 列表循环状态下,音乐会自动到下一首,此时需要重新渲染页面元素 */
        if (sMusicIndex != mExoPlayer.getCurrentMediaItemIndex()
            && mPlayMode == mExoPlayer.REPEAT_MODE_ALL) {
            sMusicIndex = mExoPlayer.getCurrentMediaItemIndex();
            updateMusicLayout(sMusicIndex);
            mAnimator.cancel();
            mAnimator.start();
        } // end if
    } // end onProgressChanged

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
    } // end onStartTrackingTouch

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
        mAnimator.resume(); // 用户停止拖动进度条,图片重新旋转
        mStartTimeTv.setText(formatPosition(seekBar.getProgress()));
    } // end onStopTrackingTouch
}; // end mListenerSb

/**
 * 内部类——定时任务类:定时更新 SeekBar 进度条
 */
private class ProgressUpdate extends TimerTask {
    @Override
    public void run() {
        runOnUiThread(() -> {
            mStartPos = mExoPlayer.getContentPosition();
            mMusicProgressSb.setProgress((int) mStartPos);
            mStartTimeTv.setText(formatPosition(mStartPos));
            mDurationPos = mExoPlayer.getDuration();
            mMusicProgressSb.setMax((int) mDurationPos);
            mDurationTimeTv.setText(formatPosition(mDurationPos));
        }); // end runOnUiThread
    } // end run
} // end ProgressUpdate

/**
 * 格式化音乐进度条起始、终止位置,显示“分:秒”
 *
 * @param pos 音乐进度条位置
 * @return “分:秒”
 */
public String formatPosition(long pos) {
    @SuppressLint("SimpleDateFormat")
    SimpleDateFormat sdf = new SimpleDateFormat("mm:ss"); // "分:秒"格式
    return sdf.format(pos);
} // end format
⑸ 功能按钮
API 说明
changePlayerMode 更换音乐播放模式
previousMusic 上一首
changePlayerState 播放/暂停
nextMusic 下一首
stopMusic 停止音乐
public void changePlayerMode() {
    if (mPlayMode == mExoPlayer.REPEAT_MODE_ALL) {
        mPlayModeIbtn.setImageResource(R.drawable.repeat_once);
        mExoPlayer.setRepeatMode(ExoPlayer.REPEAT_MODE_ONE);
        mPlayMode = ExoPlayer.REPEAT_MODE_ONE;
        Toast.makeText(MyMusicPlayerActivity.this, "单曲循环", Toast.LENGTH_SHORT).show();
    } else {
        mPlayModeIbtn.setImageResource(R.drawable.repeat_all);
        mExoPlayer.setRepeatMode(ExoPlayer.REPEAT_MODE_ALL);
        mPlayMode = ExoPlayer.REPEAT_MODE_ALL;
        Toast.makeText(MyMusicPlayerActivity.this, "列表循环", Toast.LENGTH_SHORT).show();
    } // end else
} // end changePlayerMode

public void previousMusic() {
    if (sMusicIndex == 0) {
        sMusicIndex = sMusicArray.length - 1;
    } else sMusicIndex--;
    mAnimator.cancel(); // 上一首取消动画
    mAnimator.start(); // 并重新开始旋转
    updateMusicPlayer(sMusicIndex);
} // end previousMusic

public void changePlayerState() {
    if (mExoPlayer.isPlaying()) {
        mExoPlayer.pause();
        mAnimator.pause(); // 暂停动画,直到遇上 resume()
        mPlayStateIbtn.setImageResource(R.drawable.play);
        mTimer.cancel();
        mTimer = new Timer();
        Toast.makeText(MyMusicPlayerActivity.this, "暂停", Toast.LENGTH_SHORT).show();
    } else {
        mExoPlayer.play();
        mAnimator.resume(); // 将暂停的动画重新从当前位置开始旋转,而不是重新开始
        mPlayStateIbtn.setImageResource(R.drawable.pause);
        mTimer = new Timer();
        mTimer.schedule(new ProgressUpdate(), 0, 500);
        Toast.makeText(MyMusicPlayerActivity.this, "播放", Toast.LENGTH_SHORT).show();
    } // end else
} // end changePlayerState

public void nextMusic() {
    if (sMusicIndex == sMusicArray.length - 1) {
        sMusicIndex = 0;
    } else sMusicIndex++;
    mAnimator.cancel(); // 下一首取消动画
    mAnimator.start(); // 重新开始旋转
    updateMusicPlayer(sMusicIndex);
} // end nextMusic

public void stopMusic() {
    finish();
} // end stopMusic

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐