Android | 音乐播放器 I(详解)

一、功能

  •  搜索手机内的音乐(MediaStore、ContentResolver)
  • 展示到播放界面(CursorAdapter、ListView)
  • 点击播放界面的歌曲,能够进行播放(MediaPlayer),展示到底部导航栏上

二、代码解析

1.布局文件编写

        播放界面 activity_main.xml 使用 ConstraintLayout 布局,包括 ListView,用于包括音乐的歌名,歌手名等效果信息。具体排布在 list_item.xml 中设定。list_item.xml 样式如下:

         底部导航栏在 bottom_media_toolbar.xml 中编写,其样式如下:

2.Java程序编写

(1)通过 MediaStore 申请读取外部存储权限

  • 在 AndroidManifest 中申请权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  • 动态申请权限

        相关函数:

  • checkSelfPermission —— 判断指定的权限是否已经获得授权

  • shouldShowRequestPermissionRationale —— 判断是否需要向用户展示权限申请时的说明信息

  • requestPermissions —— 动态申请权限

  • onRequestPermissionsResult —— 判断是否通过了requestPermissions方法动态申请权限

        相关代码:

    private final int REQUEST_EXTERNAL_STORAGE = 1;
    private static final String[] STRINGS = {
            Manifest.permission.READ_EXTERNAL_STORAGE,  // 读取外部存储
            Manifest.permission.WRITE_EXTERNAL_STORAGE  // 写入外部存储
    };
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE))
                requestPermissions(STRINGS, REQUEST_EXTERNAL_STORAGE); // 如果第一次点了是,下次不会弹出; 如果点了否,下次还会弹出
        } else initPlaylist();
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_EXTERNAL_STORAGE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
                initPlaylist();
        }
    }

        解析:

      I. 第一个判断语句判断 读取外部存储 的权限是否获得授权 。

if (ContextCompat.checkSelfPermission
(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
  • Manifest.permission.READ_EXTERNAL_STORAGE 来自 Manifest 类,意为 读取外部存储
public static final String READ_EXTERNAL_STORAGE ="android.permission.READ_EXTERNAL_STORAGE";
  • PackageManager.PERMISSION_GRANTED 来自PackageManager 类,意为许可授予
    /**
     * Permission check result: this is returned by {@link #checkPermission}
     * if the permission has been granted to the given package.
     */
    public static final int PERMISSION_GRANTED = 0;

       

         II.第二个判断语句判断用户是否是第一次申请权限。  如果是第一次,则会弹出系统APP权限申请。如果用户点了是,即允许APP获得该权限,则下次不会弹出。 如果点了否,下次还会弹出询问。

if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE))
                requestPermissions(STRINGS, REQUEST_EXTERNAL_STORAGE);

        III.如果通过了权限,则进行数据的初始化操作。

else initPlaylist()

       

         IV.通过requestPermissions()方法申请权限后,可以通过onRequestPermissionsResult()方法获取申请权限结果,相关参数:

  • int requestCode,对应 requestPermissions() 指定的requestCode,通过该参数判断请求的具体参数;

  • String[ ] permissionsrequestPermissions()方法中所申请的权限数组;

  • int[ ] grantResults,该数组对应于permissions对应的申请权限结果,值为PackageManager.PERMISSION_GRANTEDPackageManager.PERMISSION_DENIED

