使用通知功能

通知渠道:自Android8.0后引入的新概念。每个应用程序可以自由的创建当前应用应该拥有哪些通知渠道。而这些通知渠道的控制权掌握在用户手中。用户可以选择是否响铃、是否振动或者是否要关闭这个渠道的通知。
对于应用而言,,通知渠道一旦创建就不可修改。所以一定要设计好有哪些渠道需要设计。

通知渠道的基本使用

  1. 首先需要一个NotificationManager对通知进行管理。可以通过调用Context的getSystemService()方法获取。这个方法接收一个参数用于确定获取系统的那个服务,如:val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager。Context.NOTIFICATION_SERVICE就是通知服务

  2. 构建通知渠道:使用NotificationChannel类构建一个通知渠道,并调用NotificationManager的createNotificationChannel()方法完成创建。由于这些类是在Android8.0后添加的,因此使用前要先判断版本。

    1
    2
    3
    4
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    val channel = NotificationChannel(channelId, channelName, importance)
    manager.createNotificationChannel(channel)
    }

    创建一个通知渠道至少需要渠道ID渠道名称以及重要等级这3个参数
    通知的重要等级主要有IMPORTANCE_HIGH、IMPORTANCE_DEFAULT、IMPORTANCE_LOW、IMPORTANCE_MIN这几种

  3. 通知一般都在后台的service去实现,前端的activity,或者broadcast使用的较少。

通知的默认使用方法

就版本的不同,8.0前和8.0后使用的方法不一样。所以AndroidX库中提供了兼容的API。AndroidX库中提供了一个NotificationCompat类,使用这个类的构造器创建Notification对象,就可以保证我们可以在所有的android上正常工作了。

  1. 使用构造器构造Notification对象,val notification = NotificationCompat.Builder(context, channelId).build(),它接收两个参数,一个context,一个渠道id。需要和我们在创建通知渠道时指定的渠道ID相匹配才行。
  2. 上一步的通知只是一个空通知,什么都没有,而我们可以通过以下方法添加内容:
  • setContentTitle()方法用于指定通知的标题内容,下拉系统状态栏就可以看到这部分内容。
  • setContentText()方法用于指定通知的正文内容,同样下拉系统状态栏就可以看到这部分内容。
  • setSmallIcon()方法用于设置通知的小图标,注意,只能使用纯alpha图层的图片进行设置,小图标会显示在系统状态栏上。
  • setLargeIcon()方法用于设置通知的大图标,当下拉系统状态栏时,就可以看到设置的大图标了。
  1. 就以上方法完成后,可以通过调用NotificationManager的notify()方法将通知显示出来了。notify()方法接收两个参数:第一个参数是id,要保证为每个通知指定的id都是不同的;第二个参数则是Notification对象,这里直接将我们刚刚创建好的Notification对象传入即可。

  2. 这是一段样例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    val channel = NotificationChannel("normal", "normal", NotificationManager.IMPORTANCE_DEFAULT)
    manager.createNotificationChannel(channel)
    }
    binding.sendNotice.setOnClickListener {
    val notice = NotificationCompat.Builder(this, "normal")
    .setContentTitle("This is my title")
    .setContentText("This is 贺政涛's content")
    .setSmallIcon(R.drawable.img_1)
    .setLargeIcon(BitmapFactory.decodeResource(resources,R.drawable.img_1))
    .build()
    manager.notify(1, notice)
    }
    }
    }

    系统会自动判断是否已经存在通知渠道,如果存在的话,它就不会再次创建了

PendingIntent

pendingintent和intent很多地方都是类似的。区别在于,Intent倾向于立即执行某个动作,而PendingIntent倾向于在某个合适的时机执行某个动作,也可以把PendingIntent简单地理解为延迟执行的Intent。
它可以通过几个默认的静态方法构建如:getActivity()方法、getBroadcast()方法,还是getService()方法。这些方法接受的参数都是一样的,第一个参数是context,第二个参数用不到,设为0即可。第三个参数是一个intent对象,需要自己创建。
第四个参数用于确定PendingIntent的行为,有FLAG_ONE_SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT和FLAG_UPDATE_CURRENT这4种值可选。
然后需要连接通知的pendingintent只需要在构建时再连缀一个setcontentIntent()即可

设计再点击通知后,该通知自动消失:一种是在
NotificationCompat.Builder中再连缀一个setAutoCancel()方法,一种是显式地调用NotificationManager的cancel()方法将它取消。

