第一行代码Android 阅读笔记 第九章(仅自用)

使用手机多媒体

当某个应用希望向用户发出一些提示信息,而该应用程序又不在前台运行时,可借助通知来实现,发出一条通知,显示在手机最上方的状态栏。下拉状态栏可看到通知的详细内容。

创建通知渠道

由于发出通知数量和应用被打开率成正相关,导致许多应用想尽方法发送通知,而又不能完全屏蔽,因为这些通知内含我们关心的内容,于是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)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值