(2)通过 ContentResolver 查询手机中的歌曲

          相关代码:

    /**
     * 内容解析器
     */
    private ContentResolver mContentResolver;
    /**
     * 选择语句where子句
     */
    private final String SELECTION = MediaStore.Audio.Media.IS_MUSIC + " = ? " + " AND " + MediaStore.Audio.Media.MIME_TYPE + " LIKE ? ";
    /**
     * where子句参数
     */
    private final String[] SELECTION_ARGS = {Integer.toString(1), "audio/mpeg"};
    private void initPlaylist() {
        // 游标对象查询MP3数据
        Cursor mCursor = mContentResolver.query(
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, // 表名
                null,   // 所有列
                SELECTION,        // where子句
                SELECTION_ARGS,   // where参数
                MediaStore.Audio.Media.DEFAULT_SORT_ORDER // 默认排序方式
        );
        mCursorAdapter.swapCursor(mCursor); // 交互游标,但不删除旧的游标
        mCursorAdapter.notifyDataSetChanged(); // 刷新数据
    }

         解析:

        先了解一下 Content provider 以及 ContentResolver。

        Content provider 为共享数据提供访问接口,提供共享数据的应用可将数据存储与文件系统、SQLite 数据库或者是云端。用户通过 Content provider 提供的接口即可访问数据,而无需关心数据如何存储。
         ContentResolver 类则是用于与 Content provider 进行数据访问操作。 通过 ContentResolver 对  Content provider 进行解耦。

        下面通过ContentResolver.query()方法进行数据的查询,该方法包含如下参数:

  • Uri uri,查询 Content provider Uri

  • String[] projections,查询字段

  • String selection,对应的where子句

  • String[] selectionArgswhere子句的相关参数

  • String sortOrder,排序条件

Cursor mCursor = mContentResolver.query(
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, // 表名
                null,   // 所有列
                SELECTION,        // where子句
                SELECTION_ARGS,   // where参数
                MediaStore.Audio.Media.DEFAULT_SORT_ORDER // 默认排序方式
        );

        

(3)定义 MediaCursorAdapter 适配器与 ListView 绑定

        相关代码:

import android.content.Context;
import android.database.Cursor;
import android.provider.MediaStore;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.TextView;


public class MediaCursorAdapter extends CursorAdapter {

    private final LayoutInflater mLayoutInflater;

    public MediaCursorAdapter(Context context) {
        super(context, null, 0);
        mLayoutInflater = LayoutInflater.from(context);
    }

    public static class ViewHolder {
        TextView tvTitle;
        TextView tvArtist;
        TextView tvOrder;
        View divider;
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
        View view = mLayoutInflater.inflate(R.layout.list_item, viewGroup, false);
        if (view != null) {
            ViewHolder viewHolder = new ViewHolder();
            viewHolder.tvTitle = view.findViewById(R.id.tv_title);
            viewHolder.tvArtist = view.findViewById(R.id.tv_artist);
            viewHolder.tvOrder = view.findViewById(R.id.tv_order);
            viewHolder.divider = view.findViewById(R.id.divider);
            view.setTag(viewHolder);
            return view;
        }
        return null;
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        ViewHolder viewHolder = (ViewHolder) view.getTag();

        int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
        int artistIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);

        String title = cursor.getString(titleIndex);
        String artist = cursor.getString(artistIndex);

        int position = cursor.getPosition();

        if (viewHolder != null) {
            viewHolder.tvTitle.setText(title);
            viewHolder.tvArtist.setText(artist);
            viewHolder.tvOrder.setText(Integer.toString(position + 1));
        }
    }
}

      解析:

        MediaCursorAdapter 继承 CursorAdapter ,用于将 ContentResolver.query() 查询的歌曲的相关信息与 ListView 绑定,需要实现 newView() 以及 bindView()方法。

  • newView()方法是在用户上下滑动时,新出现的Item需要加载项布局时被调用,其创建一个新的视图,在这里与 list_item 相关的控件进行绑定。
@Override
    public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
        View view = mLayoutInflater.inflate(R.layout.list_item, viewGroup, false);
        if (view != null) {
            ViewHolder viewHolder = new ViewHolder();
            viewHolder.tvTitle = view.findViewById(R.id.tv_title);
            viewHolder.tvArtist = view.findViewById(R.id.tv_artist);
            viewHolder.tvOrder = view.findViewById(R.id.tv_order);
            viewHolder.divider = view.findViewById(R.id.divider);
            view.setTag(viewHolder);
            return view;
        }
        return null;
    }

        首先使用LayoutInflater对象的inflate()方法加载了名为R.layout.list_item的项视图布局文件。

