运用手机多媒体
使用通知
创建通知渠道
其实通知这个功能设计初衷是好的,但是他已经被开发者都玩坏了.其实就开发者而言,用户用自己的软件越多越好,所以就会频繁的去通知,但是就用户而言,这样就会补没有意义的垃圾影响生活,在过去用户要么选择通知,要么选择不通知,只能一刀切.
所以引入了通知渠道的概念
用户可以自行选择关心哪些信息,自由的选择这些通知渠道的重要程度,然后决定是通知的方式(响铃,震动,不接受…)
创建通知渠道的详细步骤
- 需要一个
NotificationManager
对通知进行管理,可以用过Context
的getSystemService()
方法获取,getSystemService()
方法接收一个字符传参数用于确定获取系统的那个服务,这里我们传入Context.NOTIFICATION_SERVICE
即可,同时,获取NotificationManager
的实例可以写成val manager=getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- 接下来使用
NotificationChannel
类构建一个通知渠道,并调用NotificationManager
的NotificationChannel
方法完成创建,由于NotificationChannel
类和createNotificationChannel()
方法都是Android8.0
新增的API
,所以还需要版本判断,if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.0)){ //渠道Id,渠道名称,重要级别 val channel=NotifiChannel(channelId,channelName,importance) manager.createNotificationChannel(channel) } 重要级别有: 紧急(发出声音并显示为提醒通知) IMPORTANCE_HIGH 高(发出声音) IMPORTANCE_DEFAULT 中等(没有声音) IMPORTANCE_LOW 低(无声音并且不会出现在状态栏中) IMPORTANCE_MIN
通知(Notification)的基本用法
- 可以在Activity中创建
- 可以在BroadcastReceier中创建
- 可以在Service中创建
流程
- 需要一个
Builder
构造器来创建Notification
对象,由于Android
的API
不太稳定,变动时常会有,所以我们会用过AndroidX
中提供的兼容API
,AndroidX
库中提供了一个NotificationCompat
类,使用这个类的构造器创举建了Notification
对象val notification=NotificationCompat.Builder(context,channelId).build() //第一个参数是上下文 //第二个是渠道的Id,需要和我们在创建通知渠道时时指定的渠道id相匹配
- 上面只是创建了一个空的
Notification
对象,我们可以在最后的build()
之前连缀任意多的设置方法来创建一个丰富的Notification
对象val notification=NotificationCompat.Builder(context,channelId)//NotificationCompat可以翻译成通知兼容器 .setContentTitle("通知的标题") .setContentText("通知的内容") .setSmallIcon(R.drawable.small_icon)//通知的小图标 .setLargeIcon(BitmapFactory.decodeResource(getResources),R.drawable,R.drawable.large_icon)//通知的大图标 .build()
- 以上工作完成之后,只需要调用
NotificationManager
的notify()
方法就可以让通知显示出来manager.nofity(1,notification)
实践出真知
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
···>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/sendNotice"
android:text="发送通知"/>
</LinearLayout>
class MainActivity : AppCompatActivity() {
lateinit var binding:ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//获取viewBinding来操作布局
binding=ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
//得到通知管理器
val manager=getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
//设置一个通知渠道
//渠道Id,渠道名称,重要级别
val channel= NotificationChannel("normal","Normal",IMPORTANCE_DEFAULT)
//通过通知管理器创建一个通知渠道
manager.createNotificationChannel(channel)
//系统只会创建一次通知渠道,下次再执行代码系统就会检测到通知渠道,然后不创建
}
binding.sendNotice.setOnClickListener {
val notification= NotificationCompat.Builder(this,"normal")
.setContentTitle("通知的标题")
.setContentText("通知的内容")
.setSmallIcon(R.drawable.small_icon)//通知的小图标
.setLargeIcon(BitmapFactory.decodeResource(resources,R.drawable.large_icon))//通知的大图标
.build()
//显示通知,每个通知指定的id必须都不同
manager.notify(1,notification)
}
}
}
但是我们会发现通知点击会没有效果,可是我们自己的手机上通知点击是有效果的啊?这就涉及到一个新概念了.就是我们下面要看的.
PendingIntent
他和Intent
都是意图,可以启动Activity
,Service
以及发送广播,但不同的是PendingIntent
更倾向于在某个合适的时机执行某个动作,而Intent
是立即指定动作
PendingIntent
提供了几个静态方法用于获取PendingIntent
的实例getActivity()
-getBroadcast()
getService()
- 几个方法的参数都是一样的:
contxet
:上下文- 第二个参数一般用不到,传入
0
就行 Intent
对象,通过这个来构建出PendingIntent
的意图- 第四个参数用于确定
PendingIntent
的行为FLAG_ONE_SHOT
获取的PendingIntent
只能使用一次FLAG_ON_CREATE
获取PendingIntent
,若描述的Intent
不存在则返回NULL
值.如果描述的PendingIntent
已经存在,则在使用新的Intent
之前会先取消掉当前的。你可以通过这个去取回,并且通过取消先前的Intent
,更新在Intent
中的数据。这能确保对象被给予新的数据。如果无法保证唯一,考虑使用flag_update_current
。FLAG_CANCEL_CURRENT
会关闭之前PendingIntent
FLAG_UPDATE_CURRENT
会更新之前PendingIntent
的消息- 传0就不会有啥效果
实践出真知
NotificationActivity中
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
···
android:orientation="vertical">
<!--android:layout_centerInParent="true"是在RelativeLayout布局中在父布局的正中心-->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="24sp"
android:text="通知活动"
android:id="@+id/notificationTextView"
android:gravity="center"/>
</RelativeLayout>
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是1的通知
manager.cancel(1)
}
}
class MainActivity : AppCompatActivity() {
lateinit var binding:ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
···
binding.sendNotice.setOnClickListener {
val intent=Intent(this,NotificationActivity::class.java)
val pi=PendingIntent.getActivity(this,0, intent,0)
val notification= NotificationCompat.Builder(this,"normal")
···
.setContentIntent(pi)//设置通知的意图内容
.setAutoCancel(false)//设置通知是不是点击完了之后就自动关闭
.build()
//显示通知,每个通知指定的id必须都不同
manager.notify(1,notification)
}
}
}
通知的进阶技巧(小声逼逼:只有少部分常用API)
setStyle()
方法:这个方法允许我们构建出富文本的通知内容。也就是说,通知中不光可以有文字和图标,还可以有其他的,setStyle()接收一个NotificationCompat.Style参数,这个参数是用来构建具体的富文本信息,比如长文字、图片比如: val notification= NotificationCompat.Builder(this,"normal") ··· .setContentText("hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh") .build() 直接设置一长串内容的话通知的时候是无法显示完全的,显示不了的就会用省略号代替 如果我们用setStyle()的话 val notification= NotificationCompat.Builder(this,"normal") ··· .setStyle(NotificationCompat.BigTextStyle().bigText("hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh")) .build() 就可以显示完全了 其实setStyle()方法中我们是创建了一个NotificationCompat.BigTextStyle对象,这个对象就是用来封装长文字信息的,只要调用他的bigText()方法并将文字内容传入就可以了 还可以显示图片 val notification= NotificationCompat.Builder(this,"normal") ··· .setStyle(NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(resources,R.drawable.big_image))) .build() 其实setStyle()方法中我们是创建了一个NotificationCompat.BigPictureStyle对象,这个对象就是用来封装图片信息的,只要调用他的BitmapFactory()的decodeResources()方法并将图片解析成Bitmap,再传入Bitmap()就可以了
- 不同重要等级的通知渠道对通知的行为的影响
需要注意的时通知渠道一旦通过代码创建出来,开发者是不能改变其重要等级的,只能由用户来改class MainActivity : AppCompatActivity() { ··· override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ··· val manager=getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){ ··· val channel2=NotificationChannel("important","Important", IMPORTANCE_HIGH) //这里把通知渠道设定成了高,那么就重要成功很高了,其实还可以试试其他的级别 manager.createNotificationChannel(channel2) } binding.sendNotice.setOnClickListener { ··· val intent1=Intent(this,NotificationActivity::class.java) val pi1=PendingIntent.getActivity(this,1,intent1,0) val notification1=NotificationCompat.Builder(this,"important") ··· .build() manager.notify(2,notification1) } } }
到了万众期待的缓解了-调用摄像头和相册
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.bo.a2_learncameraalbum">
<!--权限不能忘-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
···
<!--在代码里面用到了FileProvider所以需要注册
android:name的值是固定的
android:authorities的值必须要跟代码中的FileProvider.getUriForFile()方法的第二个参数一样
android:exported是否支持其它应用调用当前组件
android:grantUriPermissions是否授予访问Uri的权限-->
<provider
android:authorities="com.bo.a2_learncameraalbum.fileprovider"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
<!--meta-data指定共享路径-->
</provider>
</application>
</manifest>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
···>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/takePhotoBtn"
android:text="拍照"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/fromAlbumBtn"
android:text="查看相册里的照片"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/imageView"
android:layout_gravity="center_horizontal"/>
</LinearLayout>
共享路径的xml配置(./xml/file_paths.xml)
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="/"/>
<!--external-path 是用来指定Uri共享的
name里面的值可以随便填
path里面的值表明共享的具体路径,这里一个/表明是将整个SD卡共享-->
</paths>
注释之王登场
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
val takePhoto=1
lateinit var imageUri: Uri
lateinit var outputImage: File
val formAlbum=2
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
//点击拍照触发事件,拍完照之后会在Activity上显示这张照片
binding.takePhotoBtn.setOnClickListener {
/*创建File对象,储存拍照后的照片,拍摄的照片存放在手机SD卡的应用关联缓存目录下
应用关联缓存目录:SD卡中专门用于存放当前应用缓存数据的位置
File(第一个参数,第二个参数)
第一个参数:externalCacheDir-----具体的路径时/sdcard/Android/data/<package name>/cache
第二个参数:文件名称
这两个参数会被拼接成一个字符串路径,然后解析成内容Uri,存放在这个路径下
为什么用关联缓存目录来存放图片?:
Android6.0开始读写SD卡就成了危险权限,如果放在其他目录就会要申请权限,而再这个目录下就不需要
Android10.0开始公有的SD卡目录已经不再能被应用程序直接访问了,而是要用作用域存储才行
作用域存储详情要去gl大大的微信公众号看看*/
outputImage= File(externalCacheDir,"output_image.jpg")
//必须检查以免出错,检查一下这个文件存不存在,存在就替换掉
if(outputImage.exists()){
outputImage.delete()
}
outputImage.createNewFile()
/*如果当前的Android版本低于7.0就可以直接调用Uri的fromFile()方法,把File对象转化成Uri对象
此时的Uri对象对应的时这个文件的本地真实路径
如果不低于的话就需要用FileProvider类的getUriForFile(第一个参数,第二个参数,第三个参数)方法将一个File对象转化成封装过的Uri对象
第一个参数:context上下文
第二个参数:任意一个字符串,但是还是用项目的authority最好(保持唯一性)
第三个参数:需要转化的File对象*/
/*那为什么要这样if-else呢?
因为从Android7.0开始直接使用真实本地地址的Uri被认为是不安全的
会抛出一个FileUriExposedException异常
而FileProvider是一种特殊的ContentProvider,它使用和ContentProvider相似的机制来保护数据*/
imageUri=if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
FileProvider.getUriForFile(this,"com.bo.a2_learncameraalbum.fileprovider",outputImage)
}else{
Uri.fromFile(outputImage)
}
//启动相机程序(隐式intent)
//启动相机的动作是"android.media.action.IMAGE_CAPTURE"
val intent=Intent("android.media.action.IMAGE_CAPTURE")
//通过intent的附加信息来指定相机拍摄的照片输出(MediaStore.EXTRA_OUTPUT)的位置(imageUri)
//位置是一个Uri对象来制定的
intent.putExtra(MediaStore.EXTRA_OUTPUT,imageUri)
//开启一个活动(这个方法开启的活动运行完了之后会回调onActivityResult(),结果返回到onActivityResult()的data中)
startActivityForResult(intent,takePhoto)
}
binding.fromAlbumBtn.setOnClickListener {
//打开文件管理器
val intent=Intent(Intent.ACTION_OPEN_DOCUMENT)//指定意图去打开文件
intent.addCategory(Intent.CATEGORY_OPENABLE)//过滤:只打开能打开的文件
//指定显示图片
intent.type="image/*"//只能显示image目录下的图片
startActivityForResult(intent,formAlbum)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when(requestCode){
takePhoto->{
if(resultCode== Activity.RESULT_OK){
//如果运行的结果是成功的话就显示一下图片
/*还记得吗?现在的Android要跨程序获取数据就要借助contentResolver类
这里通过contentResolver开辟一个文件输入流,通过路径找到文件读入
再然后通过位图工厂把字节流形式的数据转化成一个位图*/
val bitmap=BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri))
binding.imageView.setImageBitmap(rotateRequired(bitmap))
}
}
formAlbum->{
if(resultCode==Activity.RESULT_OK&&data!=null){
data?.data.let { uri->
val bitmap= uri?.let { getBitmapFromUri(it) }
binding.imageView.setImageBitmap(bitmap)
}
}
}
}
}
private fun getBitmapFromUri(uri: Uri): Bitmap? {
//通过contentResolver用只读的方式加载文件
return contentResolver.openFileDescriptor(uri,"r")?.use{
BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
//然后把文件转成位图
//use可以在使用完这个文件之后自动关上
}
}
//手机拍摄的时候图片可能拍成歪的,这个函数就是把他扶正
private fun rotateRequired(bitmap: Bitmap): Bitmap {
val exif=ExifInterface(outputImage.path)
val organization=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,ExifInterface.ORIENTATION_NORMAL)
return when(organization){
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
}
}
播放多媒体
播放音频
Android
中播放啊音频是要通过MediaPlayer
类实现的
Mediaplayer的工作流程
- 创建一个
Mediaplayer
对象 - =>调用
setDataSource()
方法设置音频文件的路径 - =>调用
prepare()
方法使MediaPlayer()
进入准备状态 - =>调用
start()
方法播放音频 - =>调用
pause()
暂停播放 - =>调用
reset()
停止播放
操练起来
布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
···>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/play"
android:text="播放音频"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/pause"
android:text="暂停播放"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/stop"
android:text="停止播放"/>
</LinearLayout>
主活动
class MainActivity : AppCompatActivity() {
private lateinit var binding:ActivityMainBinding
private val mediaPlayer=MediaPlayer()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding= ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initMediaPlayer()
binding.play.setOnClickListener {
if(!mediaPlayer.isPlaying)
mediaPlayer.start()
}
binding.pause.setOnClickListener {
if(mediaPlayer.isPlaying)
mediaPlayer.pause()
}
binding.stop.setOnClickListener {
if(mediaPlayer.isPlaying){
mediaPlayer.reset()//重置mediaPlayer,重置完了之后需要重新初始化mediaPlayer
initMediaPlayer()
}
}
}
private fun initMediaPlayer(){
/*放在assets中的文件是可以直接加载的到assets管理器中的*/
val assetsManager=assets
/*调用openfd()方法打开句柄
反正就跟把柄一个意思,掌握了你的把柄,你就难逃法网(系统),(系统)要找到你就可以根据句柄来找你*/
val fd=assetsManager.openFd("music.mp3")
mediaPlayer.setDataSource(fd.fileDescriptor,fd.startOffset,fd.length)
mediaPlayer.prepare()
}
override fun onDestroy() {
super.onDestroy()
mediaPlayer.stop()//停止mediaPlayer的功能
mediaPlayer.release()//释放mediaPlayer的相关资源
}
}
播放视频
播放视频主要是使用VideoView
类实现
但是VideoView
并不是一个万能的视频播放工具,他对视频格式的支持和播放效率方面有很大不足
操练起来
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/play"
android:text="播放视频"/>
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/pause"
android:text="暂停播放"/>
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/stop"
android:text="停止播放"/>
</LinearLayout>
<VideoView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/videoView"/>
</LinearLayout>
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding= ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
//通过解析路径的方式获取到文件的Uri对象
val uri= Uri.parse("android.resource://$packageName/${R.raw.video}")
binding.videoView.setVideoURI(uri)//通过Uri对象把视频设置到viewView空间中去
binding.play.setOnClickListener {
if(!binding.videoView.isPlaying)
binding.videoView.start()
}
binding.pause.setOnClickListener {
if(binding.videoView.isPlaying)
binding.videoView.pause()
}
binding.stop.setOnClickListener {
if(binding.videoView.isPlaying){
binding.videoView.resume()//重新开始播放
}
}
}
override fun onDestroy() {
super.onDestroy()
binding.videoView.suspend()//把videoView占用的资源释放掉
}
}
可恶,居然只有画面,没有声音,也不知道正不正常
y.setOnClickListener {
if(!binding.videoView.isPlaying)
binding.videoView.start()
}
binding.pause.setOnClickListener {
if(binding.videoView.isPlaying)
binding.videoView.pause()
}
binding.stop.setOnClickListener {
if(binding.videoView.isPlaying){
binding.videoView.resume()//重新开始播放
}
}
}
override fun onDestroy() {
super.onDestroy()
binding.videoView.suspend()//把videoView占用的资源释放掉
}
}
**可恶,居然只有画面,没有声音,也不知道正不正常**