提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方,欢迎各位在评论中指出。
文章目录
一、创建通知渠道
通知功能可以让我们的应用程序在后台运行时也能发出一些提示信息。发送一条通知后,会在通知栏显示一个通知图标用来提示用户,下拉状态栏后可以看到通知的详细内容。我们的如果想让我们的应用发出通知,那么必须要创建自己的通知渠道才行。
创建通知渠道需要:创建NotificationManager —> 构建NotificationChannel —> 通过NotificationManager完成NotificationChannel的创建。
1.1 创建通知管理器NotificationManager
通知管理器NotificationManager用于对通知进行管理,我们通过getSystemService(Context.NOTIFICATION_SERVICE)方法可以获取系统的通知管理器服务。于是,代码可以写成如下形式:
//创建NotificationManager
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
1.2 创建通知渠道NotificationChannel
我们可以使用NotificationChannel类构建一个通知渠道,并调用NotificationManager的createNotificationChannel()方法完成创建。这些都是Android 8.0系统中新增的API,因此我们需要在运行时增加版本判断逻辑:
//创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
渠道id、 渠道名称、 重要等级
val notificationChannel = NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
notificationManager.createNotificationChannel(notificationChannel)
}
创建一个通知渠道需要传入“渠道id”、“渠道名称”、“重要等级”这三个参数:
- 渠道id:用于唯一标识这个通知渠道。
- 渠道名称:通知渠道的名称,用于在通知栏中显示。
- 重要等级:通知渠道的重要性级别,用于控制通知的显示方式。
通知渠道的重要性级别有以下五种:
- IMPORTANCE_NONE:通知无打断性,不会发出声音或振动。
- IMPORTANCE_MIN:通知为静默状态,不会发出声音或振动。
- IMPORTANCE_LOW:通知以非打扰方式显示,例如以小图标的形式。
- IMPORTANCE_DEFAULT:通知以默认方式显示,通常会发出提示音。
- IMPORTANCE_HIGH:通知以高优先级方式显示,通常会发出声音或振动,并且在屏幕顶部以浮动通知的形式显示。
二、 通知的基本用法
在上一节我们学习了如何创建通知渠道,下面我们来学习一下如何使用通知。
2.1 发送一条通知
创建通知最常见的方法是在BroadcastReceiver和Service中创建,而在Activity中创建通知相对来说是比较少见的。使用AndroidX库中提供的NotificationCompat类构建的Notification对象可以保证我们的程序在所有Android系统版本上都能正常工作。创建Notification对象的代码如下:
val notificationObject = NotificationCompat.Builder(context, channelId).build()
上面的代码只是创建了一个Notification对象,并没有实际作用。我们需要像创建AlertDialog那样在最终调用build()方法之前通过连缀多个设置方法来创建一个完整的Notification对象。下面是示例代码:
val notificationObject = NotificationCompat.Builder(this, "normal")
.setContentTitle("通知的标题")
.setContentText("通知的正文内容")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
.build()
在上面的工作都完成后,我们只需要调用NotificationManager的notify()方法让通知显示出来就可以了。notify()方法接收两个参数:
- id:通知的唯一标识符,用于在后续操作中引用该通知。
- notification对象:要显示的通知对象,包括通知标题、内容、小图标等信息。
下面就是如何通过notify()方法让通知显示出来的代码示例:
通知的id notification对象
notificationManager.notify(1, notificationObject)
最后别忘了在AndroidManifest.xml清单文件中为应用程序添加发送通知的权限声明:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
不然notify()方法可能会提示下报错信息:
到这里我们就完成了创建通知的所有流程,下面新建一个NotificationTest项目实践一下吧!首先我们为应用主界面添加一个Button用来发送通知,这个Button的id是"sendNoticeButton"。然后我们在MainActivity.kt文件中为Button按钮添加点击功能:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val sendNoticeButton: Button = findViewById(R.id.sendNoticeButton)
//创建NotificationManager
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//渠道id——唯一标识符、渠道名称——提示用户、重要等级
val notificationChannel = NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
notificationManager.createNotificationChannel(notificationChannel)
}
sendNoticeButton.setOnClickListener {
val notificationBuilder = NotificationCompat.Builder(this, "normal")
.setContentTitle("通知的标题")
.setContentText("通知的正文内容")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
.build()
notificationManager.notify(1, notificationBuilder)
Toast.makeText(this,"通知发送成功",Toast.LENGTH_SHORT).show()
}
}
}
运行程序,点击"发送通知"按钮,可以看到我们通知栏左上角已经显示了通知的icon。
在手机主界面我们也能看到应用的icon右上角也有红色标记。
下拉系统状态栏可以看到该通知的详细信息。
2.2 实现通知的点击跳转功能
在上一节中我们实现了通知的发送,不过当你点击通知时就会发现没有任何效果。如果我们想实现通知的点击跳转效果,还需要在代码中进行额外的设置,这就涉及到PendingIntent相关的知识了。
PendingIntent和Intent的功能有点类似,它们都可以指明一个意图,也都可以用于启动Activity、Service和发送广播等。不同的是,Intent倾向于立即执行某个动作,而PendingIntent倾向于在某个合适的时机执行某个动作。所以,也可以把PendingIntent简单的理解为延迟执行的Intent。PendingIntent可以根据需求选择使用getActivity()方法、getBroadcast()方法,或者getService()方法。这几个方法所接受的参数都是一样的:
- context:上下文对象,通常是当前的Context实例。
- requestCode:一个整数,用于在回调中识别请求。通常是一个唯一的整数。
- intent:一个Intent对象,表示要启动的活动或服务。
- flags:一个整数,表示PendingIntent的行为标志,一般传入0即可(在Android13及以上的版本中需要传入FLAG_IMMUTABLE)。
在创建好PendingIntent后,将setContentIntent()方法添加到NotificationCompat.Builder对象的后缀中,就可以将PendingIntent配置到通知中了。新建一个NotificationActivity界面当作点击通知后跳转的界面,然后修改MainActivity.kt中的代码,给通知加入点击功能:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
· · ·
sendNoticeButton.setOnClickListener {
//Intent对象
val intent = Intent(this, NotificationActivity::class.java)
//PendingIntent对象
val pendingIntent = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_IMMUTABLE)
val notificationObject = NotificationCompat.Builder(this, "normal")
.setContentTitle("通知的标题")
.setContentText("通知的正文内容")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
//调用setContentIntent()将PendingIntent配置到Notification对象上
.setContentIntent(pendingIntent)
.build()
notificationManager.notify(1, notificationObject)
}
}
}
可以看到,我们想要实现点击通知跳转功能。需要先创建一个Intent用于表明我们点击后要跳转到那里,然后将构建好的Intent对象传入PendingIntent的getActivity()方法中,以得到PendingIntent对象。然后在NotificationCompat.Builder()中调用setContentIntent()方法并传入PendingIntent对象即可。
现在我们点击通知就会跳转到指定的界面:
2.3 实现跳转后自动取消通知
但是我们在点击通知并成功跳转后会发现,通知栏中的通知图标并没有消失,下拉通知栏中的通知提示框也没有消失。这是因为,如果我们没有在代码中对该通知进行取消,那么该通知就会一直显示在系统状态栏上。
解决的办法是在NotificationCompat.Builder()中再连缀一个setAutoCancel()方法将通知取消:
val notificationObject = NotificationCompat.Builder(this, "normal")
· · ·
//点击后自动取消通知
.setAutoCancel(true)
.build()
当我们给setAutoCancel()方法传入true后,就表示当点击这个通知后,通知会自动取消。
2.4 在通知中显示富文本内容
NotificationCompat.Builder()提供了非常丰富的API,我们下面就来学习一下setStyle()方法吧!该方法可以让我们构建出富文本(长文字、图片等)的通知内容,setStyle()方法接收一个NotificationCompat.Style参数。
首先我们先来看一下给通知设置成长文本内容是什么样子的:
val notificationObject = NotificationCompat.Builder(this, "normal")
· · ·
.setContentText("学习如何构建通知、发送和同步数据以及使用语音操作。获取官方Android IDE和开发工具,为Android构建应用程序。")
.build()
它的效果如下:
可以看到通知内容是无法完整显示出来的,多余的部分会被省略号代替。如果我们想让剩余的部分显示出来,可以通过setStyle()方法来实现。下面是代码示例:
val notificationObject = NotificationCompat.Builder(this, "normal")
· · ·
.setStyle(NotificationCompat.BigTextStyle().bigText("学习如何构建通知、发送和同步数据以及使用语音操作。获取官方Android IDE和开发工具,为Android构建应用程序。"))
.build()
在这里,我们使用setStyle()方法替代了setContentText()方法。我们在setStyle()方法中创建了一个NotificationCompat.BigTextStyle()对象,然后调用了它的bigText()方法来传入一段长文字。
可以看到,通知内容被完整的显示出来了:
setStyle()除了可以显示长文本外,还可以显示一张大图片。它的具体用法是这样的:
val notificationObject = NotificationCompat.Builder(this, "normal")
· · ·
.setStyle(NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(resources,R.drawable.big_image)))
.build()
可以看到我们在setStyle()方法中创建了一个NotificationCompat.BigPictureStyle()对象,然后调用了它的bigPicture()方法并将图片传入。BitmapFactory.decodeResource()会将图片先解析成Bitmap对象,然后再传给bigPicture()方法。
现在重新运行程序,发送通知,效果如下:
2.5 通知渠道的重要等级
通知渠道重要等级越高,发出的通知就越容易得到用户的注意。高等级的通知渠道发出的通知可以弹出横幅、发出声音等,而低等级的通知渠道发出的通知在某下情况下可能被隐藏或者改变显示顺序。
下面我们来创建一个高等级的通知渠道:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//高等级通知渠道
val notificationChannel_high = NotificationChannel("important","Important",NotificationManager.IMPORTANCE_HIGH)
notificationManager.createNotificationChannel(notificationChannel_high)
}
sendNoticeButton.setOnClickListener {
val intent = Intent(this, NotificationActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_IMMUTABLE)
val notificationObject = NotificationCompat.Builder(this, "important")
· · ·
}
}
}
现在重新运行程序,发送通知,效果如下:
可以看到,不同于之前只是发出提示音并在状态栏显示图标。在我们将通知渠道等级修改为高等级后,通知会直接以弹窗的形式展现给用户。
三、 调用摄像头与相册
我们在生活中经常会用到扫一扫或者访问手机相册分享图片,现在我们就来学习一下他们是如何实现的吧!
3.1 调用摄像头拍照
新建CameraAlbumTest项目,修改activity_main.xml中的代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/takePhotoButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="拍照" />
<ImageView
android:id="@+id/photoImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="20dp" />
</LinearLayout>
我们在主界面增加了一个Button按钮用来打开相机,然后在按钮的下方设置了一个ImageView用来显示刚才拍摄的照片。接下来在MainActivity.kt中实现相关逻辑:
class MainActivity : AppCompatActivity() {
//拍照的请求码
val TAKE_PHOTO = 1
//照片Uri
lateinit var imageUri: Uri
//照片对象
lateinit var imageFile: File
lateinit var takePhotoButton: Button
lateinit var photoImageView: ImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
takePhotoButton = findViewById(R.id.takePhotoButton)
takePhotoButton.setOnClickListener {
//getExternalCacheDir()用于获取应用程序缓存目录,并在该目录下创建一个名为output_image.jpg的文件
imageFile = File(externalCacheDir, "output_image.jpg")
if (imageFile.exists()) {
imageFile.delete()
}
imageFile.createNewFile()
//获得图片文件的Uri对象
imageUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//Android 7 以上的版本使用FileProvider获得
Context 包名.fileprovider File
FileProvider.getUriForFile(this, "com.example.cameraalbumtest.fileprovider", imageFile)
} else {
//将File对象转换成Uri对象(output_image.jpg这张照片的本地真实路径)
Uri.fromFile(imageFile)
}
//启动相机程序
val intent = Intent("android.media.action.IMAGE_CAPTURE")
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
请求码
startActivityForResult(intent, TAKE_PHOTO)
}
}
/**
* 拍照成功的回调
*/
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
photoImageView = findViewById(R.id.photoImageView)
when (requestCode) {
TAKE_PHOTO -> {
if (resultCode == Activity.RESULT_OK) {
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri))
//将拍摄的照片设置到主页的ImageView上
photoImageView.setImageBitmap(rotateIfRequired(bitmap))
}
}
}
}
/**
* 获取图片的方向信息
*/
private fun rotateIfRequired(bitmap: Bitmap): Bitmap {
//使用ExifInterface获取图像文件的EXIF信息(拍摄日期、设备型号、曝光设置等)
val exif = ExifInterface(imageFile.path)
//从EXIF信息中提取出图像的方向
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
return when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(bitmap, 90)
ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(bitmap, 180)
ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(bitmap, 270)
else -> bitmap
}
}
/**
* 将图片进行旋转
*/
private fun rotateBitmap(bitmap: Bitmap, degree: Int): Bitmap {
//Matrix对象(用来进行2D图形变换)
val matrix = Matrix()
//设置旋转角度
matrix.postRotate(degree.toFloat())
//创建一个新的位图(旋转后的图像)
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
//释放原始图像内存
bitmap.recycle()
//返回旋转后的图像
return rotatedBitmap
}
}
首先通过getExternalCacheDir()方法获得应用缓存目录,然后将其指定为拍摄后照片的存储位置,指定照片的文件名称为output_image.jpg。
应用的缓存路径是/sdcard/Android/data/< package name >/cache,我们可以通过Android Studio的Device Explore来查看刚才拍摄的照片文件。
在低于Android 7.0的设备上使用Uri.fromFile()方法将File对象转换成Uri对象,这个Uri对象标识着output_image.jpg这张图片的本地真实路径。在高版本系统的设备上,我们则是通过FileProvider的getUriForFile()方法实现File对象转换为Uri对象的。getUriForFile()方法是在Android 7.0以后加入的新功能,因为直接使用文件的真实路径Uri是很不安全的,而FileProvider可以做到类似ContentProvider的安全机制来对数据进行保护。
接下来就是启动相机程序了,启动相机也是通过Intent来实现的。该Intent的action是"android.media.action.IMAGE_CAPTURE",我们通过putExtra()方法以键值对的形式为其指定了图片的输出地址。然后调用startActivityForResult()方法启动Actvity。我们是通过隐式Intent方式启动Activity,所以系统会自动匹配能够响应摄像请求的Activity去启动,拍下的照片会被输出到output_image.jpg文件中。
由于我们是通过startActivityForResult()方法启动Activity的,因此拍照完成后会有结果返回到onActivityResult()方法中。如果拍照成功,就调用BitmapFactory的decodeStream()方法将output_image.jpg这张图片解析成Bitmap对象,并将他设置到主页的ImageView中。
剩下的两个方法是用来将图片旋转到正确的角度的,这个跟相机程序设置有关。如果我们将上述代码修改一下,取消图片旋转。那么你会发现,你拍摄的照片总是被旋转后的,所以我们需要将照片旋转到正确的位置。
最后我们需要对FileProvider进行注册才能够使用,在AndroidManifest.xml文件中键入加入< provider >标签:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<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.CameraAlbumTest"
tools:targetApi="31">
· · ·
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.cameraalbumtest.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
在provider标签中我们引用了一个资源文件file_paths.xml,需要右键res目录—>New—>Directory—>创建xml目录。然后我们右键xm文件目录—>New—>File—>创建资源文件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>
关于FileProvider的用法说明可以参考《Android 7.0 FileProvider 使用说明》。
运行程序,效果如下图所示:
点击拍照按钮,调用系统相机程序:
拍照完成后,会将拍摄的照片显示在主界面上:
3.2 从相册中选择图片
我们继续在CameraAlbumTest项目的基础上进行修改,在主界面中添加一个用于选择相册中图片的按钮“相册”。接下来,修改MainActivity.kt中的代码,实现“相册”按钮的点击逻辑:
class MainActivity : AppCompatActivity() {
· · ·
//从册中选取的请求码
val FROM_ALBUM = 2
· · ·
lateinit var fromAlbumButton:Button
lateinit var photoImageView: ImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
· · ·
fromAlbumButton = findViewById(R.id.fromAlbumButton)
fromAlbumButton.setOnClickListener {
//打开系统文件选择器
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
//过滤条件:只显示可打开的图片文件
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/ *"
startActivityForResult(intent,FROM_ALBUM)
}
}
/**
* startActivityForResult的回调
*/
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
photoImageView = findViewById(R.id.photoImageView)
when (requestCode) {
· · ·
FROM_ALBUM -> {
//若读取相册成功并且相册照片不为空
if (resultCode == Activity.RESULT_OK && data != null)
data.data?.let { uri ->
//将Uri对象转为Bitmap对象
val bitmap = getBitMapFromUri(uri)
photoImageView.setImageBitmap(bitmap)
}
}
}
}
/**
* 将Uri对象转为Bitmap对象
*/
private fun getBitMapFromUri(uri: Uri): Bitmap? {
return contentResolver.openFileDescriptor(uri, "r")?.use {
BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
}
}
· · ·
}
运行程序,点击相册按钮:
选取任意一张图片:
四、 播放多媒体文件
听音乐和看电影是手机最常用到的功能了,下面我们就来学习一下如何播放多媒体文件吧!
4.1 播放音频文件
本小节我们将学习如何通过MediaPlayer播放音频文件。MediaPlayer的功能十分强大,将会是你的得助手!
4.1.1 MediaPlayer
Android中播放音频文件一般是通过MediaPlayer来实现的,它对多种格式的音频文件提供了非常全面支持,从而使得播放音乐变得十分简单。MediaPlayer不仅可以播放本地的音乐,还可以播放网络中的音频。下面是MediaPlayer中一些常用的控制方法:
方 法 名 | 功 能 描 述 |
---|---|
setDataSource( ) | 设置要播放的音频文件的位置 |
prepare( ) | 在开始播放之前调用,用来完成准备工作 |
start( ) | 开始或继续播放音频 |
pause( ) | 暂停播放音频 |
reset( ) | 将MediaPlayer对象重置成刚创建的状态 |
seekTo( ) | 从指定位置开始播放音频 |
stop( ) | 停止播放音频,调用后的MediaPlayer对象无法再播放音频 |
release( ) | 释放与MediaPlayer对象相关的资源 |
isPlaying( ) | 判断当前MediaPlayer是否正在播放音频 |
getDuration( ) | 获取载入的音频文件的时长 |
通过MediaPlayer播放音乐的基本流程如下:
- 创建MediaPlayer对象。
- 调用setDataSource()方法设置音乐文件的路径或者URL。
- 调用prepare()方法完成MediaPlayer的准备工作。
- 调用start()方法播放音乐。
- 调用pause()方法暂停播放音乐。可以在任何时候调用resume()方法恢复音乐的播放,音乐会从暂停的地方继续开始播放,不需要重新经历prepare()和start()方法。
- 调用resume()方法恢复播放。
- 调用stop()方法完全结束播放。在调用stop()方法后将不能再从当前位置继续播放音乐,而是需要调用prepare()方法和start()方法重新开始播放音乐。
- 在不需要使用MediaPlayer对象时,调用release()方法释放资源。
4.1.2 播放音频文件
下面我们新建一个PlayerAudioTest项目,修改它activity_main.xml中的代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal"
tools:context=".MainActivity">
<Button
android:id="@+id/playButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="播放音乐" />
<Button
android:id="@+id/pauseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="暂停播放" />
<Button
android:id="@+id/stopButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="终止播放" />
</LinearLayout>
我们在主界面添加了三个按钮,分别用来播放、暂停和终止。接下来我们需要创建一个"assets"目录用来存放我们要播放的音乐,assets文件夹必须创建在app/src/main目录下。接下来我们编辑MainActivity.kt文件,为按钮添加点击逻辑:
class MainActivity : AppCompatActivity() {
//MediaPlayer对象
private val mediaPlayer: MediaPlayer = MediaPlayer()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//绑定View控件
val playButton: Button = findViewById(R.id.playButton)
val pauseButton: Button = findViewById(R.id.pauseButton)
val stopButton: Button = findViewById(R.id.stopButton)
//初始化播放器
initMediaPlayer()
playButton.setOnClickListener {
if (!mediaPlayer.isPlaying) {
mediaPlayer.start()
}
}
pauseButton.setOnClickListener {
if (mediaPlayer.isPlaying) {
mediaPlayer.pause()
}
}
stopButton.setOnClickListener {
if (mediaPlayer.isPlaying) {
//清除MediaPlayer的状态(准备播放新音乐)
mediaPlayer.reset()
//重新设置数据源并准备播放
initMediaPlayer()
}
}
}
/**
* 初始化播放器
*/
private fun initMediaPlayer() {
//通过getAssets()方法获取assets目录下的资源文件
val assetManager = getAssets()
//文件描述符
val fd = assetManager.openFd("music.mp3")
//给MediaPlayer设置数据源(要播放的文件映射、从哪开始播放音乐、要播放多长时间的音乐)
mediaPlayer.setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
mediaPlayer.prepare()
}
/**
* 界面销毁时释放播放器资源
*/
override fun onDestroy() {
super.onDestroy()
mediaPlayer?.let {
it.stop()
it.release()
}
}
}
这里我们重点关注stopButton按钮和initMediaPlayer()方法的实现。
- stopButton按钮的点击逻辑:我们分别调用了reset()和initMediaPlayer()方法。在调用reset()方法之后,mediaPlayer对象就会被重置成全新的,然后就可以重新准备播放其他音乐了。因此先调用reset()方法清除旧的状态,再调用initMediaPlayer()方法重新设置数据源和准备播放。
- AssetManager.openFd()方法:在initMediaPlayer()方法中,我们通过getAssets()方法获得了一个AssetManager对象。该对象可以用来访问assets目录下的资源文件(图片、音频、视频和文本等)。然后我们通过AssetManager对象的openFd()方法获取"music.mp3"文件的描述符,通过这个文件描述符,我们就可以访问和操作相应的资源文件。
- MediaPlayer.setDataSource()方法:用于给MediaPlayer设置数据源。fileDescriptor是一个文件描述符,指向要播放的音频文件(这里指的是music.mp3)。startOffset是开始播放文件的偏移量,它表示你希望从文件的哪个位置开始播放音频(单位是毫秒)。length是要播放多长时间。如果你只想播放文件的前10秒,那么length就是10000(单位是毫秒)。
不要忘记在界面销毁时通过release()方法释放MediaPlayer播放器所占用的资源。
运行程序,效果如下图所示:
4.2 使用VideoView播放视频文件
VideoView组件是Android平台上的一个用于播放视频的控件。VideoView与ImageView的作用是相同的,只不过ImageView用于显示图片,而VideoView用于播放视频。VideoView和MediaPlayer的用法很类似。下面是VideoView的常用方法:
方 法 名 | 功 能 描 述 |
---|---|
setVideoPath() | 设置要播放的视频文件的位置 |
start() | 开始或继续播放视频 |
pause() | 暂停播放视频 |
resume() | 从头开始播放视频 |
seekTo() | 从指定的位置开始播放视频 |
isPlaying() | 判断当前是否正在播放视频 |
getDuration() | 获取载入的视频文件的时长 |
suspend() | 释放VideoView所占用的资源 |
VideoView除了操作简单和丰富的Api以外,在装载视频时也无需进行编码,并且播放完成后也无需手动回收资源。我们新建一个PlayVideoTest项目用来演示播放视频功能,修改主页布局文件main_activity.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<VideoView
android:id="@+id/myVideoView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/playButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="播放" />
<Button
android:id="@+id/pauseButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="暂停" />
<Button
android:id="@+id/replayButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="重新播放" />
</LinearLayout>
</LinearLayout>
我们将要播放的视频资源文件放在app/src/main/res目录下的raw文件夹内,然后为按钮添加点击事件:
class MainActivity : AppCompatActivity() {
lateinit var videoView: VideoView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val playButton: Button = findViewById(R.id.playButton)
val pauseButton: Button = findViewById(R.id.pauseButton)
val replayButton: Button = findViewById(R.id.replayButton)
videoView = findViewById(R.id.myVideoView)
//视频文件Uri(res目录下的资源文件都是以资源ID的形式存在于R文件中)
val videoUri = Uri.parse("android.resource://$packageName/${R.raw.spiderman}")
//将视频文件的Uri设置到VideoView上
videoView.setVideoURI(videoUri)
playButton.setOnClickListener {
if (!videoView.isPlaying) {
//开始播放视频
videoView.start()
}
}
pauseButton.setOnClickListener {
if (videoView.isPlaying) {
//暂停播放视频
videoView.pause()
}
}
replayButton.setOnClickListener {
if (videoView.isPlaying) {
//重新播放视频
videoView.resume()
}
}
}
override fun onDestroy() {
super.onDestroy()
//释放VideoView所占用的资源
videoView.suspend()
}
}
需要注意的是,在Android中,res目录下的资源文件是作为资源ID存在于R文件中的。所以我们创建一个Uri对象videoUri,表示要播放的视频文件的路径。
//视频文件Uri(res目录下的资源文件都是以资源ID的形式存在于R文件中)
val videoUri = Uri.parse("android.resource://$packageName/${R.raw.spiderman}")
这里的Uri对象"videoUri"是一个Android资源的URI。它由“android.resource://前缀”、“包名”和“资源文件名”这三部分组成。现在我们运行程序,效果如下图所示:
可以看到我们已经实现了基础的视频播放与暂停功能。