View view = mLayoutInflater.inflate(R.layout.list_item, viewGroup, false);

        任何实例化ViewHolder对象,初始化该对象的几个控件属性绑定了加载好的项布局中的控件,并最后通过itemViewsetTag()方法保存至itemView中以便在bindView()方法中从itemView中取出直接使用。

ViewHolder viewHolder = new ViewHolder();
viewHolder.tvTitle = view.findViewById(R.id.tv_title);
viewHolder.tvArtist = view.findViewById(R.id.tv_artist);
viewHolder.tvOrder = view.findViewById(R.id.tv_order);
viewHolder.divider = view.findViewById(R.id.divider);
view.setTag(viewHolder);

  • bindView() 方法将程查询到的歌曲(cursor类型)的相关信息,即歌名,歌手名,专辑图片等提取出来,并填充到视图中。

    可以把ContentResolver.query() 查询到的数据集当作二维表,cursor 即指向一行数据。通过cursor 的相关方法即可以获取该行指定列的表格数据:

                cursor.getColumnIndex(列名) - 获取表格指定列名的索引号

 int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);

                cursor.getString(索引号) - 获取表格指定索引号的内容

String title = cursor.getString(titleIndex);

                其中 MediaStore.Audio.Media.TITLE 来自MediaStore类,是查询到的歌曲的歌名那一列的列名。

String TITLE = "title";
@Override
    public void bindView(View view, Context context, Cursor cursor) {
        ViewHolder viewHolder = (ViewHolder) view.getTag();

        int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
        int artistIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);

        String title = cursor.getString(titleIndex);
        String artist = cursor.getString(artistIndex);

        int position = cursor.getPosition();

        if (viewHolder != null) {
            viewHolder.tvTitle.setText(title);
            viewHolder.tvArtist.setText(artist);
            viewHolder.tvOrder.setText(Integer.toString(position + 1));
        }
    }

  • 内部类ViewHolder 用于ListView的优化,暂存加载项视图布局后的各控件对象,避免通过findViewById()的方法重复进行查找绑定控件对象
public static class ViewHolder {
        TextView tvTitle;
        TextView tvArtist;
        TextView tvOrder;
        View divider;
    }

