需要拥有一部 Android 手机
通过数据线把手机连接到电脑上。然后进入设置→系统→ 开发者选项界面,并在这个界面中选中USB调试选项
从 Android 4.2 系统开始,开发者选项默认是隐藏的,你需要先进入到“关于手机”界 面,然后对着最下面的版本号那一栏连续点击,就会让开发者选项显示出来
1.通知
通知(notification)是Android系统中比较有特色的一个功能,当某个应用程序希望向用户发 出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知来实现。发出一条通知 后,手机最上方的状态栏中会显示一个通知的图标,下拉状态栏后可以看到通知的详细内容。
1.通知渠道(第三版)
通知渠道就是每条通知都要属于一个对应的渠道。每个应用程序都可以 自由地创建当前应用拥有哪些通知渠道,但是这些通知渠道的控制权是掌握在用户手上的。用 户可以自由地选择这些通知渠道的重要程度,是否响铃、是否振动或者是否要关闭这个渠道的通知。
对于每个应用来说,通知渠道的划分是非常考究的,因为通知渠道一旦创建之后就不能再修改 了,因此开发者需要仔细分析自己的应用程序一共有哪些类型的通知,然后再去创建相应的通 知渠道。这里我们参考一下Twitter的通知渠道划分
可以看到,Twitter根据自己的通知类型,对通知渠道进行了非常详细的划分。这样用户的自主选择性就比较高了,也就大大降低了用户因不堪其垃圾通知的骚扰而将应用程序卸载的概率。
而我们的应用程序如果想要发出通知,也必须创建自己的通知渠道才行,下面我们就来学习一 下创建通知渠道的详细步骤。
以下操作我将第三版中的kotlin语言改为了java版本(实际上我不确定是否是正确的改动)
首先需要一个NotificationManager对通知进行管理,可以通过调用Context的 getSystemService()方法获取。getSystemService()方法接收一个字符串参数用于确定 获取系统的哪个服务,这里我们传入Context.NOTIFICATION_SERVICE即可。因此,获取 NotificationManager的实例就可以写成:
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
接下来要使用NotificationChannel类构建一个通知渠道,并调用NotificationManager的 createNotificationChannel()方法完成创建。由于NotificationChannel类和 createNotificationChannel()方法都是Android 8.0系统中新增的API,因此我们在使用 的时候还需要进行版本判断才可以,写法如下:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = NEW NotificationChannel(channelId, channelName, importance);
manager.createNotificationChannel(channel);
}
创建一个通知渠道至少需要渠道ID、渠道名称以及重要等级这3个参数
渠道ID可以随便定义,只要保证全局唯一性就可以。
渠道名称是给用户看的,需要可以清楚地表达这个渠道的用途。
通知的重要等级主要有IMPORTANCE_HIGH、IMPORTANCE_DEFAULT、 IMPORTANCE_LOW、IMPORTANCE_MIN这几种,对应的重要程度依次从高到低。不同的重要等级会决定通知的不同行为,后面我们会通过具体的例子进行演示。当然这里只是初始状态下的重要等级,用户可以随时手动更改某个通知渠道的重要等级,开发者是无法干预的。
通知的用法还是比较 灵活的,既可以在Activity里创建,也可以在BroadcastReceiver里创建,当然还可以在后面 我们即将学习的Service里创建。相比于BroadcastReceiver和Service,在Activity里创建通 知的场景还是比较少的,因为一般只有当程序进入后台的时候才需要使用通知。
不过,无论是在哪里创建通知,整体的步骤都是相同的,下面我们就来学习一下创建通知的详细步骤。
首先需要使用一个Builder构造器来创建Notification对象,但问题在于,Android系统的每 一个版本都会对通知功能进行或多或少的修改,API不稳定的问题在通知上凸显得尤其严重,比 方说刚刚介绍的通知渠道功能在Android 8.0系统之前就是没有的。那么该如何解决这个问题 呢?其实解决方案我们之前已经见过好几回了,就是使用AndroidX库中提供的兼容API。 AndroidX库中提供了一个NotificationCompat类,使用这个类的构造器创建 Notification对象,就可以保证我们的程序在所有Android系统版本上都能正常工作了,代码如下所示:
Notification notification = new NotificationCompat.Builder(context, channelId).build();
NotificationCompat.Builder的构造函数中接收两个参数:第一个参数是context,这个 没什么好说的;第二个参数是渠道ID,需要和我们在创建通知渠道时指定的渠道ID相匹配才行。
ps:这个方法似乎已被弃用,他可以只传入context参数,id(是字符串类型)可以不传入,这部分的学习我以第三版为准。
Notification notification = new NotificationCompat.Builder(context)
.setContentTitle("This is content title")
.setContentText("This is content text")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.drawable.large_icon))
.build();
上述代码中一共调用了4个设置方法,下面我们来一一解析一下。setContentTitle()方法用 于指定通知的标题内容,下拉系统状态栏就可以看到这部分内容。setContentText()方法用 于指定通知的正文内容,同样下拉系统状态栏就可以看到这部分内容。setSmallIcon()方法 用于设置通知的小图标,注意,只能使用纯alpha图层的图片进行设置,小图标会显示在系统状 态栏上。setLargeIcon()方法用于设置通知的大图标,当下拉系统状态栏时,就可以看到设置的大图标了。
(第三版与第二版有细微的差别,第二版中多了一个.setWhen(System.currentTimeMillis()) 设置这个设置用于指定通知被创建的时间,以毫秒为单位,我以第三版为准)
以上工作都完成之后,只需要调用NotificationManager的notify()方法就可以让通知显示 出来了。notify()方法接收两个参数:第一个参数是id,要保证为每个通知指定的id都是不 同的;第二个参数则是Notification对象,这里直接将我们刚刚创建好的Notification对象传入即可。因此,显示一个通知就可以写成:
manager.notify(1, notification);
下面就让我们通过一个具体的例子来看一 看通知到底是长什么样的
新建一个NotificationTest项目,并添加一个按钮
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送通知"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
接下来修改 MainActivity中的代码:
package com.example.notificationtest;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("normal", "normal", NotificationManager.IMPORTANCE_DEFAULT);
manager.createNotificationChannel(channel);
}
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Notification notification = new NotificationCompat.Builder(MainActivity.this,"normal")
.setContentTitle("这是标题")
.setContentText("这是内容")
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(
getResources(),R.mipmap.ic_launcher))
.build();
manager.notify(1,notification);
}
});
}
}
现在可以来运行一下程序了,其实MainActivity一旦打开之后,通知渠道就已经创建成功了, 我们可以进入应用程序设置当中查看。依次点击设置→应用和通知→NotificationTest→通知
下拉系统状态栏可以看到该通知的详细信息
我们应该会下意识地认为这条通知是可以点击的。但是当我们去点击它的时候,会发现没有任何效果。
我们需要使用PendingIntent
PendingIntent的用法同样很简单,它主要提供了几个静态方法用于获取PendingIntent的实例,可以根据需求来选择是使用getActivity()方法、getBroadcast()方法,还是 getService()方法。这几个方法所接收的参数都是相同的:
第一个参数依旧是Context;
第二个参数一般用不到,传入0即可;
第三个参数是一个Intent对象,我们可以通过这个对象构建出 PendingIntent 的“意图”。
第四个参数用于确定 PendingIntent 的行为,有 FLAG_ONE_ SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT 和 FLAG_UPDATE_CURRENT 这 4 种值可选,每种值的具体含义你可以查看文档,通常情况下这个参数传入 0 就可以了。
我们回头看一下NotificationCompat.Builder这个构造器,它还可以再连缀一个 setContentIntent()方法,接收的参数正是一个 PendingIntent 对象。因此,这里就可以通过 PendingIntent 构建出一个延迟执行的“意图”,当用户点击这条通知时就会执行相应的逻辑。
现在我们来优化一下NotificationTest项目
首先需要准备好另一个活动,右击 com.example.notificationtest 包→New→Activity→Empty Activity,新建 NotificationActivity,布局起名为 notification_layout。然后修改 notification_layout.xml 中的代码:
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是notification的活动"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
接下来修改 MainActivity 中的代码
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this, NotificationActivity.class);
PendingIntent pi = PendingIntent.getActivity(MainActivity.this, 0, intent, PendingIntent.FLAG_MUTABLE);
Notification notification = new NotificationCompat.Builder(MainActivity.this,"normal")
.setContentTitle("这是标题")
.setContentText("这是内容")
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(
getResources(),R.mipmap.ic_launcher))
.setContentIntent(pi)
.build();
manager.notify(1,notification);
}
不知道为什么pi最后一个参数写0会报错,提示修改为
PendingIntent.
FLAG_MUTABLE
这里先是使用 Intent 表达启动 NotificationActivity 的“意图”,然后将构建好的 Intent 对象传入到 PendingIntent 的 getActivity()方法里,得到 PendingIntent 的实例,在 NotificationCompat.Builder 中调用 setContentIntent()方法,把它作为参数传入即可
现在运行并点击通知就会就会打开NotificationActivity的界面
但是通知还在没有消失,如果我们没有在代码中对该通知进行取消,它就会一直显示在系统的状态栏上。解决的方法有两种:一种是在 NotificationCompat.Builder中再连缀一个setAutoCancel()方法,一种是显式地调用 NotificationManager的cancel()方法将它取消。
第一种:
Notification notification = new NotificationCompat.Builder(this)
...
.setAutoCancel(true)
.build();
第二种:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.notification_layout);
NotificationManager manager = (NotificationManager) getSystemService
(NOTIFICATION_SERVICE);
manager.cancel(1);
}
这里我们在cancel()方法中传入了1,这个1是我们在创建通知的时候给每条通知指定的id。当时我们给这条通知设置的id就是1。因此,如果你想取消哪条通知,在 cancel()方法中传入该通知的id就行了。
通知还有很多其他用法
比如:
.setSound(Uri.fromFile(new File("/system/media/audio/ringtones/Luna.ogg")))
它可以在通知发出的时候播放一段指定目录下的音频
还可以设置手机静止和振动的时长,以毫秒为单位。下标为 0 的值表示手机静止的时长,下标为 1 的值表示手机振动的时长,下标为 2 的值又表示手机静止的时长,以此类推。
手机在通知到来的时候立刻振动 1 秒,然后静止 1 秒,再振动 1 秒
.setVibrate(new long[] {0, 1000, 1000, 1000 })
震动需要权限声明:
<uses-permission android:name="android.permission.VIBRATE" />
(第二版中这里有一部分控制手机前置led灯的教学,由于当前手机已经取消了该设计因此我没有写出,实际上第二版这部分的这些功能我都没有进行实际的实现)
这是手机的默认提示效果,它会根据当前手机的环境来决定播放什么铃声,以及如何振动
.setDefaults(NotificationCompat.DEFAULT_ALL)
以下是第三版的富文本显示内容
将通知的内容“这是内容”改为一段很长的文本,他会省略掉太长的内容
但是如果你真的非常需要在通知当中显示一段长文字,Android 也是支持的,通过 setStyle() 方法就可以做到,将显示内容的那一行代码改为:
.setStyle(new NotificationCompat.BigTextStyle().bigText("打完啦决斗链接阿诗勒隼登记啦金娃溜达鸡拉卡拉手机打卡拉季阿拉开挖机到啦科技阿卡拉手机打克拉拉"))
我们在 setStyle()方法中创建了一个 NotificationCompat.BigTextStyle 对象,这个对 象就是用于封装长文字信息的,我们调用它的 bigText()方法并将文字内容传入就可以了
还可以显示图片,需要我们自己准备一张图片放在drawable文件夹下并且命名为big_image:
(似乎图片与富文本不能同时显示,即便两个同时写也不会报错)
.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(getResources(), R.drawable.big_image)))
关于通知的重要等级,你可以发现目前我们做的通知并不会弹出,只会发出声音然后默默的躺在通知栏里。我们可以通过修改重要等级来使它弹出弹窗但是需要注意的是,现在开发者只能在创建通知渠道的时候为它指定初始的重要等级,如果用户不认可这个重要等级的话,可以随时进行修改,开发者对此无权再进行调整和变更,因为通知渠道一旦创建就不能再通过代码修改了。
通知的重要等级一共有 5 个常量值可选:PRIORITY_ DEFAULT 表示默认的重要程度,和不设置效果是一样的;
IMPORTANCE_MIN 表示最低的重要程度, 系统可能只会在特定的场景才显示这条通知,比如用户下拉状态栏的时候;
IMPORTANCE_LOW 表示较低的重要程度,系统可能会将这类通知缩小,或改变其显示的顺序,将其排在更重要的通知之 后;
IMPORTANCE_HIGH 表示较高的重要程度,系统可能会将这类通知放大,或改变其显示的顺序, 将其排在比较靠前的位置;
IMPORTANCE_MAX 表示最高的重要程度,这类通知消息必须要让用户立刻看到,甚至需要用户做出响应操作。
这里我们将我们之前创建通知渠道时设置的重要程度修改为high
NotificationChannel channel = new NotificationChannel("normal", "normal",NotificationManager.IMPORTANCE_HIGH);
然后删除软件并重新安装(原书是重新创建一个通知渠道,觉得改更方便一点点)
之后运行就是这个效果了
2.调用摄像头和相册
新建一个 CameraAlbumTest 项目
创建一个按钮用来拍照
创建一个ImageView用来显示照片
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="@+id/button"
app:layout_constraintStart_toStartOf="@+id/button"
app:layout_constraintTop_toBottomOf="@+id/button"
app:srcCompat="?attr/selectableItemBackground" />
然后开始编写调用摄像头的逻辑
package com.example.cameraalbumtest;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import androidx.activity.EdgeToEdge;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
public static final int TAKE_PHOTO = 1;
private ImageView picture;
private Uri imageUri;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
Button button = findViewById(R.id.button);
picture = findViewById(R.id.imageView);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 创建 File 对象,用于存储拍照后的图片
File outputImage = new File(getExternalCacheDir(),
"output_image.jpg");
try {
if (outputImage.exists()) {
outputImage.delete();
}
outputImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT >= 24) {
imageUri = FileProvider.getUriForFile(MainActivity.this,
"com.example.cameraalbumtest.fileprovider", outputImage);
} else {
imageUri = Uri.fromFile(outputImage);
}
// 启动相机程序
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
switch (requestCode){
case TAKE_PHOTO:
if (resultCode == RESULT_OK){
try {
// 将拍摄的照片显示出来
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver()
.openInputStream(imageUri));
picture.setImageBitmap(bitmap);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
break;
default:
break;
}
super.onActivityResult(requestCode, resultCode, data);
}
}
首先这里创建了一个File对象,用于存放摄像头拍下的图片,这里我们把图片命名为output_ image.jpg,并将它存放在手机 SD 卡的应用关联缓存目录下。
应用关联缓存目录就是指 SD 卡中专门用于存放当前应用缓存数据的位置,调用 getExternalCacheDir()方法可以得到这个目录,具体的路径是/sdcard/Android/data//cache。
从 Android 6.0 系统开始,读写 SD 卡被列为了危险权限,如果将图片存放在 SD 卡的任何其他目录,都要进行运行时权限处理才行,而使用应用关联目录则可以跳过这一步。
接着会进行一个判断,如果运行设备的系统版本低于 Android 7.0,就调用 Uri 的 fromFile() 方法将 File 对象转换成 Uri 对象,这个 Uri 对象是 output_image.jpg 这张图片的本地真实路径。否则,就调用 FileProvider 的 getUriForFile()方法将 File 对象转换成一个封装过的 Uri 对象。getUriForFile()方法接收 3 个参数,第一个参数要求传入 Context 对象,第二个参数可以是任意唯一的字符串,第三个参数则是我们刚刚创建的 File 对象。之所以要进行这样一层转换,是因为从 Android 7.0 系统开始,直接使用本地真实路径的 Uri 被认为是不安全的,会抛出一个 FileUriExposedException 异常。而 FileProvider 则是一种特殊的内容提供器,它使用了和内容提供器类似的机制来对数据进行保护,可以选择性地将封装过的 Uri 共享给外部,从而提高了应用的安全性。
接下来构建出了一个 Intent 对象,并将这个 Intent 的 action 指定为 android.media. action.IMAGE_CAPTURE,再调用 Intent 的 putExtra()方法指定图片的输出地址,这里填入刚刚得到的 Uri 对象,最后调用 startActivityForResult()来启动活动。
由于我们使用的是一个隐式 Intent,系统会找出能够响应这个 Intent 的活动去启动,这样照相机程序就会被打开,拍下的照片将会输出到 output_image.jpg 中。
刚才我们是使用 startActivityForResult()来启动活动的,因此拍完照后会有结果返回到 onActivityResult()方法中。如果拍照成功,就调用 BitmapFactory 的 decodeStream()方法将 output_image.jpg 这张照片解析成 Bitmap 对象,然后把它设置到 ImageView 中显示。
不过刚才提到了内容提供器,那么我们要在 AndroidManifest.xml 中对内容提供器进行注册,如下所示:
在application标签内
<provider
android:authorities="com.example.cameraalbumtest.fileprovider"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
此时@xml这里报错是正常的,因为我们还没有创建
右击res目录→New→Directory,创建一个xml目录,接着右击xml目录→New→File,创建一个file_paths.xml文件。然后修改file_paths.xml文件中的内容
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="/" />
</paths>
external-path就是用来指定Uri共享路径的,name属性的值可以随便填,path属性的值表 示共享的具体路径。这里使用一个单斜线表示将整个SD卡进行共享,当然你也可以仅共享存放 output_image.jpg这张图片的路径。
新建一个按钮用来读取相册
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="相册"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.494"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
修改代码逻辑:
Button button1 = findViewById(R.id.button2);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//打开文件选择器
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
//指定只显示图片
intent.setType("image/*");
startActivityForResult(intent,CHOOSE_PHOTO);
}
});
//并在onActivityResult方法中的switch中添加
case CHOOSE_PHOTO:
if (resultCode == RESULT_OK && data != null){
if (data.getData() != null){
Uri uri = data.getData();
//将选择的图片显示
try {
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
picture.setImageBitmap(bitmap);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
在按钮点击事件里,我们先构建了一个Intent对象,并将它的 action指定为Intent.ACTION_OPEN_DOCUMENT,表示打开系统的文件选择器。接着给这个 Intent对象设置一些条件过滤,只允许可打开的图片文件显示出来,然后调用 startActivityForResult()方法即可。
在调用startActivityForResult()方法 的时候,我们给第二个参数传入的值CHOOSE_PHOTO变成了fromAlbum,这样当选择完图片回到 onActivityResult()方法时,就会进入fromAlbum的条件下处理图片。
3.播放多媒体文件
这里学习播放音频和视频
1.音频
在Android中播放音频文件一般是使用MediaPlayer类实现的,它对多种格式的音频文件提供 了非常全面的控制方法,从而使播放音乐的工作变得十分简单。
新建一个PlayAudioTest项目添加两个按钮,用来进行音频的播放/暂停和结束
(我只加了俩,原文加了三个)
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="播放"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="停止"
app:layout_constraintEnd_toEndOf="@+id/button1"
app:layout_constraintStart_toStartOf="@+id/button1"
app:layout_constraintTop_toBottomOf="@+id/button1" />
MediaPlayer可以用于播放网络、本地以及应用程序安装包中的音频。这里我们以播放应用程序安装包中的音频来举例。
Android Studio允许我们在项目工程中创建一个assets目录,并在这个目录下存放任意文件和子目录,这些文件和子目录在项目打包时会一并被打包到安装文件中,然后我们在程序中就可以借助AssetManager这个类提供的接口对assets目录下的文件进行读取。 首先创建assets目录,它必须创建在app/src/main这个目录下面,也就是和java、 res这两个目录是平级。右击app/src/main→New→Directory,在弹出的对话框中输 入“assets”,目录就创建完成了。 我们需要自行准备一份.mp3文件放入asset文件夹中
然后修改MainActivity中的代码
package com.example.playaudiotest;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.FaceDetector;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
private MediaPlayer mediaPlayer = new MediaPlayer();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
initMediaPlayer();
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (!mediaPlayer.isPlaying()){
mediaPlayer.start();//开始播放
}else {
mediaPlayer.pause();//如果是播放状态就暂停
}
}
});
Button button1 = findViewById(R.id.button2);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mediaPlayer.reset();//停止播放
initMediaPlayer();//重新回到准备状态
}
});
}
private void initMediaPlayer(){
AssetManager assetManager = getAssets();
try {
AssetFileDescriptor faceDetector = assetManager.openFd("music.mp3");
mediaPlayer.setDataSource(faceDetector.getFileDescriptor(),faceDetector.getStartOffset(),faceDetector.getLength());
mediaPlayer.prepare();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mediaPlayer.stop();
mediaPlayer.release();
}
}
可以看到,在类初始化的时候,我们就先创建了一个MediaPlayer的实例,然后在 onCreate()方法中调用initMediaPlayer()方法,为MediaPlayer对象进行初始化操作。
在initMediaPlayer()方法中,首先通过getAssets()方法得到了一个AssetManager的实例,AssetManager可用于读取assets目录下的任何资源。接着我们调用了openFd()方法将音频文件句柄打开,后面又依次调用了setDataSource()方法和prepare()方法,为 MediaPlayer做好了播放前的准备。
接下来我们看一下各个按钮的点击事件中的代码。当点击“播放/暂停”按钮时会进行判断,如果当前 MediaPlayer没有正在播放音频,则调用start()方法开始播放,否则就pause()暂停。
当点击“停止”按钮时会直接调用reset()方法将MediaPlayer重置为刚刚创建的状态,然后重新调用一遍initMediaPlayer()方法。
最后在onDestroy()方法中,我们还需要分别调用stop()方法和release()方法,将与 MediaPlayer相关的资源释放掉。
这样就完成了
2.视频
播放视频文件其实并不比播放音频文件复杂,主要是使用VideoView类来实现的。这个类将视 频的显示和控制集于一身,我们仅仅借助它就可以完成一个简易的视频播放器。VideoView的 用法和MediaPlayer也比较类似
新建PlayVideoTest项目(我是直接使用的上面播放音频的项目进行,所以下文会有所不同,请注意)
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="视频播放/暂停"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="关闭视频"
app:layout_constraintBottom_toTopOf="@+id/button4"
app:layout_constraintEnd_toEndOf="@+id/button4"
app:layout_constraintStart_toStartOf="@+id/button4" />
<VideoView
android:id="@+id/videoView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/button5"
tools:layout_editor_absoluteX="55dp"
tools:ignore="MissingConstraints" />
放置了两个按钮用来控制播放/暂停与关闭
并且放置了一个VideoView来显示视频
VideoView不支持直接播放assets目录下的视频资源,但res目录下允许我们再创建一个raw目录,像诸 如音频、视频之类的资源文件也可以放在这里,并且VideoView是可以直接播放这个目录下的视频资源的。
现在右击app/src/main/res→New→Directory,在弹出的对话框中输入“raw”,完成raw目录的创建,并把要播放的视频资源放在里面。这里需要自己提前准备一个video.mp4资源
然后修改MainActivity中的代码:
private VideoView videoView;//类变量
Uri uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + getPackageName() + "/raw/video");
videoView = findViewById(R.id.videoView);
videoView.setVideoURI(uri);
Button button2 = findViewById(R.id.button4);
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (!videoView.isPlaying()){
videoView.start();
}else videoView.pause();
}
});
Button button3 = findViewById(R.id.button5);
button3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
videoView.resume();
videoView.pause();
}
});
@Override
protected void onDestroy() {
super.onDestroy();
videoView.suspend();
}
注意Uri的设置与书中不一样,如果抄书会无法找到资源文件,我搜索了相关资料Uri调用raw文件夹文件时的格式是:
Uri uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/raw/" + fileName);
它和前面播放音频的代码比较类似。
首先在 onCreate()方法中调用了Uri.parse()方法,将raw目录下的video.mp4文件解析成了一个Uri对象,这里使用的写法是Android要求的固定写法。
然后调用VideoView的 setVideoURI()方法将刚才解析出来的Uri对象传入,这样VideoView就初始化完成了。
当点击“播放/暂停”按钮时会判断,如果当前没有正在播放视频, 则调用start()方法开始播放,否则调用pause()方法暂停。
当点击“关闭”按钮时会调用 resume()方法从头播放视频,调用pause()方法使视频暂停。
最后在onDestroy()方法中调用suspend()方法,将VideoView所占用的资源释放掉。