android 之 多媒体
通知
通知(notification)是Android系统中比较有特色的一个功能,当某个应用程序希望向用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知来实现。发出一条通
创建通知渠道
每条通知都要属于一个对应的渠道。每个应用程序都可以自由地创建当前应用拥有哪些通知渠道,但是这些通知渠道的控制权是掌握在用户手上的。用户可以自由地选择这些通知渠道的重要程度,是否响铃、是否振动或者是否要关闭这个渠道的通知
通知渠道一旦创建之后就不能再修改了,因此开发者需要仔细分析自己的应用程序一共有哪些类型的通知,然后再去创建相应的通知渠道
步骤
- 使用 getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 得到 NotificationManager
- 使用 NotificationChannel 构建一个通知渠道
- 使用 NotificationChannel.createNotificationChannel 完成创建
代码
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// NotificationChannel类和createNotificationChannel()方法都是Android 8.0系统中新增的API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 创建通知渠道
val notificationChannel =
NotificationChannel(
CHANNEL_ID,
"Normal",
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(notificationChannel)
}
创建通知渠道的方法:需要传入渠道ID、渠道名称以及重要等级
public NotificationChannel(String id, CharSequence name, @Importance int importance)
| 参数 | 说明 |
|---|---|
| id | 渠道ID,保证唯一性就可以 |
| name | 渠道名称,是给用户看的,需要可以清楚地表达这个渠道的用途 |
| importance | 重要等级,主要有IMPORTANCE_HIGH、IMPORTANCE_DEFAULT、IMPORTANCE_LOW、IMPORTANCE_MIN这几种。不同的重要等级会决定通知的不同行为。当然这里只是初始状态下的重要等级,用户可以随时手动更改某个通知渠道的重要等级,开发者是无法干预的 |
创建通知渠道的代码只在第一次执行的时候才会创建,当下次再执行创建代码时,系统会检测到该通知渠道已经存在了,因此不会重复创建,也并不会影响运行效率
通知的基本用法
通知的用法比较灵活,既可以在Activity、BroadcastReceiver、Service里创建,在Activity里创建通知的场景是比较少的,因为一般只有当程序进入后台的时候才需要使用通知
步骤
-
使用一个 NotificationCompat.Builder 构造器来构建 Notification 对象NotificationCompat.Builder 构造器接收两个参数:第一个参数是context,第二个参数是渠道ID,需要和我们在创建通知渠道时指定的渠道ID相匹配才行
-
调用 NotificationCompat.Builder 的各种方法丰富的 Notification 对象
-
调用 NotificationCompat.Builder.build() 创建出 Notification 对象
-
调用 NotificationManager.notify() 方法让通知显示出来
代码
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// NotificationChannel类和createNotificationChannel()方法都是Android 8.0系统中新增的API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 创建通知渠道
val notificationChannel =
NotificationChannel(
CHANNEL_ID,
"Normal",
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(notificationChannel)
}
//第一个参数是context,第二个参数是渠道ID,需要和我们在创建通知渠道时指定的渠道ID相匹配
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID).apply {
// 配置通知的标题、正文内容、图标
setContentTitle("this is content title")
setContentText("this is content text")
// 小图标会显示在系统状态栏上,只能使用纯alpha图层的图片(.png)进行设置,否则只是一块灰色区域
setSmallIcon(R.drawable.small_icon)
// 当下拉系统状态栏时,就可以看到设置的大图标
setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.big_image))
}.build()
//让通知显示出来
notificationManager.notify(1, notification)
效果
image-20211210150145087.png
使用 PendingIntent 添加点击效果
Intent倾向于立即执行某个动作,而PendingIntent倾向于在某个合适的时机执行某个动作。所以,也可以把PendingIntent简单地理解为延迟执行的Intent
PendingIntent 主要提供了几个静态方法用于获取 PendingIntent 的实例,可以根据需求来选择是使用getActivity()方法、getBroadcast()方法,还是getService()方法
下面的代码添加了 PendingIntent ,点击通知,会跳转到 NewsActivity
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// NotificationChannel类和createNotificationChannel()方法都是Android 8.0系统中新增的API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 创建通知渠道
val notificationChannel =
NotificationChannel(
CHANNEL_ID,
"Normal",
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(notificationChannel)
}
val intent = Intent(this, NewsActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
//第一个参数是context,第二个参数是渠道ID,需要和我们在创建通知渠道时指定的渠道ID相匹配
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID).apply {
// 设置 PendingIntent,点击通知会执行 PendingIntent 里面的 Intent 的意图
setContentIntent(pendingIntent)
// 配置通知的标题、正文内容、图标
setContentTitle("this is content title")
setContentText("this is content text")
// 小图标会显示在系统状态栏上,只能使用纯alpha图层的图片(.png)进行设置,否则只是一块灰色区域
setSmallIcon(R.drawable.small_icon)
// 当下拉系统状态栏时,就可以看到设置的大图标
setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.big_image))
}.build()
//让通知显示出来
notificationManager.notify(1, notification)
取消通知图标
运行上面的代码,会发现,点击通知,跳转到 NewsActivity 后,系统状态上的通知图标不会消失,解决的方法有两种:一种是在NotificationCompat.Builder中再连缀一个setAutoCancel()方法,一种是显式地调用NotificationManager的cancel()方法将它取消
第一种方法:
//第一个参数是context,第二个参数是渠道ID,需要和我们在创建通知渠道时指定的渠道ID相匹配
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID).apply {
// 点击跳转后,取消通知的显示
setAutoCancel(true)
// 设置 PendingIntent,点击通知会执行 PendingIntent 里面的 Intent 的意图
setContentIntent(pendingIntent)
......
}.build()
第二种方法:
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// 取消通知的显示,参数为 通知的id
// 也就是 notificationManager.notify() 方法传入的第一个参数
notificationManager.cancel(45)
通知的更多效果
NotificationCompat.Builder.setStyle()
setStyle() 方法接收一个NotificationCompat.Style参数,这个参数就是用来构建具体的富文本信息的,如长文字、图片
setStyle() 长文字
如果setContentText方法传入的文字过多,会造成省略,如下:
image-20211210152039827.png
setStyle() 就可以解决此问题
代码
//第一个参数是context,第二个参数是渠道ID,需要和我们在创建通知渠道时指定的渠道ID相匹配
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID).apply {
......
setStyle(
NotificationCompat.BigTextStyle().bigText(
"this is content text this is content text " +
"this is content text this is content text " +
"this is content text this is content text " +
"this is content text this is content text " +
"this is content text "
)
)
......
}.build()
效果
image-20211210152333415.png
setStyle() 显示大图片
代码
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID).apply {
......
setStyle(
NotificationCompat.BigPictureStyle()
.bigPicture(BitmapFactory.decodeResource(resources, R.drawable.big_image))
)
......
}.build()
效果
image-20211210152632624.png
多次调用 setStyle() 设置效果,以最后一次为准
通知的重要等级
通知渠道的重要等级越高,发出的通知就越容易获得用户的注意。比如高重要等级的通知渠道发出的通知可以弹出横幅、发出声音,而低重要等级的通知渠道发出的通知不仅可能会在某些情况下被隐藏,而且可能会被改变显示的顺序,将其排在更重要的通知之后
开发者只能在创建通知渠道的时候为它指定初始的重要等级,如果用户不认可这个重要等级的话,可以随时进行修改,开发者对此无权再进行调整和变更,因为通知渠道一旦创建就不能再通过代码修改了
重要等级高的通知渠道
只要将 NotificationChannel 的第三个参数改为 IMPORTANCE_HIGH ,就可以了,但要注意的是,需要打开应用的悬浮通知权限
悬浮通知权限
下面是打开悬浮通知权限的界面方法,但目前没有找到判断其是否打开的方法
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //Android 8.0及以上
// 跳转到悬浮通知权限页面
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, IMPORTANT_CHANNEL_ID)
startActivity(intent)
}
如下面的界面
image-20211210182341725.png
同时满足 IMPORTANCE_HIGH 和打开悬浮通知权限才会有下面的效果
image-20211210182601344.png
显示在其他应用上层的权限
有一种权限很容易和悬浮通知权限混淆,那就是显示在其他应用上层的权限,6.0以前的系统版本,悬浮窗权限是默认开启,6.0之后的系统版本,要做以下处理
这个权限需要在 AndroidManifest.xml 文件中添加下面的权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
然后再代码中判断是否有此权限,没有就打开授权界面,必须要在 AndroidManifest.xml 文件中添加权限,否则打开的列表中没有该应用
if (!Settings.canDrawOverlays(this)) {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
startActivity(intent)
}
调用摄像头和相册
使用相机拍照后再获取图片
class MediaActivity : AppCompatActivity() {
companion object {
private const val TAG = "MediaActivity"
private const val TAKE_PHOTO_REQUEST_CODE = 3
}
private lateinit var outputImage: File
private lateinit var imageUri: Uri
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_media)
take_photo.setOnClickListener {
// 用于存储拍照后的图片
outputImage = File(externalCacheDir, "output_image.jpg")
if (outputImage.exists()) {
outputImage.delete()
}
outputImage.createNewFile()
imageUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(
this,
"com.example.androidstudy.fileProvider",
outputImage
)
} else {
Uri.fromFile(outputImage)
}
// 启动相机
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
startActivityForResult(intent, TAKE_PHOTO_REQUEST_CODE)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
TAKE_PHOTO_REQUEST_CODE -> {
if (resultCode == RESULT_OK) {
// 将刚刚拍摄的照片取出来
val bitmap =
BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri))
photo_imageview.setImageBitmap(rotateIfRequired(bitmap))
}
}
}
}
private fun rotateIfRequired(bitmap: Bitmap): Bitmap {
val exif = ExifInterface(outputImage.path)
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 {
val matrix = Matrix()
matrix.postRotate(degree.toFloat())
val rotatedBitmap =
Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
bitmap.recycle()
return rotatedBitmap
}
}
创建了一个File对象,用于存放摄像头拍下的图片,并存放在手机SD卡的应用关联缓存目录下
应用关联缓存目录就是指SD卡中专门用于存放当前应用缓存数据的位置,调用getExternalCacheDir()方法可以得到这个目录,具体的路径是/sdcard/Android/data/<package name>/cache
从Android6.0系统开始,读写SD卡被列为了危险权限,如果将图片存放在SD卡的任何其他目录,都要进行运行时权限处理
另外,从Android 10.0系统开始,公有的SD卡目录已经不再允许被应用程序直接访问了,而是要使用作用域存储
上面的代码还不能运行,因为还需要在 AndroidManifest.xml 中注册,如下:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.androidstudy.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
android:name属性的值是固定的,android:authorities属性的值必须和刚才FileProvider.getUriForFile()方法中的第二个参数一致,<meta-data>指定Uri的共享路径,@xml/file_paths是需要新建的文件,内容如下
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path
name="files-path"
path="." />
<cache-path
name="cache-path"
path="." />
<external-path
name="external_storage_root"
path="." />
<external-files-path
name="external_file_path"
path="." />
<external-cache-path
name="external_cache_path"
path="." />
<!--配置root-path,这样子可以读取到sd卡和一些应用分身的目录,否则微信分身保存的图片,就会导致 java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/999/tencent/MicroMsg/WeiXin/export1544062754693.jpg-->
<root-path
name="root-path"
path="" />
</paths>
上面的属性说明,例如 external-path 标签
external-path就是用来指定Uri共享路径的,name属性的值可以随便填,path属性的值表示共享的具体路径。这里使用 . 表示将整个SD卡进行共享,也可以仅共享存放output_image.jpg这张图片的路径
从相册中选择图片
打开系统的文件选择器,只允许可打开的图片文件显示出来
open_album.setOnClickListener {
// 打开文件选择器
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
// 指定只显示图片
intent.type = "image/*"
startActivityForResult(intent, OPEN_ALBUM)
}
将选择的图片显示
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
OPEN_ALBUM -> {
if (resultCode == RESULT_OK) {
// 将选择的图片显示
data?.data?.let {
val bitmap =
BitmapFactory.decodeStream(contentResolver.openInputStream(it))
photo_imageview.setImageBitmap(bitmap)
}
}
}
}
}
播放多媒体文件
Android中播放音频文件一般是使用MediaPlayer类实现,可以用于播放网络、本地以及 APK 中的音频
MediaPlayer 使用方法
| 方法 | 说明 |
|---|---|
| setDataSource() | 设置音频文件的路径 |
| prepare() | 使MediaPlayer进入准备状态 |
| start() | 开始播放音频 |
| pause() | 暂停播放 |
| reset() | 将MediaPlayer对象重置到刚刚创建的状态 |
| seekTo() | 从指定位置开始播放音频 |
| stop() | 停止播放音频,调用后的MediaPlayer对象无法在播放音频 |
| release() | 释放MediaPlayer相关资源 |
| isPlaying() | 判断是否正在播放 |
| getDuration() | 获取载入的音频文件的时长 |
MediaPlayer 状态图
mediaplayer.jpg
代码
初始化MediaPlayer
private lateinit var mediaPlayer: MediaPlayer
......
private fun initMediaPlayer() {
mediaPlayer.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
setDataSource(assets.openFd("test.mp3"))
}
prepare()
}
}
使用MediaPlayer
mediaPlayer = MediaPlayer()
initMediaPlayer()
start_audio.setOnClickListener {
if (!mediaPlayer.isPlaying) {
mediaPlayer.start()
}
}
pause_audio.setOnClickListener {
if (mediaPlayer.isPlaying) {
mediaPlayer.pause()
}
}
reset_audio.setOnClickListener {
if (mediaPlayer.isPlaying) {
mediaPlayer.reset()
initMediaPlayer()
}
}
释放MediaPlayer
override fun onDestroy() {
super.onDestroy()
mediaPlayer.stop()
mediaPlayer.release()
}
播放视频
播放视频文件主要是使用VideoView类来实现的
VideoView不支持直接播放assets目录下的视频资源。res目录下允许我们再创建一个raw目录,像诸如音频、视频之类的资源文件也可以放在这里,并且VideoView是可以直接播放这个目录下的视频资源的
使用VideoView
val uri = Uri.parse("android.resource://$packageName/${R.raw.test_video}")
videoview.setVideoURI(uri)
start_video.setOnClickListener {
if (!videoview.isPlaying) {
videoview.start()
}
}
pause_video.setOnClickListener {
if (videoview.isPlaying) {
videoview.pause()
}
}
replay_video.setOnClickListener {
if (videoview.isPlaying) {
videoview.resume()
}
}
释放VideoView
override fun onDestroy() {
//将VideoView所占用的资源释放掉
videoview.suspend()
}