(4)设置 播放列表点击监听器

          相关代码:

    @Override
    protected void onStart() {
        super.onStart();
        if (mMediaPlayer == null) mMediaPlayer = new MediaPlayer();
    }

    @Override
    protected void onStop() {
        if (mMediaPlayer != null) {
            mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
        super.onStop();
    }
private final ListView.OnItemClickListener mPlaylistClickListener = new ListView.OnItemClickListener() {
        /**
         *  为底部导航栏设置点击的歌曲,使用 MediaPlayer 进行点击歌曲的播放
         */
        @Override
        public void onItemClick(AdapterView<?> arg0, View arg1, int i, long l) {

            Cursor cursor = mCursorAdapter.getCursor();
            if (cursor != null && cursor.moveToPosition(i)) { // cursor.moveToPosition(i) 移动到指定行
                // 获取索引
                int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
                int artistIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
                int albumIdIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID);
                int dataIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);

                // 获取资源
                String title = cursor.getString(titleIndex);
                String artist = cursor.getString(artistIndex);
                long albumId = cursor.getLong(albumIdIndex);
                String data = cursor.getString(dataIndex);

                // 为 MediaPlayer 设置播放的歌曲MP3数据
                Uri dataUri = Uri.parse(data);
                if (mMediaPlayer != null) {
                    try {
                        mMediaPlayer.reset(); // 设置为空闲 Idle 状态
                        mMediaPlayer.setDataSource(MainActivity.this, dataUri); // 设置歌曲MP3数据
                        mMediaPlayer.prepare(); // 设置为准备 Prepared 状态
                        mMediaPlayer.start(); // 设置为开始 Started 状态
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }
                bottomNavigation.setVisibility(View.VISIBLE);// 设置底部导航栏可显示

                // 将 歌名、歌手、封面 绑定到控件上
                if (tvBottomTitle != null) tvBottomTitle.setText(title);
                if (tvBottomArtist != null) tvBottomArtist.setText(artist);
                Uri albumUri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, albumId); // 查询
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    try {
                        Bitmap album = mContentResolver.loadThumbnail(albumUri, new Size(640, 480), null);
                        ivAlbumThumbnail.setImageBitmap(album);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };
mPlaylist.setOnItemClickListener(mPlaylistClickListener);

        解析:

        在MainActivity管理MediaPlayer对象,在onStart()方法中启动MediaPlayer,并在onStop()中释放。

    @Override
    protected void onStart() {
        super.onStart();
        if (mMediaPlayer == null) mMediaPlayer = new MediaPlayer();
    }

    @Override
    protected void onStop() {
        if (mMediaPlayer != null) {
            mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
        super.onStop();
    }

        通过 mCursorAdapter.getCursor() 方法获得 cursor 对象。如果 cursor 不为空且能够移动到用户所点击到的歌曲对应的位置 i ,那么就获取歌曲的相关信息,展示到底部导航栏上,并设置 MediaPlayer ,进行歌曲的播放。onItemClick()相关参数说明:

  • AdapterView<?> adapterView,表示ListView对象;

  • View view,在ListView中被点击的Item对应的布局控件对象;

  • int i,被点击的布局控件对象viewAdapter中的序号;

  • long l, 被点击的ItemListView中的序号;

private final ListView.OnItemClickListener mPlaylistClickListener = new ListView.OnItemClickListener() {
        /**
         *  为底部导航栏设置点击的歌曲,使用 MediaPlayer 进行点击歌曲的播放
         */
        @Override
        public void onItemClick(AdapterView<?> arg0, View arg1, int i, long l) {

            Cursor cursor = mCursorAdapter.getCursor();
            if (cursor != null && cursor.moveToPosition(i)) { // cursor.moveToPosition(i) 移动到指定行
                // 获取索引
                int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
                int artistIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
                int albumIdIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID);
                int dataIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);

                // 获取资源
                String title = cursor.getString(titleIndex);
                String artist = cursor.getString(artistIndex);
                long albumId = cursor.getLong(albumIdIndex);
                String data = cursor.getString(dataIndex);

                // 为 MediaPlayer 设置播放的歌曲MP3数据
                Uri dataUri = Uri.parse(data);
                if (mMediaPlayer != null) {
                    try {
                        mMediaPlayer.reset(); // 设置为空闲 Idle 状态
                        mMediaPlayer.setDataSource(MainActivity.this, dataUri); // 设置歌曲MP3数据
                        mMediaPlayer.prepare(); // 设置为准备 Prepared 状态
                        mMediaPlayer.start(); // 设置为开始 Started 状态
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }
                bottomNavigation.setVisibility(View.VISIBLE);// 设置底部导航栏可显示

                // 将 歌名、歌手、封面 绑定到控件上
                if (tvBottomTitle != null) tvBottomTitle.setText(title);
                if (tvBottomArtist != null) tvBottomArtist.setText(artist);
                Uri albumUri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, albumId); // 查询
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    try {
                        Bitmap album = mContentResolver.loadThumbnail(albumUri, new Size(640, 480), null);
                        ivAlbumThumbnail.setImageBitmap(album);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };

        最后在 onCreate() 方法中绑定监听器。

mPlaylist.setOnItemClickListener(mPlaylistClickListener);

(5)设置 播放按钮单击 监听器

        相关代码:

private Boolean ivPlayStatus = true;
    /**
     * 播放按钮单击 监听器
     */
    private final View.OnClickListener ivPlayButtonClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (view.getId() == R.id.iv_play) {
                ivPlayStatus = !ivPlayStatus;
                if (ivPlayStatus) {
                    ivPlay.setImageResource(R.drawable.ic_baseline_pause_circle_outline_24);
                } else {
                    ivPlay.setImageResource(R.drawable.ic_baseline_play_circle_outline_24);
                }
            }
        }
    };
if (ivPlay != null) ivPlay.setOnClickListener(ivPlayButtonClickListener);

        解析:

        这里仅仅是改变了图标,如果要想实现对歌曲的暂停控制,还需要用到绑定服务的知识,这个以后再讲。通过标志位 ivPlayStatus 判断当前图标的状态,点击后修改图标以及 ivPlayStatus。

private final View.OnClickListener ivPlayButtonClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (view.getId() == R.id.iv_play) {
                ivPlayStatus = !ivPlayStatus;
                if (ivPlayStatus) {
                    ivPlay.setImageResource(R.drawable.ic_baseline_pause_circle_outline_24);
                } else {
                    ivPlay.setImageResource(R.drawable.ic_baseline_play_circle_outline_24);
                }
            }
        }
    };

        在 onCreate() 方法中绑定监听器。

