课本
书籍资源进入官网下载,PC端进入
第九章-丰富你的程序,运用手机多媒体
引言
在很早以前,手机的功能普遍比较单调,仅仅就是用来打电话和发短信的。而如今,手机在我们的生活中正扮演着越来越重要的角色,各种娱乐活动都可以在手机上进行:上班的路上太无聊,可以戴着耳机听音乐;外出旅行的时候,可以在手机上看电影;无论走到哪里,遇到喜欢的事物都可以用手机拍下来。
手机上众多的娱乐方式少不了强大的多媒体功能的支持,而Android在这方面做得非常出色。它提供了一系列的API,使得我们可以在程序中调用很多手机的多媒体资源,从而编写出更加丰富多彩的应用程序。本章我们就将学习Android中一些常用的多媒体功能的使用技巧。
手机上众多的娱乐方式少不了强大的多媒体功能的支持,而Android在这方面做得非常出色。它提供了一系列的API,使得我们可以在程序中调用很多手机的多媒体资源,从而编写出更加丰富多彩的应用程序。本章我们就将学习Android中一些常用的多媒体功能的使用技巧。
将程序运行到手机上
不必我多说,首先你需要拥有一部Android手机。现在Android手机早就不是什么稀罕物,几乎已经是人手一部了,如果你还没有的话,赶紧去购买吧。
不管有线调试还是无线调试,都需要将开发者选项打开因为不同手机的打开开发者选项方式可能不同,所以这里需要你自己去查资料.
有线调试
打开开发者选项->打开开发者选项->开启USB调试.打开Android Studio,将数据线插入手机.如果这是你首次使用这部手机连接电脑的话,手机上应该还会出现一个如图所示的弹窗提示。勾选“一律允许使用这台计算机进行调试”的选项,然后点击“允许”,这样下次连接电脑的时候就不会再弹出这个提示了。
这里以小米手机为例
现在观察手机,会发现自己的手机已经在线了.
选中自己的这台设备,就可以使用真实的手机来运行程序了。
无线调试
- 在进行无线调试前应确保手机和电脑处于同一局域网,如;


- 查看手机的IP和调试端口

- 打开ADB工具(AndroidStudio自带,寻找方式不细说,自行搜索),输入以下命令:
1 | adb connect IP:端口 |

