实验目的
掌握MediaStore内容提供器的基本使用方法;
掌握AsyncTaskLoader、LoaderManager框架进行异步数据加载的方法;
实验要求
使用AsyncTaskLoader进行数据异步加载;
使用BottomNavigationView显示底部操作;
实验内容
本项目要实现音乐播放器列表功能,通过ContentResolver从MediaStore内容提供器中查询设备外部存储卡中的多媒体音乐信息。 并通过BottomNavigationView提供底部操作栏用于控制音乐播放(音乐播放功能在下一项目中实现)。项目最终运行效果如图1. 音乐播放器1运行效果所示。
步骤一,创建GGMusic项目
使用Android Studio创建一个名为GGMusic项目,选择Empty Activity。
步骤二,修改activity_main.xml布局文件
修改activity_main.xml布局文件,在其中添加ListView为根容器的唯一子控件,并设置ListView控件的相关属性,具体代码如下所示。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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">
<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" />
</android.support.constraint.ConstraintLayout>
注:ListView中的divider属性用于设置每一项的分割样式,这里设置为@android:color/transparent,表示为透明色。
步骤三,定义list_item.xml项布局文件
1. 新建list_item.xml文件
为在ListView中显示音乐文件信息,需自定义项布局文件list_item.xml。在Android Studio的工程窗口中右击res文件夹,选择【新建】->【Android Resource File】。在弹出的New Resource File对话框中,设置文件名为list_item,资源类型为Layout。
2. 修改list_item.xml布局
在音乐播放器列表中,每一项需要显示四项信息,分别是:
歌曲序号,在布局中使用TextView显示,id为tv_order;
歌曲名,在布局中使用TextView显示,id为tv_title;
歌手名,在布局中使用TextView显示,id为tv_artist;
分割线,用于表示每一项的分割,在布局中使用View显示,id为divider。
在list_item.xm文件中,根容器使用ContraintLayout,其代码如下所示。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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">
<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:text="Title"
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="0dp"
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"
/>
</android.support.constraint.ConstraintLayout>
注:分割线View控件(id:divider)的background属性为自定义的颜色@color/colorDivider,应在/res/values/colors.xml文件中进行定义,在本项目中设置其颜色值为#4DAAA9A9。
步骤四,申请读取外部存储权限
通过MediaStore读取设备多媒体音乐文件时,需要申请读取外部存储权限,这可分为两个部分进行设置。
1. AndroidManifest配置清单中申请权限
在AndroidManifest配置清单中使用申请读取外部存储权限,具体代码如下所示。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.glriverside.xgqin.ggmusic">
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
...
</application>
</manifest>
2. 动态申请权限及权限检查
在Android 6.0版本后,系统对权限的管理更为严格,部分权限在配置清单文件中申请是无效的, 这需要App在运行时动态进行申请,否则App运行会出现权限异常报错。
动态申请权限涉及到以下几个函数接口:
checkSelfPermission,该方法是ContextCompat类的静态方法,用于检测指定的权限是否已经获得授权;
shouldShowRequestPermissionRationale,该方法是ActivityCompat类的静态方法,用于判断是否需要向用户展示权限申请时的说明信息;
requestPermissions,该方法是Activity类的方法,用于动态申请权限;
onRequestPermissionsResult,该方法是OnRequestPermissionsResultCallback接口方法,通常由Activity类进行覆盖,用于检测通过requestPermissions方法动态申请权限的结果。
2.1 定义所需申请权限数组
在MainActivity中首先定义所需申请的权限数组以及requestCode,这两个参数将在requestPermissions方法中使用,代码如下所示。
public class MainActivity extends AppCompatActivity {
private final int REQUEST_EXTERNAL_STORAGE = 1;
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
...
}
2.2 在MainActivity中检测所需权限及权限申请
权限检测及申请通常作为一个整体进行,权限检测及申请一般在需要使用到某权限的业务代码逻辑中进行处理。 完成权限检测及申请,需要分别调用checkSelfPermission()、shouldShowRequestPermissionRationale()、 requestPermissions()三个方法完成。
在本项目中,我们在MainActivity活动的onCreate()方法中进行权限检测及申请操作,具体代码如下所示。
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(
MainActivity.this,
Manifest.permission.READ_EXTERNAL_STORAGE)) {
} else {
requestPermissions(PERMISSIONS_STORAGE,
REQUEST_EXTERNAL_STORAGE);
}
} else {
initPlaylist();
}
}
首先使用checkSelfPermission()方法检测App是否已经获得指定的权限(在这里为:Mainfest.permission.READ_EXTERNAL_STORAGE)。该函数的两个参数分别为:
String,权限字符串,可通过Manifest.permission静态类定义的权限字符串进行指明;
如果App已经获得权限,则该函数返回值为PERMISSION_GRANTED,否则返回PERMISSION_DENIED。
因此根据checkSelfPermission()方法的返回值决定下一步的操作:
情况1:返回PackageManager.PERMISSION_GRANTED,App已经获得权限,可直接执行相应的操作;
情况2:返回PackageManager.PERMISSION_DENIED,App未获得权限,需申请权限,后根据申请权限结果决定是否执行相应的操作。
在情况1中,直接调用initPlaylist()方法通过MediaStore内容提供器查询外部存储卡上的多媒体音乐文件并进行显示,该操作在步骤五中进行描述。
在情况2中,调用了shouldShowRequestPermissionRationale()方法,如果App之前请求过此权限但用户拒绝了请求,此方法将返回true;如果用户在过去拒绝了权限请求,并在权限请求系统对话框中选择了【Don’t ask again】选项,此方法将返回false。 根据此不同,需要实现不同的应对方式。通常来说,返回为true时,App可自定义消息提示用户为何需要申请该权限,以便用户充分理解授权该权限的危险。返回为false时,再次向用户申请权限。
2.3 在MainActivity中处理权限申请结果
通过requestPermissions()方法申请权限后,可以通过onRequestPermissionsResult()方法获取申请权限结果,该函数包含三个参数,分别是:
int requestCode,对应于requestPermissions()方法中指定的requestCode,可通过该参数判断请求的具体参数;
String[ ] permissions,requestPermissions()方法中所申请的权限数组;
int[ ] grantResults,该数组对应于permissions对应的申请权限结果,其值为PackageManager.PERMISSION_GRANTED或PackageManager.PERMISSION_DENIED。
onRequestPermissionsResult()方法在MainActivity进行重载,具体代码如下所示。
public class MainActivity extends AppCompatActivity {
...
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_EXTERNAL_STORAGE:
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initPlaylist();
}
break;
default:
break;
}
}
}
步骤五,通过ContentResolver查询MediaStore多媒体文件
1. 定义ContentResolver的query方法的查询条件
在本项目中需要查询MediaStore中的多媒体音频文件(即MP3文件),可以通过MediaStore.Audio.Media这个合约类来查询。 该类包含了用于查询多媒体音频文件所需的URI及元数据定义。其中本项目中使用到的两个元数据为:
MediaStore.Audio.Media.IS_MUSIC,表示音频文件是否属于音乐类型的元数据字段;
MediaStore.Audio.Media.MIME_TYPE,表示音频文件的MIME类型,其中MP3文件对应的MIME类型为audio/mpeg。
定义的查询条件包括SELECTION字符串及SELECTION_ARGS字符串数组,这两个分别对应与ContentResolver.query()方法中的selection参数 及selectionArgs参数,具体代码如下所示。
public class MainActivity extends AppCompatActivity {
private ContentResolver mContentResolver;
private ListView mPlaylist;
private MediaCursorAdapter mCursorAdapter;
private final String SELECTION =
MediaStore.Audio.Media.IS_MUSIC + " = ? " + " AND " +
MediaStore.Audio.Media.MIME_TYPE + " LIKE ? ";
private final String[] SELECTION_ARGS = {
Integer.toString(1),
"audio/mpeg"
};
...
2. 实例化MediaCursorAdapter适配器对象并在ListView中绑定该适配器
通过ContentResolver.query()方法查询得到的返回结果为Cursor类型,该类型对象指向查询结果集, 如果将该Cursor对象绑定至ListView中,则需要通过CursorAdapter适配器类型。
代码中的MediaCursorAdapter适配器是从CursorAdapter继承而来的子类,主要负责将ListView所需 的项视图布局的加载及数据的绑定,对于MediaCursorAdapter的介绍具体见步骤六。 实例化MediaCursorAdapter适配器对象并与ListView进行绑定的操作代码如下所示。
label={lst:code10_init_adapter}, escapeinside=``]
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContentResolver = getContentResolver();
mCursorAdapter = new MediaCursorAdapter(MainActivity.this);
mPlaylist.setAdapter(mCursorAdapter);
...
}
...
}
3. 使用ContentResolver查询数据
使用ContentResolver查询数据的代码放在initPlaylist()方法中,该方法为MainActivity的私有方法, 该方法是在权限检测及权限申请时两种情况下被调用的,具体见步骤四。
在initPlaylist()方法中,通过ContentResolver.query()方法进行查询数据,该方法包含的参数有:
Uri uri,查询的Content provider的Uri;
String[] projections,查询的字段;
String selection,查询语句对应的where子句;
String[] selectionArgs,与selection共同构成where子句;
String sortOrder,查询的结果排序条件;
在项目中,query()方法的第一个参数被设置为MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,表示查询 MediaStore中保存在外部存储设备中的多媒体音频文件。第二个参数设为null,表示查询多媒体音频文件的所有属性字段数据; 第三及第四个参数为SELECTION、SELECTION_ARGS,表示查询MIME类型为MP3的音乐文件;第五个参数设定为MediaStore.Audio.Media.DEFAULT_SORT_ORDER表示按照默认的字段进行排序。
public class MainActivity extends AppCompatActivity {
...
private void initPlaylist() {
mCursor = mContentResolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
null,
SELECTION,
SELECTION_ARGS,
MediaStore.Audio.Media.DEFAULT_SORT_ORDER
);
mCursorAdapter.swapCursor(cursor);
mCursorAdapter.notifyDataSetChanged();
}
}
步骤六,定义MediaCursorAdapter适配器类
MediaCursorAdapter适配器类继承至CursorAdapter,其主要用于将ContentResolver.query()方法查询的数据集与ListView做绑定。 在MediaCursorAdapter类中需要覆盖基类CursorAdapter的两个方法:
newView(Context context, Cursor cursor, ViewGroup viewGroup),该方法三个参数,其中Cursor表示ListView当前项的游标对象,可以通过该对象直接读取相应的多媒体音乐属性字段;ViewGroup表示项视图布局的父容器(在这里是ListView控件对象);
bindView(View view, Context context, Cursor cursor) ,该方法三个参数,其中View表示当前项的布局,Cursor表示当前项对应的游标对象。
1. 定义MediaCursorAdapter构造函数
MediaCursorAdapter构造函数主要完成调用基类构造函数,获得LayoutInflater对象等工作,具体代码如下所示。
public class MediaCursorAdapter extends CursorAdapter {
private Context mContext;
private LayoutInflater mLayoutInflater;
public MediaCursorAdapter(Context context) {
super(context, null, 0);
mContext = context;
mLayoutInflater = LayoutInflater.from(mContext);
}
...
}
2. 重写newView()方法
newView()方法中主要做项视图布局的加载操作,通过LayoutInflater对象加载指定的项视图布局。为了提高效率,在MediaCursorAdapter类中还定义了ViewHolder内部类,用于暂存加载项视图布局后的各控件对象,避免通过findViewById()的方法重复进行查找绑定控件对象。
newView()方法是当ListView控件响应用户上下滑动时,新出现的Item需要加载项布局时被调用。我们首先使用LayoutInflater对象的inflate()方法加载了名为R.layout.list_item的项视图布局文件。
通过实例化ViewHolder对象,并初始化该对象的几个控件属性绑定了加载好的项布局中的控件,并最后通过itemView的setTag()方法保存至itemView中以便在bindView()方法中从itemView中取出直接使用,具体代码如下所示。
public class MediaCursorAdapter extends CursorAdapter {
...
@Override
public View newView(Context context,
Cursor cursor, ViewGroup viewGroup) {
View itemView = mLayoutInflater.inflate(R.layout.list_item,
viewGroup, false);
if (itemView != null) {
ViewHolder vh = new ViewHolder();
vh.tvTitle = itemView.findViewById(R.id.tv_title);
vh.tvArtist = itemView.findViewById(R.id.tv_artist);
vh.tvOrder = itemView.findViewById(R.id.tv_order);
vh.divider = itemView.findViewById(R.id.divider);
itemView.setTag(vh);
return itemView;
}
return null;
}
...
public class ViewHolder {
TextView tvTitle;
TextView tvArtist;
TextView tvOrder;
View divider;
}
}
3. 重写bindView()方法
在bindView()方法中,首先通过第一个参数View对象的getTag()方法获取暂存的项布局当中的所有控件对象。
其次通过第三个参数Cursor对象的getColumnIndex(),getString()两个方法分别获取到所需多媒体音频文件属性字段,其中的字段包括:
MediaStore.Audio.Media.TITLE,表示多媒体音频文件的歌曲名;
MediaStore.Audio.Media.ARTIST,表示多媒体音频文件的歌手名。
将获取到的歌曲名、歌手名、当前歌曲在结果集的序号设置到项布局的相应控件对象属性上,即可完成bindView()方法的操作,具体代码如下所示。
public class MediaCursorAdapter extends CursorAdapter {
...
@Override
public void bindView(View view,
Context context, Cursor cursor) {
ViewHolder vh = (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 (vh != null) {
vh.tvTitle.setText(title);
vh.tvArtist.setText(artist);
vh.tvOrder.setText(Integer.toString(position+1));
}
}
}
步骤七,编译、部署APK
编译并部署GGMusic项目的APK,运行效果如图1. 音乐播放器1运行效果所示。项目目前仅能读取设备外部存储卡上的多媒体音频文件,如何播放多媒体音频文件在下一个实验中进行介绍。
实验小结
通过本次实验,你应该掌握了如下知识内容:
使用ContentResolver、MediaStore查询设备多媒体音频文件;
使用requestPermissions、onRequestPermissionsResult等方法进行动态权限检测及申请;
使用自定义MediaCursorAdapter适配器类进行ListView项视图布局加载及数据绑定;