if (ivPlay != null) ivPlay.setOnClickListener(ivPlayButtonClickListener);

(6)onCreate()方法绑定主布局以及其他操作

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 实例化相关对象
        mContentResolver = getContentResolver();
        mCursorAdapter = new MediaCursorAdapter(MainActivity.this);

        // 绑定 ListView
        mPlaylist = findViewById(R.id.lv_playlist);

        // 设置适配器
        mPlaylist.setAdapter(mCursorAdapter);

        // 绑定 BottomNavigationView
        bottomNavigation = findViewById(R.id.navigation); // 拿到主布局的底部导航栏对象
        LayoutInflater.from(MainActivity.this).inflate(R.layout.bottom_media_toolbar, bottomNavigation, true); // 把 bottom_media_toolbar 放到主布局的底部导航栏中

        // 让底部导航栏一开始消失的
        bottomNavigation.setVisibility(View.GONE);

        // 绑定 BottomNavigationView 其他控件
        tvBottomTitle = bottomNavigation.findViewById(R.id.tv_bottom_title);
        tvBottomArtist = bottomNavigation.findViewById(R.id.tv_bottom_artist);
        ivAlbumThumbnail = bottomNavigation.findViewById(R.id.iv_thumbnail);
        ivPlay = bottomNavigation.findViewById(R.id.iv_play);

        // 权限查询
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE))
                requestPermissions(STRINGS, REQUEST_EXTERNAL_STORAGE); // 如果第一次点了是,下次不会弹出; 如果点了否,下次还会弹出
        } else initPlaylist();

        // 设置监听器
        mPlaylist.setOnItemClickListener(mPlaylistClickListener);
        if (ivPlay != null) ivPlay.setOnClickListener(ivPlayButtonClickListener);
    }

(7)在手机中导入歌曲

        在 Android studio 中点击 Device File Explorer,这里是打开了手机的文件系统。找到 sdcard 目录下的 Music 目录,将自己下载的 mp3 音乐拷贝进去。 

        然后点击运行,就能够在虚拟机上进行歌曲的播放啦。

         附:好像在虚拟机上运行前还需要对手机文件进行一次扫描,扫描的软件我放在链接中。把软件拷贝到虚拟机上,点击 Start mediascan 后,编写的播放器上才可以显示刚刚放进去的歌曲。不过在真机上运行就不需要这些操作。

音乐播放器相关插件用于同步扫描文件系统-Android文档类资源-CSDN文库


三、源代码

1.布局文件

  • activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

<!--音乐-->
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/navigation"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">
        <ListView
            android:id="@+id/lv_playlist"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:divider="@android:color/transparent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>
    </androidx.constraintlayout.widget.ConstraintLayout>

