运用手机多媒体

将程序运行到手机上

进入“关于手机”界面,然 后对着最下面的版本号那一栏连续点击,就会让开发者选项显示出来。然后进入设置→系统→ 开发者选项界面,并在这个界面中选中USB调试选项。

使用通知

创建通知渠道

通知渠道:每条通知都要属于一个对应的渠道

创建通知渠道的详细步骤

首先需要一个NotificationManager对通知进行管理,可以通过调用Context的getSystemService()方法获取。getSystemService()方法接收一个字符串参数用于确定获取系统的哪个服务,传入Context.NOTIFICATION_SERVICE即可。

val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

接下来要使用NotificationChannel类构建一个通知渠道,并调用NotificationManager的createNotificationChannel()方法完成创建。由于NotificationChannel类和createNotificationChannel()方法都是Android 8.0系统中新增的API,因此我们在使用的时候还需要进行版本判断才可以

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

创建一个通知渠道至少需要渠道ID、渠道名称以及重要等级这3个参数,其中ID是可以随便定义,只要保证全局唯一性就可以。名称是给用户看的,需要可以清楚地表达这个渠道的用途。通知的重要等级主要有IMPORTANCE_HIGH, IMPORTANCE_DEFAULT, IMPORTANCE_LOW, IMPORTANCE_MIN这几种,对应的重要程度依次重高到低

通知的基本用法

使用一个Builder构造器来创建Notification对象

val notification = NotificationCompat.Builder(context,channelId).build()

第一个参数是context,第二个参数是渠道ID,需要我和我们在创建通知时指定的渠道ID相匹配才行

我们还可以在最终的build()方法之前连缀任意多的设置方法来创建一个丰富的Notification对象

val notification = NotificationCompat.Builder(context,channelId)
.setContentTitle("This is content title")
.setContentText("This is content text")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResoruce(getResoruces(),R.drawable.large_icon))
.build()

setContentTitle()方法用于指定通知的标题内容;setContentText()方法用于指定通知的正文内容。setSmallIcon()方法用于设置通知的小图标。setLargeIcon()方法用于指定通知的大图标

以上工作都完成后,调用NotificationManager的notify()方法就可以让通知显示出来了。notify方法接收两个参数:第一个参数时id,要保证为每个通知指定的id都是不同的;第二个参数则是Notification对象

manager.notify(1,notification)

新建一个NotificationTest项目,并修改activity_main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

<Button
android:id="@+id/sendNotice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send Notice" />

</LinearLayout>

布局文件非常简单,里面只有一个“Send Notice”按钮,用于发出一条通知。接下来修改 MainActivity中的代码,如下所示:

class MainActivity:AppCompatActivity(){
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState:Bundle?)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val sedNotice = binding.sendNotice
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)
}
sendNotice.setOnClickListener{
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()
manager.notify(1,notification)
}
}
}

还要申请权限

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

实现通知的点击效果,我们还需要在代码中进行相应的设置,这就涉及了一个新的概念—— PendingIntent。

PendingIntent从名字上看起来就和Intent有些类似,它们确实存在不少共同点。比如它们都可 以指明某一个“意图”,都可以用于启动Activity、启动Service以及发送广播等。不同的是, Intent倾向于立即执行某个动作,而PendingIntent倾向于在某个合适的时机执行某个动作。所 以,也可以把PendingIntent简单地理解为延迟执行的Intent。

class MainActivity:AppCompatActivity(){
override fun onCreate(savedInstanceState:Bundle?){
sendNotice.setOnClickListener{
val intent = Intent(this,NotificationActivity::class.java)
val pi = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_IMMUTABLE)
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))
.setContentIntent(pi)
.setAutoCancel(true)
.build()
manager.notify(1, notification)
}
}
}

通知的进阶技巧

设置长文字, 通过setStyle()方法

val notification = NotificationCompat.Builder(this, "normal") 
...
.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."))
.build()

除了显示长文字之外,通知里还可以显示一张大图片,具体用法是基本相似的:

val notification = NotificationCompat.Builder(this, "normal") 
...
.setStyle(NotificationCompat.BigPictureStyle().bigPicture(
BitmapFactory.decodeResource(resources, R.drawable.big_image)))
.build()

接下来,我们学习一下不同重要等级的通知渠道对通知的行为具体有什么影响。其实简单来 讲,就是通知渠道的重要等级越高,发出的通知就越容易获得用户的注意。比如高重要等级的通知渠道发出的通知可以弹出横幅、发出声音,而低重要等级的通知渠道发出的通知不仅可能 会在某些情况下被隐藏,而且可能会被改变显示的顺序,将其排在更重要的通知之后。

但需要注意的是,开发者只能在创建通知渠道的时候为它指定初始的重要等级,如果用户不认 可这个重要等级的话,可以随时进行修改,开发者对此无权再进行调整和变更,因为通知渠道 一旦创建就不能再通过代码修改了。

