本章介绍了通知及使用技巧、调用摄像头及读取相册、播放音视频。最后我们介绍了infix函数这种高级语法糖的用法。
9.1.将程序运行到手机上
没啥好讲的
9.2.使用通知
某app不在前台运行时却希望向用户发出一些提示信息,可以借助通知来实现。发出通知后,最上方状态栏会显示一个通知的图标,下拉状态栏可以获取通知的详细内容。
//第一步:getSystemService用于获取系统的那个服务,需要一个NotificationManager对同志进行管理
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//第二步:创建通知渠道,低于8.0无法创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
//构建一个通知渠道,创建的话需要知道渠道ID、渠道名称和重要等级。渠道ID随便定义,保证全局唯一性
//渠道名称给用户看,清楚表明用途,重要等级有四种。IMPORTANT_HIGH、DEFAULT、LOW、MIN。
val channel = NotificationChannel(channelID,channelName,importance)
//完后创建通知渠道
manager.createNotificationChannel(channel)
}
9.2.1.创建通知渠道
每个app都乱发送通知,用户烦不胜烦。要么同意接收所有信息,要么屏蔽所有信息,这也是Android通知功能的痛点。因此Android8.0引入通知渠道的概念。
通知渠道是每条通知都要属于一个相应的渠道。每个app可自由创建当前应用拥有哪些通知渠道,但这些通知渠道的控制权是掌握在用户手上的,用户可以选择这些通知渠道重要程度,是否响铃、震动或者关闭。譬如微博可以创建两种通知渠道,一个关注、一个推荐。
创建通知渠道的代码如下:
//第一步:getSystemService用于获取系统的那个服务,需要一个NotificationManager对同志进行管理
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//第二步:创建通知渠道,低于8.0无法创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
//构建一个通知渠道,创建的话需要知道渠道ID、渠道名称和重要等级。渠道ID随便定义,保证全局唯一性
//渠道名称给用户看,清楚表明用途,重要等级有四种。IMPORTANT_HIGH、DEFAULT、LOW、MIN。
val channel = NotificationChannel(channelID,channelName,importance)
//完后创建通知渠道
manager.createNotificationChannel(channel)
}
9.2.2.通知的基本用法
可以在Service、BroadCastReceiver和Activity中创建,前两者较多,步骤相同。AndroidX提供的兼容API提供了NotificationCompat类用以创建Notification对象以保证所有系统版本均可运行:
//第一个参数是context,第二个参数是渠道ID,可以连缀任意多的方法来创建一个丰富的Notification对象
val notification = NotificationCompat.Builder(context,channelId).build()
//让通知显示出来,第一个参数是ID,每个通知指定的id不同,第二个参数是Notification对象
manager.notify(1,notification)
创建NotificationTest项目:
package com.example.myapplication
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.graphics.BitmapFactory
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.core.app.NotificationCompat
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//步骤一:获取NotificationManager实例
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)
}
//步骤三:点击事件里完成通知的创建工作
send_Notice.setOnClickListener {
//build方法之前连缀任意多的方法创建一个丰富的Notification对象,基本设置包括:
//setContentTitle标题内容;setContentText文本内容;setSmallIcon设置通知的小图标,纯alpha图层;setLargeIcon大图标
val notification = NotificationCompat.Builder(this,"normal")
.setContentTitle("This is content title")
.setContentText("This is content text")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources,R.drawable.large_icon))
.build()
//让通知显示出来,一个是id,一个是notification对象。
manager.notify(1,notification)
}
}
}
仅仅显示可不行,点击通知的效果要有啊,涉及到PendingIntent,延迟执行的Intent。可以通过getActivity、getBroadcase和getService几个方法来获取PendingIntent实例。新建NotificationActivity这一Activity。修改xml和点击事件:
send_Notice.setOnClickListener {
val intent = Intent(this, NotificationActivity::class.java)
//第一个参数是Context,第二个参数用不到,第三个参数时Intnet对象,通过这个对象构建出PendingIntent的意图;第四个参数是PendingIntent的行为,FLAG_ONE_SHOT等
val pi = PendingIntent.getActivity(this, 0, intent, 0)
//build方法之前连缀任意多的方法创建一个丰富的Notification对象,基本设置包括:
//setContentTitle标题内容;setContentText文本内容;setSmallIcon设置通知的小图标,纯alpha图层;setLargeIcon大图标
//setContentIntent设置延迟Intent;setAutoCancel自动取消掉
val notification = NotificationCompat.Builder(this, "normal")
.setContentIntent(pi)
.setAutoCancel(true)
.setContentTitle("This is content title")
.setContentText("This is content text")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
.build()
//让通知显示出来,一个是id,一个是notification对象。
manager.notify(1, notification)
}
当然可以将setAutoCancel(true)改为修改NotificationActivity里面的内容:
class NotificationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_notification)
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.cancel(1)
}
}
9.2.3.通知的进阶技巧
可以使用setStyle取代setContentText,因为后者长文本时不能完全显示,后面的都会被省略。
val notification = NotificationCompat.Builder(this, "normal")
.setContentIntent(pi)
.setContentTitle("This is content title")
.setStyle(NotificationCompat.BigTextStyle().bigText("豫章故郡,洪都新府。星分翼轸,地接衡庐。襟三江而带五湖,控蛮荆而引瓯越。物华天宝,龙光射牛斗之墟;人杰地灵,徐孺下陈蕃之榻。雄州雾列,俊采星驰。台隍枕夷夏之交,宾主尽东南之美。都督阎公之雅望,棨戟遥临;宇文新州之懿范,襜帷暂驻。十旬休假,胜友如云;千里逢迎,高朋满座。腾蛟起凤,孟学士之词宗;紫电青霜,王将军之武库。家君作宰,路出名区;童子何知,躬逢胜饯。"))
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
.build()
也可以显示大图片。
//BitmapFactory.decodeResource将图片解析为Bitmap对象,在传入BigPicture中
.setStyle(NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(resources, R.drawable.big_image)))
也可以调整优先级:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel2 =
NotificationChannel("important", "Important", NotificationManager.IMPORTANCE_HIGH)
manager.createNotificationChannel(channel2)
}
...
val notification = NotificationCompat.Builder(this, "important")
9.3.调用摄像头和相册
新建CameraAlbumTest项目,修改布局文件:
修改MainActivity:
package com.example.myapplication
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 androidx.core.content.FileProvider
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File
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)
take_photo.setOnClickListener {
//创建File对象,用于存储拍照后的照片,存储位置是SD卡的应用关联缓存目录下,6.0后读写SD卡是危险权限,使用关联目录cache可以跳过这一步。Android10.0后使用作用域存储。
outputimage = File(externalCacheDir, "output_image.jpg")
//如果File已经存在,删掉,并调用createNewFile创建新文件
if (outputimage.exists()) {
outputimage.delete()
}
outputimage.createNewFile()
imageuri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//如果系统版本大于7.0,本地真实路径uri不安全,会抛出异常。 FileProvider.getUriForFile可以将File对象转换为一个封装后的uri对象。
//getUriForFile接收三个参数,一个是context,另一个是任意字符串,第三个是File对象。FileProvider使用类似ContentProvider的机制进行保护,提高程序安全性。
FileProvider.getUriForFile(this, "com.example.camera.fileprovider", outputimage)
} else {
//如果系统版本低于7.0,调用Uri.fromFile将File对象转换为Uri对象,这个对象是本地真实路径
Uri.fromFile(outputimage)
}
//Intent的action进行指定,Intent的putExtra指定图片的输出地址,刚刚得到uri对象
val intent = Intent("android.media.action.IMAGE_CAPTURE")
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageuri)
//启动Activity,隐式的。调用之后返回到onActivityResult方法。
startActivityForResult(intent, takePhoto)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
takePhoto -> {
//将拍摄的照片显示出来,拍照成功的话使用BitmapFactory.decodeStream将图片解析为bitmap对象
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(imageuri))
//最后设置到ImageView当中,再加上照片旋转的处理。
image_view.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文件:
新建xml目录,新建file_paths.xml。
9.3.2.从相册中选择图片
布局就不说了,一个button组件。主要是MainActivity里的修改。
class MainActivity : AppCompatActivity() {
val takePhoto = 1
val fromAlbum = 2
lateinit var imageuri: Uri
lateinit var outputimage: File
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
....
from_album_btn.setOnClickListener {
//1.打开文件选择器,Intent的action指定为打开系统文件选择器。
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
//2.指定只显示图片,增加过滤条件,只允许打开图片文件显示出来
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
//3.选择完图片后进入onActivityResult方法
startActivityForResult(intent, fromAlbum)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
...
fromAlbum -> {
//如果data不等于null
if (resultCode == Activity.RESULT_OK && data != null) {
//调用Intnet的getData方法获取图片的uri。在调用getBitmapFromUri将uri转换为Bitmap对象,最后显示出来。
data.data?.let { uri ->
val bitmap = getBitmapFromUri(uri)
image_view.setImageBitmap(bitmap)
}
}
}
}
}
private fun getBitmapFromUri(uri: Uri) = contentResolver.openFileDescriptor(uri, "r")?.use {
BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
}
.....
}
9.4.播放多媒体文件
9.4.1.播放音频
音频文件MediaPlayer类中的方法:
新建PlayAudioTest项目,新建assets目录用于存储音乐文件,修改布局文件activity_main.xml:
修改MainActivity里的代码:
package com.example.myapplication
import android.media.MediaPlayer
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
//类初始化创建一个MediaPlayer的实例
private val mediaPlayer = MediaPlayer()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//初始化
initMediaPlayer()
play_music_btn.setOnClickListener {
//对于细节的处理堪称完美,来了先判断,用完之后初始化、销毁等等
if (!mediaPlayer.isPlaying){
mediaPlayer.start()
}
}
pause_music_btn.setOnClickListener {
if (mediaPlayer.isPlaying){
mediaPlayer.pause()
}
}
stop_music_btn.setOnClickListener {
if (mediaPlayer.isPlaying){
//重置为刚才的状态并重现调用initMediaPlayer方法
mediaPlayer.reset()
initMediaPlayer()
}
}
}
private fun initMediaPlayer(){
//得到一个assetManager实例,assetManager可读取assets目录下的所有资源
val assetManager = assets
//调用openFd将音频文件句柄打开,做一次调用setDataSource设置要播放文件的位置、prepare完成初始化
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()
}
}
9.4.2.播放视频
借助VideoView类来实现,VideoView并不是万能工具类,其支持的格式不多、效率低。视频放在新建的raw目录下,方法如下:
新建PlayVideoTest项目,修改activity_main.xml。
修改MainActivity.java:
package com.example.myapplication
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//将raw目录下的video.MP4解析为一个uri对象
val uri = Uri.parse("android.resource://$packageName/${R.raw.video}")
//调用setVideoURI将解析出来的uri对象传入,这样完成了初始化
video_view.setVideoURI(uri)
play_video_btn.setOnClickListener {
if (!video_view.isPlaying){
video_view.start()
}
}
pause_video_btn.setOnClickListener {
if (video_view.isPlaying){
video_view.pause()
}
}
stop_video_btn.setOnClickListener {
if (video_view.isPlaying){
video_view.resume()//重新播放
}
}
}
override fun onDestroy() {
super.onDestroy()
video_view.suspend()
}
}
9.5.Kotlin课堂:使用infix函数构建更可读的用法
val map = mapOf("Apple" to 1, "Banana" to 2, "Pear" to 3)
for ((fruit, number) in map) {
println("fruit is " + fruit + ",number is " + number)
}
to并不是Kotlin关键字,借助了高级语法糖:infix函数,infix只是调整了写法,他是将A .to( B)改为A to B。Infix可提高代码的可读性。举例来讲:
//判断字符串是否以某个参数开头
if ("Hello Kitty".startsWith("Hello")) {
//处理逻辑
}
//将其改写为infix函数形式:
//借助infix函数,使用更可读的写法
if ("Hello Kitty" beginsWith "hello"){
}
//String类的扩展函数,添加一个beginsWith,内部实现基于startsWith方法。
// 加上infix后beginsWith变为infix函数,除了传统的调用方式,还有特殊语法糖格式
infix fun String.beginsWith(prefix: String) = startsWith(prefix)
Infix函数需要满足两个条件:1.不能定义为顶层函数,必须为类成员函数或者扩展函数;2.只能接受一个参数,参数类型没有限制。举例来说:
val list2 = listOf("Apple", "Banana", "Orange", "Pear")
if (list2.contains("Banana")) {
//处理具体逻辑
}
//使用infix写法
if (list2.has("Banana")) {
//处理具体逻辑
}
//给所有Collection接口添加一个扩展函数,这是因为Collection是所有Java和Kotlin集合的总接口,因此给它添加一个has函数所有集合子类都能用了
infix fun Collection.has(element: T) = contains(element)
研究A to B,发现是使用了Pair函数,自己仿写整一个:
val map = mapOf("Apple" with 1, "Banana" with 2, "Pear" with 3)
infix fun A.with(that: B): Pair = Pair(this, that)