<!--底部导航-->
    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/navigation"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
  • list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

<!--标号-->
    <TextView
        android:id="@+id/tv_order"
        android:text="1"
        android:textSize="20sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:layout_marginStart="16dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>
<!--歌名-->
    <TextView
        android:id="@+id/tv_title"
        android:textColor="@color/purple_700"
        android:textSize="18sp"
        android:layout_marginTop="8dp"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="8dp"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/tv_order"/>
<!--作者-->
    <TextView
        android:id="@+id/tv_artist"
        android:text="Artist name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        app:layout_constraintTop_toBottomOf="@id/tv_title"
        app:layout_constraintStart_toStartOf="@id/tv_title"/>
<!--分隔栏-->
    <View
        android:id="@+id/divider"
        android:layout_width="340dp"
        android:layout_height="1dp"
        android:layout_marginTop="8dp"
        android:background="@color/colorDivider"
        app:layout_constraintStart_toStartOf="@id/tv_title"
        app:layout_constraintTop_toBottomOf="@id/tv_artist"/>

</androidx.constraintlayout.widget.ConstraintLayout>
  • bottom_media_toolbar.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <!--播放进度-->
    <ProgressBar
        android:id="@+id/progress"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="0dp"
        android:layout_height="3dp"
        android:progress="28"
        android:progressBackgroundTint="@android:color/transparent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="@id/tv_bottom_title"
        app:layout_constraintEnd_toEndOf="parent"/>
    <!--专辑封面图-->
    <ImageView
        android:id="@+id/iv_thumbnail"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_marginStart="8dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <!--歌曲名-->
    <TextView
        android:id="@+id/tv_bottom_title"
        android:text="Title"
        android:textSize="14sp"
        android:textColor="@color/colorPrimary"
        android:layout_marginTop="4dp"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="56dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toEndOf="@id/iv_thumbnail" />

    <!--歌手名-->
    <TextView
        android:id="@+id/tv_bottom_artist"
        android:text="Artist name"
        android:textSize="12sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="4dp"
        app:layout_constraintTop_toBottomOf="@id/tv_bottom_title"
        app:layout_constraintStart_toStartOf="@id/tv_bottom_title"/>

    <!--播放控制图标-->
    <ImageView
        android:id="@+id/iv_play"
        android:src="@drawable/ic_baseline_pause_circle_outline_24"
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:clickable="true"
        android:layout_marginEnd="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout >

2.资源文件

  • colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#FF6200EE</color>
    <color name="purple_700">#FF3700B3</color>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>
    <color name="colorDivider">#4DAAA9A9</color>
    <color name="colorPrimary">#FF000000</color>
</resources>
  • ic_baseline_pause_circle_outline_24.xml
<vector android:height="24dp" android:tint="#000000"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M9,16h2L11,8L9,8v8zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM13,16h2L15,8h-2v8z"/>
</vector>
  • ic_baseline_play_circle_outline_24.xml
<vector android:height="24dp" android:tint="#000000"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M10,16.5l6,-4.5 -6,-4.5v9zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
</vector>

3.Java程序

  • MainActivity.java
import android.Manifest;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Size;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import com.google.android.material.bottomnavigation.BottomNavigationView;

import java.io.IOException;