4. 连上后AndroidStudio就会自动检测到
使用通知
通知(notification)是Android系统中比较有特色的一个功能,当某个应用程序希望向用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知来实现。发出一条通知后,手机最上方的状态栏中会显示一个通知的图标,下拉状态栏后可以看到通知的详细内容。Android的通知功能自推出以来就大获成功,连iOS系统也在5.0版本之后加入了类似的功能。
通知渠道
了解什么是通知渠道
由于近几年无良厂商的滥用权限,在Android8后系统引用了通知渠道这个概念.什么是通知渠道呢?可以是使用通知栏推送、震动、响铃等方式通知我们如物流、聊天、优惠等应用自定义的通知类型.
拿小米查看淘宝的通知渠道来举例:
可以看见红框部分,淘宝创建了不少通知渠道。
创建通知渠道
- 创建通知渠道可以通过上下文中的
getSystemService()方法获取一个系统服务,该方法接收一个字符串确定选择的什么服务,传入入Context.NOTIFICATION_SERVICE即可获取通知管理NotificationManager.代码如下:
1 | val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager |
- 使用
NotificationChannel创建一个通知渠道,然后调用NotificationManager的createNotificationChannel()方法完成注册。
1 | // 因为是Android8的api,所以要判断版本 |
NotificationChannel()方法接收三个参数:
- 参数一: 渠道Id,保持全局唯一性即可。
- 参数二: 渠道名称,在设置中展示给用户看的,用于表达渠道用途。
- 参数三: 初始渠道等级,用户后续可更改。有IMPORTANCE_HIGH()、IMPORTANCE_DEFAULT()、IMPORTANCE_LOW、IMPORTANCE_MIN这四种。
通知的基本用法
通知可以在Activity、BroadcastReceiver或后面的Service中创建,相比于BroadcastReceiver和Service,在Activity里创建通知的场景还是比较少的,因为一般只有当程序进入后台的时候才需要使用通知。
- 为了解决不同安卓版本带来的不兼容问题,这里使用
NotificationCompat来创建一个Notification对象.NotificationCompat.Builder()接收两个参数:1
val notification = NotificationCompat.Builder(context, channelId).build()
- 参数一: 上下文context
- 参数二:渠道id
- 上面创建的是一个空的Notification对象,我们可以在最后的Build()方法之前链式调用任意多个方法来设置Notification对象,一下是一些常见配置:
1 | val notification = NotificationCompat.Builder(context, channelId) |
- 要想显示通知,调用用NotificationManager的
notify()方法就可以让通知显示出来了。
1 | anager.notify(1, notification) |
notify()方法接收两个参数:
- 参数一:通知的id,保证唯一即可
- 参数二: Notification对象
实践
到这里就已经把创建通知的每一个步骤都分析完了,下面就让我们通过一个具体的例子来看一看通知到底是长什么样的。
- 新建一个
NotificationTest项目,并修改activity_main.xml中的代码,如下所示:
1 |
|
这里新建了一个发送通知按钮
- 接下来修改
MainActivity中的代码,如下所示:
1 | class MainActivity : AppCompatActivity() { |
可以看到,我们首先获取了NotificationManager的实例,并创建了一个ID为normal通知渠道。创建通知渠道的代码只在第一次执行的时候才会创建,当下次再执行创建代码时,系统会检测到该通知渠道已经存在了,因此不会重复创建,也并不会影响运行效率。
接下来在发送通知按钮的点击事件里完成了通知的创建工作,创建的过程正如前面所描述的一样。注意,在NotificationCompat.Builder的构造函数中传入的渠道ID也必须叫normal,如果传入了一个不存在的渠道ID,通知是无法显示出来的。另外,通知上显示的图标你可以使用自己准备的图片,也可以使用随书源码附带的图片资源(源码下载地址见前言),新建一个drawable-xxhdpi目录,将图片放入即可。
3. 现在可以来运行一下程序了,其实MainActivity一旦打开之后,通知渠道就已经创建成功了,我们可以进入应用程序设置当中查看。依次点击设置→应用和通知→NotificationTest→通知,如图所示
可以看到已经创建了一个Normal通知渠道
4. 接下来回到NotificationTest程序当中,然后点击发送通知按钮,你会在系统状态栏的最左边看到一个小图标,如图所示。
如果你尝试点击这个通知会发现,这条通知会没有任何效果.如果想要实现点击跳转我们需要涉及一个新概念PendingIntent.
PendingIntent
PendingIntent从名字上看起来就和Intent有些类似,它们确实存在不少共同点。比如它们都可以指明某一个意图,都可以用于启动Activity、启动Service以及发送广播等。不同的是,Intent倾向于立即执行某个动作,而PendingIntent倾向于在某个合适的时机执行某个动作。所以,也可以把PendingIntent简单地理解为延迟执行的Intent。
PendingIntent的用法同样很简单,它主要提供了几个静态方法用于获取PendingIntent的实例,可以根据需求来选择是使用getActivity()方法、getBroadcast()方法,还是getService()方法。这几个方法所接收的参数都是相同的:
- 参数一: 上下文Context
- 参数二: 传入0即可
- 参数三: Intent对象,这个对象构建出PendingIntent的
意图 - 参数四: PendingIntent的行为,有
LAG_ONE_SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT和FLAG_UPDATE_CURRENT这4种值可选
了解PendingIntent后,再去看NotificationCompat.Builder,它还可以链式调用setContentIntent()方法,接收的参数正是一个PendingIntent对象。因此,这里就可以通过PendingIntent构建一个延迟执行的“意图”,当用户点击这条通知时就会执行相应的逻辑。
现在我们来优化一下NotificationTest项目,给刚才的通知加上点击功能,让用户点击它的时候可以启动另一个Activity。
- 首先需要准备好另一个Activity,右击com.example.notificationtest包→New→Activity→Empty Activity,新建
NotificationActivity。然后修改activity_notification.xml中的代码,如下所示:
1 |
|
- 下面我们修改
MainActivity中的代码,给通知加入点击功能,如下所示:
1 | class MainActivity : AppCompatActivity() { |
在新增的代码中,我们先使用Intent表达出我们想要启动NotificationActivity的意图,然后将它传入PendingIntent的getActivity()方法里,得到PendingIntent的实例,接着在NotificationCompat.Builder中调用setContentIntent()方法,把它作为参数传入即可。
3. 现在重新运行一下程序,并点击发送通知按钮,依旧会发出一条通知。然后下拉系统状态栏,点击一下该通知,就会打开NotificationActivity的界面了,如图所示。
4. 在上面点击了通知后会发现通知图标并没有消失,如果我们没有在代码中对该通知进行取消,它就会一直显示在系统的状态栏上。解决的方法有两种:一种是在NotificationCompat.Builder中再连缀一个setAutoCancel()方法,一种是显式地调用NotificationManager的cancel()方法将它取消。两种方法我们都学习一下。
- setAutoCancel()中传入true表示点击这个通知的时候,通知会自动取消。
1
2
3
4val notification = NotificationCompat.Builder(this, "normal")
...
.setAutoCancel(true)
.build()
2. cancel()中传入取消通知的id,前面我们设置的1就传入11
manager.cancel("normal")
- 本小节代码
通知的进阶技巧
理解通知的基本使用后我们来了解NotificationCompat.Builder的更多Api,当然,每一个API都详细地讲一遍不太可能,我们只能从中选一些比较常用的API进行学习。
setStyle()方法
这个方法允许我们构建出富文本的通知内容。也就是说,通知中不光可以有文字和图标,还可以包含更多的东西。setStyle()方法接收一个NotificationCompat.Style参数,这个参数就是用来构建具体的富文本信息的,如长文字、图片等。
- 假设想要在通知中显示一段长文字,可以使用setStyle()方法替代setContentText()方法.
1 | val notification = NotificationCompat.Builder(this,"normal").apply { |
在setStyle()方法中,我们创建了一个NotificationCompat.BigTextStyle对象,这个对象就是用于封装长文字信息的,只要调用它的bigText()方法并将文字内容传入就可以了。
2. 再次重新运行程序并触发通知,效果如图所示。
3. 除了显示长文字之外,通知里还可以显示一张大图片,具体用法是基本相似的:
1 | val notification = NotificationCompat.Builder(this,"normal").apply { |
- 再次重新运行程序并触发通知,效果如图所示。

重要等级
接下来,我们学习一下不同重要等级的通知渠道对通知的行为具体有什么影响。其实简单来讲,就是通知渠道的重要等级越高,发出的通知就越容易获得用户的注意。比如高重要等级的通知渠道发出的通知可以弹出横幅、发出声音,而低重要等级的通知渠道发出的通知不仅可能会在某些情况下被隐藏,而且可能会被改变显示的顺序,将其排在更重要的通知之后。
但需要注意的是,开发者只能在创建通知渠道的时候为它指定初始的重要等级,如果用户不认可这个重要等级的话,可以随时进行修改,开发者对此无权再进行调整和变更,因为通知渠道一旦创建就不能再通过代码修改了。
- 既然无法修改之前创建的通知渠道,那么我们就只好再创建一个新的通知渠道来测试了。修改
MainActivity中的代码,如下所示:
1 | class MainActivity : AppCompatActivity() { |
- 点击
发送通知按钮,效果如图所示。
部分定制化os,如小米无法看到弹窗 - 本小节代码
调用摄像头和相册
我们平时在使用QQ或微信的时候经常要和别人分享图片,这些图片可以是用手机摄像头拍的,也可以是从相册中选取的。这样的功能实在是太常见了,几乎是应用程序必备的功能,那么本节我们就学习一下调用摄像头和相册方面的知识。
调用摄像头拍照
先来看看摄像头方面的知识,现在很多应用会要求用户上传一张图片作为头像,这时打开摄像头拍张照是最简单快捷的。下面就让我们通过一个例子学习一下,如何才能在应用程序里调用手机的摄像头进行拍照。
- 新建一个
CameraAlbumTest项目,然后修改activity_main.xml中的代码,如下所示:
1 |
|
Button是用于打开摄像头进行拍照的,而ImageView则是用于将拍到的图片显示出来。
2. 然后开始编写调用摄像头的具体逻辑,修改MainActivity中的代码,如下所示:
1 | class MainActivity : AppCompatActivity() { |
这段代码首先使用[registerForActivityResult()](https://developer.android.com/training/basics/intents/result)定义了一个ActivityResultLauncher启动器(这个方法代替了`startActivityForResult`),这个方法接收两个参数,这里我们传入了照片的合约:
- 参数一: 接收一个安卓预先定义好的[ActivityResultContract](https://developer.android.com/reference/androidx/activity/result/contract/ActivityResultContract)合约(官方叫协定contract),如果没有需要的合约可以[创建自己的合约](https://developer.android.com/training/basics/intents/result?hl=zh-cn#custom)
- 参数二: 接收一个合约完成后的回调函数,传入的回调参数视参数一而定.
接下来预定义了两个变量imageUri和outputImage分别是拍照后照片的Uri和照片的路径
然后在button的点击事件中先创建了一个File对象用于存放摄像头拍的照片,且重命名为`output_image.jpg`,然后将照片保存至关系缓存`/sdcard/Android/data/<package name>/cache`文件夹中,我们可以使用`getExternalCacheDir()`方法可以得到这个目录,若想获存到其它位置则需要获得运行时权限.从Android10开始引入了[作用域存储](https://blog.csdn.net/guolin_blog/article/details/105419420)这个概念,公有的SD卡目录已经不再允许被应用程序直接访问了,而是要使用作用域存储才行。
然后进行Android版本判断,如果低于Android7.0就使用Uri的`fromFile()`方法将File对象转为Uri对象,表示图片本地的真实路径.否则就调用FileProvider的`getUriForFile()`方法将File对象转为一个封装过的Uri对象.
getUriForFile()方法接收3个参数:
- 参数一: 上下文Context对象
- 参数二: 任意唯一的字符串
- 参数三:一个File对象.
之所以要进行这样一层转换,是因为从Android 7.0系统开始,直接使用本地真实路径的Uri被认为是`不安全`的,会抛出一个FileUriExposedException异常。而`FileProvider`则是一种特殊的ContentProvider,它使用了和ContentProvider类似的机制来对数据进行保护,可以选择性地将封装过的Uri共享给外部,从而提高了应用的安全性。
最后再调用开头的定义的`照片的启动器`,然后启动.
- 这里调用了相机,自然就要在
AndroidManifest.xml中对它进行注册才行,代码如下所示:
1 |
|
android:name属性的值是固定的,而android:authorities属性的值必须和刚才FileProvider.getUriForFile()方法中的第二个参数一致。另外,这里还在<provider>标签的内部使用<meta-data>指定Uri的共享路径,并引用了一个@xml/file_paths资源。当然,这个资源现在还是不存在的,下面我们就来创建它。
4. 右击res目录→New→Directory,创建一个xml目录,接着右击xml目录→New→File,创建一个file_paths.xml文件。然后修改file_paths.xml文件中的内容,如下所示:
1 | <?xml version="1.0" encoding="utf-8"?> |
external-path就是用来指定Uri共享路径的,name属性的值可以随便填,path属性的值表示共享的具体路径。这里使用一个单斜线表示将整个SD卡进行共享,当然你也可以仅共享存放output_image.jpg这张图片的路径。
5. 这样代码就编写完了,现在将程序运行到手机上,点击拍照按钮即可进行拍照。拍照完成后,点击中间按钮就会回到我们程序的界面。同时,拍摄的照片也显示出来了,如图所示。
从相册中选择图片
虽然调用摄像头拍照既方便又快捷,但我们并不是每次都需要当场拍一张照片的。因为每个人的手机相册里应该都会存有许多张图片,直接从相册里选取一张现有的图片会比打开相机拍一张照片更加常用。一个优秀的应用程序应该将这两种选择方式都提供给用户,由用户来决定使用哪一种。下面我们就来看一下,如何才能实现从相册中选择图片的功能。
- 还是在CameraAlbumTest项目的基础上进行修改,编辑
activity_main.xml文件,在布局中添加一个按钮,用于从相册中选择图片,代码如下所示:
1 | <?xml version="1.0" encoding="utf-8"?> |
- 然后修改
MainActivity中的代码,加入从相册选择图片的逻辑,代码如下所示:
1 | class MainActivity : AppCompatActivity() { |
在这里我们仅仅新增了一个启动器,然后启动它就好了
3. 现在重新运行程序,然后点击一下选择相册按钮,就会打开系统的文件选择器了,如图所示。
调用摄像头拍照以及从相册中选择图片是很多Android应用都会带有的功能,现在你已经将这两种技术都学会了,如果将来在工作中需要开发类似的功能,相信你一定能轻松完成的。不过,目前我们的实现还不算完美,因为如果某些图片的像素很高,直接加载到内存中就有可能会导致程序崩溃。更好的做法是根据项目的需求先对图片进行适当的压缩,然后再加载到内存中。至于如何对图片进行压缩,就要考验你查阅资料的能力了,这里就不再展开进行讲解了。
本小节代码
播放多媒体文件
手机上最常见的休闲方式毫无疑问就是听音乐和看电影了,随着移动设备的普及,越来越多的人可以随时享受优美的音乐,观看精彩的电影。Android在播放音频和视频方面做了相当不错的支持,它提供了一套较为完整的API,使得开发者可以很轻松地编写出一个简易的音频或视频播放器,下面我们就来具体地学习一下。
播放音频
在Android中播放音频文件一般是使用MediaPlayer类实现的,它对多种格式的音频文件提供了非常全面的控制方法,从而使播放音乐的工作变得十分简单。下表列出了MediaPlayer类中一些较为常用的控制方法。
- 下面就让我们通过一个具体的例子来学习一下吧,新建一个
PlayAudioTest项目,然后修改activity_main.xml中的代码,如下所示:
1 |
|
布局中仅有三个按钮,不过多解释.
2. 本例中使用本地音频播放,但是也可以使用网络音乐.在项目中创建一个assets目录,在其下任意子目录都可以被AssetManager这个类提供的接口对其下的文件进行读取。
3. 创建assets目录吧,它必须创建在app/src/main这个目录下面,也就是和java、
res这两个目录是平级的。右击app/src/main→New→Directory,在弹出的对话框中输
入“assets”,目录就创建完成了。
4. 在随书资源中,将music.mp3复制到assets目录中.
5. 然后修改MainActivity中的代码,如下所示:
1 | class MainActivity : AppCompatActivity() { |
在类初始化的时候,先创建了一个MediaPlayer的实例,然后在onCreate()方法中调用initMediaPlayer()方法,为MediaPlayer对象进行初始化操作。在initMediaPlayer()方法中,首先通过getAssets()方法得到了一个AssetManager的实例,AssetManager可用于读取assets目录下的任何资源。接着我们调用了openFd()方法将音频文件句柄打开,后面又依次调用了setDataSource()方法和prepare()方法,为MediaPlayer做好了播放前的准备。
- 这样一个简易版的音乐播放器就完成了,现在将程序运行到手机上,界面如图所示

- Github
播放视频
播放视频文件其实并不比播放音频文件复杂,主要是使用VideoView类来实现的。这个类将视
频的显示和控制集于一身,我们仅仅借助它就可以完成一个简易的视频播放器。VideoView的
用法和MediaPlayer也比较类似,常用方法如下表所示。
- 新建
PlayVideoTest项目,然后修改activity_main.xml中的代码,如下所示:
1 | <?xml version="1.0" encoding="utf-8"?> |
这个布局文件中同样放置了3个按钮,分别用于控制视频的播放、暂停和重新播放。另外在按钮的下面又放置了一个VideoView,稍后的视频就将在这里显示。
2. VideoView不支持播放assets目录下的视频资源,所以我们可以在res目录下新建一个raw目录,像诸如音频、视频之类的资源文件也可以放在这里,且VideoView支持播放该目录下的视频资源。
3. 现在右击app/src/main/res→New→Directory,在弹出的对话框中输入raw,完成raw目录的创建,并把要播放的视频资源放在里面。这里我提前准备了一个video资源(资源下载方式见前言),如图所示,你也可以使用自己准备的视频资源。
4. 然后修改MainActivity中的代码,如下所示:
1 | class MainActivity : AppCompatActivity() { |
这段代码现在看起来就非常简单了,因为它和前面播放音频的代码比较类似。我们首先在onCreate()方法中调用了Uri.parse()方法,将raw目录下的video.mp4文件解析成了一个Uri对象,这里使用的写法是Android要求的固定写法。然后调用VideoView的setVideoURI()方法将刚才解析出来的Uri对象传入,这样VideoView就初始化完成了。
- 现在将程序运行到手机上,点击一下
播放按钮,就可以看到视频已经开始播放了,如图所示。
- 这样的话,你就已经将VideoView的基本用法掌握得差不多了。不过,为什么它的用法和MediaPlayer这么相似呢?其实VideoView只是帮我们做了一个很好的
封装而已,它的背后仍然是使用MediaPlayer对视频文件进行控制的。另外需要注意,VideoView并不是一个万能的视频播放工具类,它在视频格式的支持以及播放效率方面都存在着较大的不足。所以,如果想要仅仅使用VideoView就编写出一个功能非常强大的视频播放器是不太现实的。但是如果只是用于播放一些游戏的片头动画,或者某个应用的视频宣传,使用VideoView还是绰绰有余的。 - Github
Kotlin课堂:使用infix函数构建更可读的语法
在之前的章节中我们使用过类似A to B这样的语法,这种语法结构的优点是可读性高,相比于调用一个函数,它更接近于使用英语的语法来编写程序。可能你会好奇,这种功能是怎么实现的呢?to是不是Kotlin语言中的一个关键字?本节的Kotlin课堂中,我们就对这个功能进行深度解密。
首先,to不是一个关键字,而是一个由infix实现的语法糖,而infix只是将函数的语法规则调整了下,比如A to B等价于A.to(B)的写法.
接下来将通过几个例子来学习infix的语法
简化startsWith()函数
String类中有一个startsWith()函数,你一定使用过,它可以用于判断一个字符串是否是以某个指定参数开头的。比如说下面这段代码的判断结果一定会是true:
1
2val reuset = "你好世界".startsWith("你好")
println(reuset)- startsWith()函数的用法虽然非常简单,但是借助infix函数,我们可以使用一种更具可读性的语法来表达这段代码。新建一个infix.kt文件,然后编写如下代码:
1
infix fun String.beginsWith(prefix: String) = startsWith(prefix)
这里给String添加了一个拓展函数beginsWith()用于判断是否字符串以某个参数开头的,且实现就是调用的String的startsWith()函数.加上infix关键字后它就变成了一个语法糖beginsWith()
3. 下面这段代码相当于调用了前面字符串的beginsWith()方法
1
val reuset = "你好世界" beginsWith "你好" // true
4. 由于infix的特殊性,它只能是某个类的成员函数或拓展函数;其次,它只能接收一个参数,但类型没有限制.看完了简单的例子,接下来我们再看一个复杂一些的例子
- 比如这里有一个集合,如果想要判断集合中是否包括某个指定元素,一般可以这样写:
1
2
3val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = list.contains("Banana")
println(result)- 但我们仍然可以借助infix函数让这段代码变得更加具有可读性。在infix.kt文件中添加如下代码:
1
infix fun <T> Collection<T>.has(element: T) = contains(element)
- 现在我们就可以使用如下的语法来判断集合中是否包括某个指定的元素:
1
2val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = list has "Banana" // true
Git时间:版本控制工具进阶
不讲,自行了解