通知进阶

  1. 通知构造器有一个方法setStyle()。这个方法可以设置富文本。可以实现以下功能:
  • 让通知可以放很长的文本,而不会被系统隐藏
  • 可以再通知内容中放入图片
  1. 通知的重要程度:决定了通知是会以横幅的形式出现还是通知栏响一下

调用摄像头和相册

摄像头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package top.zfxt.cameraalbumtest

import android.app.Activity
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.media.ExifInterface
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.MediaStore
import android.provider.MediaStore.Audio.Media
import androidx.core.content.FileProvider
import androidx.core.graphics.rotationMatrix
import top.zfxt.cameraalbumtest.databinding.ActivityMainBinding
import java.io.File

class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
val takePhoto = 1
lateinit var imageUri: Uri
lateinit var outputImage: File
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
//为按钮注册点击事件
binding.takePhotoBtn.setOnClickListener {

//创建File对象,用于存储拍照后的图片
/**
* [externalCacheDir] 是指手机SD卡的应用关联缓存目录
* 具体的路径是/sdcard/Android/data/<package name>/cache
* 因为从Android 6.0系统开始,读写SD卡
* 被列为了危险权限,如果将图片存放在SD卡的任何其他目录,
* 都要进行运行时权限处理才行,而使用应用关联目录则可以跳过这一步
* 另外,从Android 10.0系统开始,公有的SD卡目录已经不再允许被应用程序直接访问了,而是要使用作用域存储才行
*/
outputImage = File(externalCacheDir, "output_image.png")
if (outputImage.exists()) {
outputImage.delete()
}
outputImage.createNewFile()
/**
* 如果运行设备的系统版本低于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则是一种特殊的
* ContentProvider,它使用了和ContentProvider类似的机制来对数据进行保护,可以选择性
* 地将封装过的Uri共享给外部,从而提高了应用的安全性。
*/
imageUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(this, "top.zfxt.cameraalbumtest.fileprovider", outputImage)
} else {
Uri.fromFile(outputImage)
}
//启动相机程序
//拍下的照片将会输出到output_image.jpg中
val intent = Intent("android.media.action.IMAGE_CAPTURE")
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
startActivityForResult(intent, takePhoto)
}
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when(requestCode) {
takePhoto->{
if(resultCode== Activity.RESULT_OK){
//将拍摄的图片显示出来
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri))
binding.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() // 将不再需要的Bitmap对象回收
return rotatedBitmap
}
}

我们还需要在AndroidManifest.xml中声明fileProvider

1
2
3
4
5
6
7
8
<provider
android:authorities="top.zfxt.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路径文件

1
2
3
4
 <?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="/" />
</paths>

访问相册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
binding.fromAlbumBtn.setOnClickListener {
//打开文件选择器
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
//指定只显示图片
intent.type = "image/*"
startActivityForResult(intent, 2)

}
...
...
2 -> {
if (resultCode == Activity.RESULT_OK && data != null) {
data.data?.let {
//将选择的图片展示出来
val bitmap = getBitmapFromUri(it)
binding.imageView.setImageBitmap(bitmap)
}
}
}
...

private fun getBitmapFromUri(uri: Uri) = contentResolver
.openFileDescriptor(uri, "r")?.use {
BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
}

播放多媒体文件

mediaPlayer

Android常用的用来处理音频文件的是MediaPlayer类。

以上是一些常用的控制方法。

工作流程:

  1. 首先需要创建一个MediaPlayer对象,然后调用setDataSource()方法设置音频文件的路径。
  2. 调用prepare()方法使MediaPlayer进入准备状态
  3. 接下来调用start()方法就可以开始播放音频,调用pause()方法就会暂停播放,调用reset()方法就会停止播放。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    class MainActivity : AppCompatActivity() {
    private val mediaplayer = MediaPlayer()
    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    initMediaPlayer()
    binding.play.setOnClickListener {
    if (!mediaplayer.isPlaying) {
    mediaplayer.start()
    }
    }
    binding.pause.setOnClickListener {
    if (mediaplayer.isPlaying) {
    mediaplayer.pause() // 暂停播放
    }
    }
    binding.stop.setOnClickListener {
    if (mediaplayer.isPlaying) {
    mediaplayer.reset() // 停止播放
    }

    }
    }

    private fun initMediaPlayer() {
    val assetMannager = assets
    val fd = assetMannager.openFd("肉肉.mp3")
    mediaplayer.setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
    mediaplayer.prepare()
    }

    override fun onDestroy() {
    super.onDestroy()
    mediaplayer.stop()
    mediaplayer.release()
    }
    }

videoPlayer