public class MainActivity extends AppCompatActivity {

/* 底部导航组件 */
    /**
     * 底部导航视图
     */
    private BottomNavigationView bottomNavigation;
    /**
     * 歌曲名
     */
    private TextView tvBottomTitle;
    /**
     * 歌手名
     */
    private TextView tvBottomArtist;
    /**
     * 专辑封面图
     */
    private ImageView ivAlbumThumbnail;
    /**
     * 播放按钮图标
     */
    private ImageView ivPlay;
    /**
     * 播放状态
     */
    private Boolean ivPlayStatus = true;

/* 媒体播放器 MediaPlayer */
    /**
     * 媒体播放器
     */
    private MediaPlayer mMediaPlayer = null;

/* 内容解析器 ContentResolver */
    /**
     * 内容解析器
     */
    private ContentResolver mContentResolver;
    /**
     * 选择语句where子句
     */
    private final String SELECTION = MediaStore.Audio.Media.IS_MUSIC + " = ? " + " AND " + MediaStore.Audio.Media.MIME_TYPE + " LIKE ? ";
    /**
     * where子句参数
     */
    private final String[] SELECTION_ARGS = {Integer.toString(1), "audio/mpeg"};
    /**
     * 请求外部存储
     */
    private final int REQUEST_EXTERNAL_STORAGE = 1;
    /**
     * 权限存储
     */
    private static final String[] STRINGS = {
            Manifest.permission.READ_EXTERNAL_STORAGE,  // 读取外部存储
            Manifest.permission.WRITE_EXTERNAL_STORAGE  // 写入外部存储
    };

/* 媒体光标适配器 MediaCursorAdapter */
    /**
     * 列表显示
     */
    private ListView mPlaylist;
    /**
     * 媒体光标适配器
     */
    private MediaCursorAdapter mCursorAdapter;

/* 监听器 */
    /**
     * 播放列表点击 监听器
     * */
    private final ListView.OnItemClickListener mPlaylistClickListener = new ListView.OnItemClickListener() {
        /**
         *  为底部导航栏设置点击的歌曲,使用 MediaPlayer 进行点击歌曲的播放
         */
        @Override
        public void onItemClick(AdapterView<?> arg0, View arg1, int i, long l) {

            Cursor cursor = mCursorAdapter.getCursor();
            if (cursor != null && cursor.moveToPosition(i)) { // cursor.moveToPosition(i) 移动到指定行
                // 获取索引
                int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
                int artistIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
                int albumIdIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID);
                int dataIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);

                // 获取资源
                String title = cursor.getString(titleIndex);
                String artist = cursor.getString(artistIndex);
                long albumId = cursor.getLong(albumIdIndex);
                String data = cursor.getString(dataIndex);

                // 为 MediaPlayer 设置播放的歌曲MP3数据
                Uri dataUri = Uri.parse(data);
                if (mMediaPlayer != null) {
                    try {
                        mMediaPlayer.reset(); // 设置为空闲 Idle 状态
                        mMediaPlayer.setDataSource(MainActivity.this, dataUri); // 设置歌曲MP3数据
                        mMediaPlayer.prepare(); // 设置为准备 Prepared 状态
                        mMediaPlayer.start(); // 设置为开始 Started 状态
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }
                bottomNavigation.setVisibility(View.VISIBLE);// 设置底部导航栏可显示

                // 将 歌名、歌手、封面 绑定到控件上
                if (tvBottomTitle != null) tvBottomTitle.setText(title);
                if (tvBottomArtist != null) tvBottomArtist.setText(artist);
                Uri albumUri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, albumId); // 查询
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    try {
                        Bitmap album = mContentResolver.loadThumbnail(albumUri, new Size(640, 480), null);
                        ivAlbumThumbnail.setImageBitmap(album);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };
    /**
     * 播放按钮单击 监听器
     */
    private final View.OnClickListener ivPlayButtonClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (view.getId() == R.id.iv_play) {
                ivPlayStatus = !ivPlayStatus;
                if (ivPlayStatus) {
                    ivPlay.setImageResource(R.drawable.ic_baseline_pause_circle_outline_24);
                } else {
                    ivPlay.setImageResource(R.drawable.ic_baseline_play_circle_outline_24);
                }
            }
        }
    };



    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 实例化相关对象
        mContentResolver = getContentResolver();
        mCursorAdapter = new MediaCursorAdapter(MainActivity.this);

        // 绑定 ListView
        mPlaylist = findViewById(R.id.lv_playlist);

        // 设置适配器
        mPlaylist.setAdapter(mCursorAdapter);

        // 绑定 BottomNavigationView
        bottomNavigation = findViewById(R.id.navigation); // 拿到主布局的底部导航栏对象
        LayoutInflater.from(MainActivity.this).inflate(R.layout.bottom_media_toolbar, bottomNavigation, true); // 把 bottom_media_toolbar 放到主布局的底部导航栏中

        // 让底部导航栏一开始消失的
        bottomNavigation.setVisibility(View.GONE);

        // 绑定 BottomNavigationView 其他控件
        tvBottomTitle = bottomNavigation.findViewById(R.id.tv_bottom_title);
        tvBottomArtist = bottomNavigation.findViewById(R.id.tv_bottom_artist);
        ivAlbumThumbnail = bottomNavigation.findViewById(R.id.iv_thumbnail);
        ivPlay = bottomNavigation.findViewById(R.id.iv_play);

        // 权限查询
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE))
                requestPermissions(STRINGS, REQUEST_EXTERNAL_STORAGE); // 如果第一次点了是,下次不会弹出; 如果点了否,下次还会弹出
        } else initPlaylist();

        // 设置监听器
        mPlaylist.setOnItemClickListener(mPlaylistClickListener);
        if (ivPlay != null) ivPlay.setOnClickListener(ivPlayButtonClickListener);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_EXTERNAL_STORAGE) {// 判断授权结果
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
                initPlaylist();
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        if (mMediaPlayer == null) mMediaPlayer = new MediaPlayer();
    }

    @Override
    protected void onStop() {
        if (mMediaPlayer != null) {
            mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
        super.onStop();
    }

    private void initPlaylist() {
        // 游标对象查询MP3数据
        Cursor mCursor = mContentResolver.query(
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, // 表名
                null,   // 所有列
                SELECTION,        // where子句
                SELECTION_ARGS,   // where参数
                MediaStore.Audio.Media.DEFAULT_SORT_ORDER // 默认排序方式
        );
        mCursorAdapter.swapCursor(mCursor); // 交互游标,但不删除旧的游标
        mCursorAdapter.notifyDataSetChanged(); // 刷新数据
    }
}

  • MediaCursorAdapter.java
