使用手机多媒体
当某个应用希望向用户发出一些提示信息,而该应用程序又不在前台运行时,可借助通知来实现,发出一条通知,显示在手机最上方的状态栏。下拉状态栏可看到通知的详细内容。
创建通知渠道
由于发出通知数量和应用被打开率成正相关,导致许多应用想尽方法发送通知,而又不能完全屏蔽,因为这些通知内含我们关心的内容,于是android8.0引入了通知渠道。
每条通知属于一个对应的渠道,每个应用可以自由创建当前应用拥有哪些通知渠道,但通知渠道的控制权在用户手上,用户可以自由选择这些通知渠道的重要程度,是否响铃、是否振动、是否关闭该渠道的通知。
有赖于通知渠道,用户可以自主地选择接受哪些通知。
而对于每个应用,通知渠道的划分十分考究,需要根据通知类型创建通知渠道,而通知渠道一旦创建了之后就不能再修改。
创建通知渠道
//创建NotificationManager对通知进行管理,通过getSystemService方法获取,
//此方法接受一个字符串参数用于确定获得系统哪个服务
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//通过NotificationChannel类创建一个通知渠道,
//并调用NotificationManager的createNotificationChannel()完成创建,
//由于NotificationChannel类和createNotificationChannel()方法都是android8.0系统中新增的API,
//因此在使用时还需进行版本判断
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.0){
//参数分别为渠道ID、渠道名称、重要等级
//渠道定义任意,但需保证全局唯一性
//渠道名称是给用户看的,命名需要言简意赅
//重要等级主要有IMPORTANCE_HIGH、IMPORTANCE_DEFAULT、IMPORTANCE_LOW、IMPORTANCE_MIN,
//程度由高到低,不同等级决定通知的不同行为,这里仅为初始状态,用户可手动修改,开发者无法干预
val channel = NotificationChannel(channelId,channelName,importance)
manager.createNotificationChannel(channel)
}
通知使用较为灵活,既可在Activity里创建,也可在BroadcastReceiver里创建,当然还可在Service里创建,但activity创建通知场景较少,因为只有当程序进入后台时才需要使用通知。
首先需要使用一个builder构造器来创建Notification对象,由于每个版本都会对通知功能进行或多或少的修改,API不稳定的问题在通知上更甚,为实现兼容,需要使用AndroidX库中提供的兼容API。
AndroidX库中提供了一个NotificationCompat类,能够使用该类的构造器创建Notification对象,就可保证程序在任意版本系统上正常工作。
//参数分别为context,渠道ID,渠道ID需要和我们在创建通知渠道时指定的渠道ID相匹配才行
val notification = NotificationCompat.Builder(context, channelId)
.setContentTitle("This is content title")//指定通知的标题内容
.setContentText("This is content text")//指定通知的正文内容
.setSmallIcon(R.drawable.small_icon)//指定通知的小图标
//指定通知的大图标,下拉状态栏时可看见
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.drawable.large_icon))
.build()
//显示通知,参数为id、Notification,而每个通知指定的id都不同
manager.notify(1, notification)
此时的通知还不具备点击即可打开应用的功能,还需要我们进行实现。
PendingIntent,与intent相仿,都可启动activity、service以及发送广播,而不同在于Intent倾向于立即执行某个动作,而PendingIntent倾向于在某个合适时机执行某个动作,即延迟执行的intent。
PendingIntent提供了几个静态方法:getActivity()、getBroadcast()、getService()方法,这几个方法接受的参数都相同,第一个为context,第二个一般用不到,传0即可,第三个参数为intent对象,第四个参数用于确定PendingIntent的行为,有:FLAG_ONE_SHOT、FLAG_NO_CRATE、FLAG_CANCEL_CURRENT、FLAG_UPDATE_CURRENT4种,一般传入0。
若api版本>=31,参数4取值只能FLAG_IMMUTABLE 或 FLAG_MUTABLE(可变 / 不可变)
前面的NotificationCompat.Builder还可连缀方法:setContentIntent,构建一个延迟执行的意图,当用户点击这条通知就会执行相应的逻辑。
button_s2.setOnClickListener {
val intent = Intent(this,MainActivity::class.java)
val pg = PendingIntent.getActivity(this,0, intent,FLAG_IMMUTABLE)
val notification = NotificationCompat.Builder(this,"normal")
.setContentTitle("This is content tittle")
.setContentText("This is content text")
.setContentIntent(pg)
.setSmallIcon(R.drawable.geat1)
.setLargeIcon(BitmapFactory.decodeResource(resources,R.drawable.geat2))
.build()
manager.notify(1,notification)
}
此时点击通知后,自动会打开指定跳转的activity,但通知未消失,我们可以通过两种方式使其消失。
val notification = NotificationCompat.Builder(this, "normal")
...
//点击后自动取消
.setAutoCancel(true)
.build()
class NotificationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_notification)
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as
NotificationManager
//传入要取消的通知的id
manager.cancel(1)
}
}
通知的进阶技巧
setStyle():允许我们构建出富文本的通知内容,即通知内不仅包含文字和图标。
setStyle()接受一个NotificationCompat.Style参数,用于构建具体的富文本信息。
当通知文本内容十分多时,会采用省略号来省略后面的内容。
//bigText用于显示长文字,NotificationCompat.BigTextStyle对象用于封装长文字信息。
.setStyle(NotificationCompat.BigTextStyle().bigText("Learn how to build
notifications, send and sync data, and use voice actions. Get the official
Android IDE and developer tools to build apps for Android."))
//通知中显示图片
.setStyle(NotificationCompat.BigPictureStyle().bigPicture(
BitmapFactory.decodeResource(resources, R.drawable.big_image)))
不同重要等级的通知渠道
通知渠道的重要等级越高,发出的通知就越容易获得用户注意。
比如高重要等级的通知渠道发出的通知可以弹出横幅、发出声音,而低等级的通知渠道发出的通知不仅可能被隐藏,还可能会被改变显示的顺序,将其排在更重要的通知之后。
开发者仅能在创建通知渠道时指定其初始的重要等级,若用户不认可此重要等级,可随时进行修改,开发者无权再进行调整和变更,因为通知渠道一旦创建就不能通过代码修改。
每注册一个通知渠道,设置内都会被记录,不能通过代码修改,只能用户自行修改
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
...
//必须要注册新的通知渠道
//重要的信息是弹出的横幅形式,类似微信
val channel2 = NotificationChannel("important", "Important",
NotificationManager.IMPORTANCE_HIGH)
manager.createNotificationChannel(channel2)
}
sendNotice.setOnClickListener {
val intent = Intent(this, NotificationActivity::class.java)
val pi = PendingIntent.getActivity(this, 0, intent, 0)
val notification = NotificationCompat.Builder(this, "important")
...
}
}
}
调用摄像头和相册
class MainActivity : AppCompatActivity() {
val takePhoto = 1
lateinit var imageUri: Uri
lateinit var outputImage: File
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
takePhotoBtn.setOnClickListener {
// 创建File对象,用于存储拍照后的图片,将其存放在手机SD卡的应用关联缓存目录下
//应用关联缓存目录是指sd卡中专门用于存放当前应用缓存数据的位置,
//调用getExternalCacheDir()方法可以得到这个目录
//从android6.0开始,读写sd卡被列为了危险权限,如果将图片存放在sd卡的任何其他目录,
//都需要进行运行时权限处理,使用应用关联目录则可跳过
//从android10之后,只能使用作用域存储
outputImage = File(externalCacheDir, "output_image.jpg")
if (outputImage.exists()) {
outputImage.delete()
}
outputImage.createNewFile()
imageUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//将File对象转换成Uri对象
//参数2为任意唯一的字符串
//在android7.0后,直接使用本地真实路径的Uri被认为不安全,
//而FileProvid是一种特殊的ContentProvider,使用了类似的机制对数据进行保护。
FileProvider.getUriForFile(this, "com.example.cameraalbumtest.
fileprovider", outputImage)
} else {
Uri.fromFile(outputImage)
}
// 启动相机程序,隐式Intent
val intent = Intent("android.media.action.IMAGE_CAPTURE")
//指定图片存放地址
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
//使结果返回到onActivityResult()
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))
imageView.setImageBitmap(rotateIfRequired(bitmap))
}
}
}
}
//自动旋转图片,因为有的手机认为打开拍摄时手机应该是横屏,回到竖屏时会发生90度旋转
//此处通过判断图片方向,当需要旋转时,旋转对应角度再显示出来。
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
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.cameraalbumtest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<provider//进行注册
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.cameraalbumtest.fileprovider"//同fileprovider的参数2
android:exported="false"
android:grantUriPermissions="true">
<meta-data//指定Uri的共享路径
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
//创建个xml目录,再创建文件file_paths
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
//此处的/指共享整个sd卡,也可修改为完整路径
<external-path name="my_images" path="/" />
</paths>
打开相册
class MainActivity : AppCompatActivity() {
...
val fromAlbum = 2
override fun onCreate(savedInstanceState: Bundle?) {
...
fromAlbumBtn.setOnClickListener {
// 打开文件选择器
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
// 指定只显示图片
intent.type = "image/*"
//操作原理同打开相机,通过此方法接受返回的结果,并将返回的结果以图片的形式呈现
startActivityForResult(intent, fromAlbum)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
...
fromAlbum -> {
if (resultCode == Activity.RESULT_OK && data != null) {
data.data?.let { uri ->
// 将选择的图片显示
//Uri->Bitmap
val bitmap = getBitmapFromUri(uri)
imageView.setImageBitmap(bitmap)
}
}
}
}
}
private fun getBitmapFromUri(uri: Uri) = contentResolver
.openFileDescriptor(uri, "r")?.use {
BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
}
...
}
当图片像素过高,直接载入内存中可能会导致程序崩溃,根据项目的需求先对图片进行适当的压缩,再加载到内存中。
播放多媒体文件
在android中播放音频文件一般是MediaPlayer类实现,对各种格式的音频文件提供了十分全面的控制方法。
MediaPlayer用于播放网络、本地、应用程序安装包中的音频,AS允许我们在项目中创建assets目录,并在此目录下存放任意文件和子目录,这些文件和子目录在项目打包时会被一并打包到安装文件中,然后我们在程序中就可以借助AssetManager这个类提供的接口对assets目录下的文件进行读取。
assets目录必须创建在app/src/main目录下,与java、res目录平级。
此处放入一份音频资源在assets目录中。
class MainActivity : AppCompatActivity() {
private val mediaPlayer = MediaPlayer()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initMediaPlayer()
play.setOnClickListener {
if (!mediaPlayer.isPlaying) {
mediaPlayer.start() // 开始播放
}
}
pause.setOnClickListener {
if (mediaPlayer.isPlaying) {
mediaPlayer.pause() // 暂停播放
}
}
stop.setOnClickListener {
if (mediaPlayer.isPlaying) {
mediaPlayer.reset() // 停止播放
initMediaPlayer()
}
}
}
private fun initMediaPlayer() {
//通过getAsstets方法得到一个AssetManager的实例,AM可以读书assets目录下的任何资源
//通过openFd方法将音频文件句柄打开,依次调用setDataSource和prepare方法对MP做好播放前的准备
val assetManager = assets
val fd = assetManager.openFd("music.mp3")
mediaPlayer.setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
mediaPlayer.prepare()
}
override fun onDestroy() {
super.onDestroy()
//释放资源
mediaPlayer.stop()
mediaPlayer.release()
}
}
播放视频
使用VideoView类实现,此类将视频的显示和控制集于一身
Video View的常用方法
VideoView不支持直接播放assets目录下的视频资源,因此我们在res目录下创建一个raw目录,存放诸如音频、视频之类的资源文件,且VideoView可直接播放这个目录下的视频资源。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//解析成uri对象
val uri = Uri.parse("android.resource://$packageName/${R.raw.video}")
//初始化videoView对象
videoView.setVideoURI(uri)
play.setOnClickListener {
if (!videoView.isPlaying) {
videoView.start() // 开始播放
}
}
pause.setOnClickListener {
if (videoView.isPlaying) {
videoView.pause() // 暂停播放
}
}
replay.setOnClickListener {
if (videoView.isPlaying) {
videoView.resume() // 重新播放
}
}
}
override fun onDestroy() {
super.onDestroy()
videoView.suspend()
}
}
其实看得出,VideoView和MediaPlayer十分相似,因为VideoView其实只是一个封装,背后还是MP对视频文件进行控制。
VV不是一个万能的视频播放工具类,对视频格式的支持以及播放效率方法都存在较大的不足。
infix
前面多次使用 A to B这样的语法结构来构建键值对,而to其实并不是kotlin中的一个关键字,之所以可以使用 A to B这样的语法结构,是因为kotlin提供了一种高级语法糖特性:infix函数。
//string类提供的startwith判断字符串是否以某个指定参数开头
if("hello".startWith("hello"){
}
//string的拓展函数,内部实现时调用的String类的startWith函数
infix fun String.beginsWith(prefix:String) = startWith(prefix)
//infix使得beginwith称为infix函数,多了种语法糖格式调用beginsWith函数,使得代码更具可读性
if("hello" beginsWith "hello"){
}
//另一个例子
infix fun <T> Collection<T>.has(element: T) = contains(element)
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
if (list has "Banana") {
// 处理具体的逻辑
}
to语法的源码实现也是如此
//通过泛型和拓展函数,使得A拓展了to函数,并返回Pair类型对象
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)