既然无法修改之前创建的通知渠道,那么我们就只好再创建一个新的通知渠道来测试了。修改 MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val sendNotice = binding.sendNotice
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)
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,PendingIntent.FLAG_IMMUTABLE)
val notification = NotificationCompat.Builder(this, "important")
.setContentTitle("This is content title")
.setContentText("This is content text")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
.setContentIntent(pi)
.build()
manager.notify(1,notification)
}
}
}

调用摄像头和相册

调用摄像头拍照

新建一个CameraAlbumTest项目,然后修改activity_main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/takePhotoBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Take Photo"
/>
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
/>
</LinearLayout>

可以看到,布局文件中只有两个控件:一个Button和一个ImageView。Button是用于打开摄 像头进行拍照的,而ImageView则是用于将拍到的图片显示出来。

然后开始编写调用摄像头的具体逻辑,修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity(){
val takePhoto = 1
lateinit var imageUri : Uri
lateinit var outputImage : File
lateinit var ImageView : ImageView
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
imageView = binding.imageView
val takePhotoBtn = binding.takePhoto
takePhoto.setOnClickListener{
outputImage = File(externalCacheDir,"output_image.jpg")
if(outputImage.exists()){
outputImage.delete()
}
outputImage.createNewFile()
imageUri = if(Build.VERSION.SDK_INT>=Build.VERSION_CODE.N){
FileProvider.getUriForFile(this,"com.example.cameraalbumtest.fileprovider",outputImage)
}else{
Uri.fromFile(outputImage)
}
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(requestCode == Activity.RESULT_OK){
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri))
imageView.setImageBitmap(rotataIfRequired(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,189)
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 rotateBitmap = Bitmap.createBitmap(bitmap,0,0,bitmap.width,bitmap.height,matrix,true)
bitmap.recycle()
return rotateBitmap
}

}
}

首先创建了一个File对象,用于存放摄像头拍下的图片,这里我们把图片命名为output_image.jpg,并存放在手机SD卡的应用关联缓存目录下。应用关联缓存目录就是指SD卡中专门存放当前应用缓存数据的位置,调用getExternalCacheDir()方法可以得到这个目录,具体的路径是/sdcard/Android/data//cache。那么为什么要使用应用关联缓存目录来存放图片呢?因为从Android6.0系统开始,读写SD卡被列为了危险权限,如果将图片存放在SD卡的任何其他目录,都要进行运行时权限处理才行,而使用关联目录则可以跳过这一步。

接着会进行一个判断,如果运行设备的系统版本低于Android 7.0,就调用Uri的fromFile()将File对象转成Uri对象,这个对象标识着output_image.jgp这张图片的本地真实路径。否则,就调用FileProvider的getUriForFile()方法将File对象转换成一个封装过的Uri对象。getUriForFile()方法接收3个参数:第一个参数要求Context对象,第二个参数可以时任意唯一的字符串,第三个参数是File对象

不过现在还没结束,刚才提到了ContentProvider,那么我们自然要在AndroidManifest.xml 中对它进行注册才行,代码如下所示:

、、、
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.cameraalbumtest.fileprovider"
android:exported="false"
android:grantUriPermissons="true"
>
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"
/>
</provider>

android:name属性的值是固定的,而android:authorities属性的值必须和刚才 FileProvider.getUriForFile()方法中的第二个参数一致。另外,这里还在 标签的内部使用指定Uri的共享路径,并引用了一个@xml/file_paths资源。 当然,这个资源现在还是不存在的,下面我们就来创建它。

右击res目录→New→Directory,创建一个xml目录,接着右击xml目录→New→File,创建一 个file_paths.xml文件。然后修改file_paths.xml文件中的内容,如下所示:

<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name ="my_images" path="/"/>
</paths>

从相册中选择图片

修改MainActivity中的代码,加入从相册选择图片的逻辑,代码如下所示:

class MainActivity : AppCompatActivity(){
val fromAlbum = 2
override fun onCreate(svaedInstanceState:Bundle?){
、、、
fromAlbumBtn.setOnClickListener{
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORYY_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->
val bitmap = getBitmapFromUri(uri)
imageView.setImageBitmap(bitmap)
}
}
}
}
}

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

播放多媒体文件

播放音频

在Android中播放音频文件一般是使用MediaPlayer类

MediaPlayer可以用于播放网络、本地以及应用程序安装包中的音频。这里简单起见,我们就 以播放应用程序安装包中的音频来举例吧。

Android Studio允许我们在项目工程中创建一个assets目录,并在这个目录下存放任意文件和 子目录,这些文件和子目录在项目打包时会一并被打包到安装文件中,然后我们在程序中就可 以借助AssetManager这个类提供的接口对assets目录下的文件进行读取。