import android.content.Context;
import android.database.Cursor;
import android.provider.MediaStore;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.TextView;

/**
 *  为播放界面设置歌曲
 */

public class MediaCursorAdapter extends CursorAdapter {

    private final Context mContext;

    private final LayoutInflater mLayoutInflater;

    public MediaCursorAdapter(Context context) {
        super(context, null, 0);
        mContext = context;
        mLayoutInflater = LayoutInflater.from(mContext);
    }

    public static class ViewHolder {
        TextView tvTitle;
        TextView tvArtist;
        TextView tvOrder;
        View divider;
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
        // 直接写死
        View view = mLayoutInflater.inflate(R.layout.list_item, viewGroup, false);
        if (view != null) {
            ViewHolder viewHolder = new ViewHolder();
            viewHolder.tvTitle = view.findViewById(R.id.tv_title);
            viewHolder.tvArtist = view.findViewById(R.id.tv_artist);
            viewHolder.tvOrder = view.findViewById(R.id.tv_order);
            viewHolder.divider = view.findViewById(R.id.divider);
            view.setTag(viewHolder);
            return view;
        }
        return null;
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        ViewHolder viewHolder = (ViewHolder) view.getTag();

        int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
        int artistIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);

        String title = cursor.getString(titleIndex);
        String artist = cursor.getString(artistIndex);

        int position = cursor.getPosition();

        if (viewHolder != null) {
            viewHolder.tvTitle.setText(title);
            viewHolder.tvArtist.setText(artist);
            viewHolder.tvOrder.setText(Integer.toString(position + 1));
        }
    }
}

4.配置文件

  • AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.musiclist">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MaterialComponents.Light.NoActionBar"
        tools:targetApi="31">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值