那么首先来创建assets目录吧,它必须创建在app/src/main这个目录下面,也就是和java、 res这两个目录是平级的。右击app/src/main→New→Directory,在弹出的对话框中输 入“assets”,目录就创建完成了。

然后修改MainActivity的代码

class MainActivity : AppCompatActivity{
private val mediaPlayer = MediaPlayer()

override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(saveInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initMediaPlayer()
val play = binding.play
val pause = binding.pause
val stop = binding.stop
play.setOnClickListener{
if(!mediaPlayer.isPlaying){
mediaPlayer.start()
}
}
pause.setOnClickListener{
if(mediaPlayer.isPlaying){
mediaPlayer.pause()
}
}
stop.setOnClickListener{
if(mediaPlayer.isPlaying){
mediaPlayer.reset()
initMediaPlayer()
}
}
private fun initMedaiPlayer(){
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类来实现

新建PlayVideoTest项目,然后修改 activity_main.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >

<Button
android:id="@+id/play"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Play" />

<Button
android:id="@+id/pause"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Pause" />

<Button
android:id="@+id/replay"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Replay" />
</LinearLayout>

<VideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

</LinearLayout>

现在右击app/src/main/res→New→Directory,在弹出的对话框中输入“raw”,完成raw目录 的创建,并把要播放的视频资源放在里面。

然后修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity() {
private lateinit var videoView : VideoView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val play = binding.play
val pause = binding.pause
val replay = binding.replay
videoView = binding.videoView
val uri = Uri.parse("android.resource://$packageName/${R.raw.video}")
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()
}
}

在 onCreate()方法中调用了Uri.parse()方法,将raw目录下的video.mp4文件解析成了一个 Uri对象,这里使用的写法是Android要求的固定写法。

Kotlin课堂:使用infix函数构建更可读的语法

String类中有一个startsWith()函数,你一定使用过,它可以用于判断一个字符串是否是以 某个指定参数开头的。比如说下面这段代码的判断结果一定会是true:

if ("Hello Kotlin".startsWith("Hello")) { 
// 处理具体的逻辑
}

startsWith()函数的用法虽然非常简单,但是借助infix函数,我们可以使用一种更具可读 性的语法来表达这段代码。新建一个infix.kt文件,然后编写如下代码:

infix fun String.beginsWith(prefix: String) = startsWith(prefix)

首先,除去最前面的infix关键字不谈,这是一个String类的扩展函数。我们给String类添 加了一个beginsWith()函数,它也是用于判断一个字符串是否是以某个指定参数开头的,并 且它的内部实现就是调用的String类的startsWith()函数。

但是加上了infix关键字之后,beginsWith()函数就变成了一个infix函数,这样除了传统 的函数调用方式之外,我们还可以用一种特殊的语法糖格式调用beginsWith()函数,如下所 示:

if ("Hello Kotlin" beginsWith "Hello") { 
// 处理具体的逻辑
}

从这个例子就能看出,infix函数的语法规则并不复杂,上述代码其实就是调用的” Hello Kotlin “这个字符串的beginsWith()函数,并传入了一个”Hello”字符串作为参数。但是 infix函数允许我们将函数调用时的小数点、括号等计算机相关的语法去掉,从而使用一种更 接近英语的语法来编写程序,让代码看起来更加具有可读性。

另外,infix函数由于其语法糖格式的特殊性,有两个比较严格的限制:首先,infix函数是 不能定义成顶层函数的,它必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某 个类当中;其次,infix函数必须接收且只能接收一个参数,至于参数类型是没有限制的。只 有同时满足这两点,infix函数的语法糖才具备使用的条件,你可以思考一下是不是这个道 理。

看完了简单的例子,接下来我们再看一个复杂一些的例子。比如这里有一个集合,如果想要判 断集合中是否包括某个指定元素,一般可以这样写:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape") 
if (list.contains("Banana")) {
// 处理具体的逻辑
}

很简单对吗?但我们仍然可以借助infix函数让这段代码变得更加具有可读性。在infix.kt文件 中添加如下代码:

infix fun <T> Collection<T>.has(element: T) = contains(element) 

现在我们就可以使用如下的语法来判断集合中是否包括某个指定的元素:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape") 
if (list has "Banana") {
// 处理具体的逻辑
}

好了,两个例子都已经看完了,你对于infix函数应该也了解得差不多了。但是或许现在你的 心中还有一个疑惑没有解开,就是mapOf()函数中允许我们使用A to B这样的语法来构建键值 对,它的具体实现是怎样的呢?为了解开谜团,我们直接来看一看to()函数的源码吧,按住 Ctrl键(Mac系统是command键)点击函数名即可查看它的源码,如下所示:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)