9.1 前置工作

如果想使用自己的手机而不是虚拟机对应用进行开发,就可以参照下面的步骤进行操作,否则下面的步骤可以跳过。

为了方便演示,这里是在虚拟机演示如何开启USB调试,手机端步骤类似。

打开手机开发者模式

image.pngimage.pngimage.png

开启USB调试

image.pngimage.pngimage.pngimage.png


9.2 使用通知

通知的简介

  1. 通知是Android系统中比较有特色的一个功能,当某个应用程序希望向用户发出一些**提示信息**,而该应用程序又不在前台运行时,就可以借助通知来实现。发出一条通知后,手机最上方的状态栏中会显示一个通知的图标,下拉状态栏后可以看到通知的详细内容。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22022942/1650871607797-db97eb7e-efbd-4369-b52d-1d291a154627.png#clientId=u3f4a4ca5-88cb-4&from=paste&height=691&id=uade93873&originHeight=864&originWidth=408&originalType=binary&ratio=1&rotation=0&showTitle=false&size=125193&status=done&style=none&taskId=u194cf785-840f-45ec-b50c-103e64ac4a4&title=&width=326.4)

通知的渠道

  1. 为了让用户自主过滤各种推送,Android 8.0系统引入了**通知渠道**这个概念,指的是每条通知都要属于一个对应的渠道。每个应用程序都可以自由地创建当前应用拥有哪些通知渠道,但是这些通知渠道的控制权是掌握在用户手上的。用户可以自由地选择这些通知渠道的重要程度,是否响铃、是否振动或者是否要关闭这个渠道的通知。

简单的来说,为了方便管理各个通知,加入了渠道这个概念,用户可以选择关闭不想要的通知的渠道。

这个渠道大家可以在设置里面的应用管理中查看。
image.pngimage.png
image.pngimage.png

创建通知渠道

先看实例:

实例

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="vertical"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent">
  5. <Button
  6. android:id="@+id/sendNotice"
  7. android:layout_width="wrap_content"
  8. android:layout_height="wrap_content"
  9. android:text="Send Notice" />
  10. </LinearLayout>
  1. package com.example.notificationtest
  2. import android.app.NotificationChannel
  3. import android.app.NotificationManager
  4. import android.content.Context
  5. import android.graphics.BitmapFactory
  6. import android.os.Build
  7. import androidx.appcompat.app.AppCompatActivity
  8. import android.os.Bundle
  9. import android.widget.Button
  10. import androidx.core.app.NotificationCompat
  11. class MainActivity : AppCompatActivity() {
  12. override fun onCreate(savedInstanceState: Bundle?) {
  13. super.onCreate(savedInstanceState)
  14. setContentView(R.layout.activity_main)
  15. val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
  16. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  17. val channel = NotificationChannel("normal", "Normal",NotificationManager.IMPORTANCE_DEFAULT)
  18. manager.createNotificationChannel(channel)
  19. }
  20. val sendNotice: Button = findViewById(R.id.sendNotice)
  21. sendNotice.setOnClickListener {
  22. val notification = NotificationCompat.Builder(this, "normal")
  23. .setContentTitle("This is content title")
  24. .setContentText("This is content text")
  25. .setSmallIcon(R.drawable.small_icon)
  26. .setLargeIcon(BitmapFactory.decodeResource(resources,R.drawable.large_icon))
  27. .build()
  28. manager.notify(1, notification)
  29. }
  30. }
  31. }

下面拆解出来解析:

创建通知管理者NotificationManager

  1. val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

很显然,通知这个功能属于系统服务,所以需要调用getSystemService用于获取系统服务,其中的参数就根据我们需要的服务来写入,这里就是通知服务,所以是Context.NOTIFICATION_SERVICE,最后记得使用as关键词,可以理解为把getSystemService返回的对象转化为NotificationManager类型。

getSystemService原方法返回的是Object类型。

构建通知渠道

  1. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  2. val channel = NotificationChannel("normal", "Normal",NotificationManager.IMPORTANCE_DEFAULT)
  3. manager.createNotificationChannel(channel)
  4. }

首先这里外面嵌套了一个if语句,这个if语句的作用是用于判断手机SDK版本是否支持创建消息渠道。
Build.VERSION.SDK_INT表示的是当前的SDKBuild.VERSION_CODES._O_表示的是支持创建消息渠道的最低版本
image.png

查看源码可得,这个最低版本就是26

里面两句就是创建渠道的语法:

  1. val channel = NotificationChannel("normal", "Normal",NotificationManager.IMPORTANCE_DEFAULT)
  2. manager.createNotificationChannel(channel)

首先要定义一个渠道channel,需要三个参数:

  • 渠道ID
  • 渠道名称
  • 重要等级
    • IMPORTANCE_HIGH
    • IMPORTANCE_DEFAULT
    • IMPORTANCE_LOW
    • IMPORTANCE_MIN

前面两个ID名称很好理解,最后一个参数,对应的重要程度依次从高到低,不同的重要等级会决定通知的不同行为。
接下来创建渠道,就只需要把刚才的channel当做参数传入即可。

发送通知

  1. val sendNotice: Button = findViewById(R.id.sendNotice)
  2. sendNotice.setOnClickListener {
  3. val notification = NotificationCompat.Builder(this, "normal")
  4. .setContentTitle("This is content title")
  5. .setContentText("This is content text")
  6. .setSmallIcon(R.drawable.small_icon)
  7. .setLargeIcon(BitmapFactory.decodeResource(resources,R.drawable.large_icon))
  8. .build()
  9. manager.notify(1, notification)
  10. }

首先需要通过NotificationCompat定义构建一个通知

  1. val notification = NotificationCompat.Builder(this, "normal")
  1. 接下来就是对这个通知设置相应的属性:
  1. setContentTitle()方法用于指定通知的标题内容,下拉系统状态栏就可以看到这部分内容。
  2. setContentText()方法用于指定通知的正文内容,同样下拉系统状态栏就可以看到这部分内容。
  3. setSmallIcon()方法用于设置通知的小图标,注意,只能使用纯alpha图层的图片进行设置,小图标会显示在系统状态栏上。
  4. setLargeIcon()方法用于设置通知的大图标,当下拉系统状态栏时,就可以看到设置的大图标了。
    1. BitmpFactory.decodeResource(Resource res,int id)根据给定的资源Id解析成位图
  5. build()方法用于构建

更多参见:
Android开发——Notification通知的使用及NotificationCopat.Builder常用设置API - Stars-one - 博客园
最后使用**manager**发送通知即可:

  1. manager.notify(1, notification)

notify()方法接收两个参数:

  • 第一个参数是id,要保证为每个通知指定的id都是不同的;
  • 第二个参数则是Notification对象;

image.png

点击通知

如果你使用过Android手机,此时应该会下意识地认为这条通知是可以点击的。但是当你去点击它的时候,会发现没有任何效果。
要想实现通知的点击效果,我们还需要在代码中进行相应的设置——PendingIntent
PendingIntent类似于intent,都有转跳activity的作用,PendingIntent可以简单地理解为延迟执行Intent

点击效果

新建一个NotificationActivity

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="match_parent"
  3. android:layout_height="match_parent" >
  4. <TextView
  5. android:layout_width="wrap_content"
  6. android:layout_height="wrap_content"
  7. android:layout_centerInParent="true"
  8. android:textSize="24sp"
  9. android:text="This is notification layout"
  10. />
  11. </RelativeLayout>

修改一下前面的MainActivity中的点击事件:

  1. sendNotice.setOnClickListener {
  2. val intent = Intent(this, NotificationActivity::class.java)
  3. val pi = PendingIntent.getActivity(this, 0, intent, 0)
  4. val notification = NotificationCompat.Builder(this, "normal")
  5. .setContentTitle("This is content title")
  6. .setContentText("This is content text")
  7. .setSmallIcon(R.drawable.small_icon)
  8. .setLargeIcon(BitmapFactory.decodeResource(resources,R.drawable.large_icon))
  9. .setContentIntent(pi)
  10. .build()
  11. manager.notify(1, notification)
  12. }
  1. 重点是这两句:
  1. val intent = Intent(this, NotificationActivity::class.java)
  2. val pi = PendingIntent.getActivity(this, 0, intent, 0)

第一句很简单,就是定义一个意图,转跳到NotificationActivity
第二句根据需求来选择是使用getActivity()方法、getBroadcast()方法,还是getService()方法。他们的参数说明如下:

  • 第一个参数是上下文
  • 第二个参数 requestCode,一般写0就可以了。
  • 第三个参数是 Intent,用来存储信息
  • 第四个参数用于确定PendingIntent行为(通常写**0**就可以了)
    • FLAG_CANCEL_CURRENT
    • FLAG_UPDATE_CURRENT
    • FLAG_ONE_SHOT
    • FLAG_NO_CREATE

      注意配置通知属性的时候要加上setContentIntent(pi)

这样之后,点击通知,就可以跳转到NotificationActivity

通知的状态图标

配置通知属性的时候加上:

  1. setAutoCancel(true)

就表示当点击这个通知的时候,通知会自动取消。
也可以写在NotificationActivity当中:

  1. class NotificationActivity : AppCompatActivity() {
  2. override fun onCreate(savedInstanceState: Bundle?) {
  3. super.onCreate(savedInstanceState)
  4. setContentView(R.layout.activity_notification)
  5. val manager = getSystemService(Context.NOTIFICATION_SERVICE) as
  6. NotificationManager
  7. manager.cancel(1)
  8. }
  9. }
  1. 这里在`cancel()`方法中传入了`**1**`,创建通知的时候给每条通知指定的`id`条通知设置的`id`就是`**1**`。因此,如果想取消哪条通知,在`cancel()`方法中传入该通知的`id`就行了。

通知的进阶技巧

NotificationCompat.Builder中提供了非常丰富的API,以便我们创建出更加多样的通知效果。其中就包含了**setStyle()**方法,这个方法允许我们构建出富文本的通知内容。也就是说,通知中不光可以有文字和图标,还可以包含更多的东西。

显示长文字通知

使用setStyle()方法替代setContentText()方法,并在setStyle()方法中创建一个NotificationCompat.BigTextStyle对象,这个对象就是用于封装长文字信息的,只要调用它的bigText()方法并将文字内容传入就可以了。

  1. sendNotice.setOnClickListener {
  2. val intent = Intent(this, NotificationActivity::class.java)
  3. val pi = PendingIntent.getActivity(this, 0, intent, 0)
  4. val notification = NotificationCompat.Builder(this, "normal")
  5. .setContentTitle("This is content title")
  6. .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."))
  7. .setSmallIcon(R.drawable.small_icon)
  8. .setLargeIcon(BitmapFactory.decodeResource(resources,
  9. R.drawable.large_icon))
  10. .setContentIntent(pi)
  11. .setAutoCancel(true)
  12. .build()
  13. manager.notify(1, notification)
  14. }

image.png

显示大图片

仍然调用的setStyle()方法,在参数中创建一个NotificationCompat.BigPictureStyle对象,这个对象就是用于设置大图片的,然后调用它的bigPicture()方法并将图片传入即可。

  1. sendNotice.setOnClickListener {
  2. val intent = Intent(this, NotificationActivity::class.java)
  3. val pi = PendingIntent.getActivity(this, 0, intent, 0)
  4. val notification = NotificationCompat.Builder(this, "normal")
  5. .setContentTitle("This is content title")
  6. .setContentText("This is content text")
  7. .setStyle(NotificationCompat.BigPictureStyle().bigPicture(
  8. BitmapFactory.decodeResource(resources, R.drawable.big_image)))
  9. .setSmallIcon(R.drawable.small_icon)
  10. .setLargeIcon(BitmapFactory.decodeResource(resources,
  11. R.drawable.large_icon))
  12. .setContentIntent(pi)
  13. .setAutoCancel(true)
  14. .build()
  15. manager.notify(1, notification)
  16. }

注意这里需要提前导入图片。

显示重要通知

通知渠道的重要等级越高,发出的通知就越容易获得用户的注意。比如高重要等级的通知渠道发出的通知可以弹出横幅、发出声音,而低重要等级的通知渠道发出的通知不仅可能会在某些情况下被隐藏,而且可能会被改变显示的顺序,将其排在更重要的通知之后。
修改一下MainActivity

需要添加和修改的部分已经给出注释了

  1. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  2. val channel = NotificationChannel("normal", "Normal",
  3. NotificationManager.IMPORTANCE_DEFAULT)
  4. // 添加一个新的渠道 并且设立更高的重要等级
  5. val channel2 = NotificationChannel("important", "Important",
  6. NotificationManager.IMPORTANCE_HIGH)
  7. manager.createNotificationChannel(channel2)
  8. manager.createNotificationChannel(channel)
  9. }
  10. val sendNotice: Button = findViewById(R.id.sendNotice)
  11. sendNotice.setOnClickListener {
  12. val intent = Intent(this, NotificationActivity::class.java)
  13. val pi = PendingIntent.getActivity(this, 0, intent, 0)
  14. // 这里启动重要等级的通知渠道
  15. val notification = NotificationCompat.Builder(this, "important")
  16. .setContentTitle("This is content title")
  17. .setContentText("This is content text")
  18. .setStyle(NotificationCompat.BigPictureStyle().bigPicture(
  19. BitmapFactory.decodeResource(resources, R.drawable.big_image)))
  20. .setSmallIcon(R.drawable.small_icon)
  21. .setLargeIcon(BitmapFactory.decodeResource(resources,
  22. R.drawable.large_icon))
  23. .setContentIntent(pi)
  24. .setAutoCancel(true)
  25. .build()
  26. manager.notify(1, notification)
  27. }
  1. 运行结果如下:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22022942/1651047453526-6b0d457d-fccd-4eed-a221-6a5805fc5cfe.png#clientId=u7d148cba-efce-4&from=paste&height=678&id=u1e731cb4&originHeight=847&originWidth=476&originalType=binary&ratio=1&rotation=0&showTitle=false&size=54026&status=done&style=none&taskId=u3eb3b8a0-1a39-4181-8526-1891e2f0aa5&title=&width=380.8)

9.3 调用摄像头和相册

调用摄像头

初始化创建

创建CameraAlbumTest项目

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="vertical"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent" >
  5. <Button
  6. android:id="@+id/takePhotoBtn"
  7. android:layout_width="match_parent"
  8. android:layout_height="wrap_content"
  9. android:text="Take Photo" />
  10. <ImageView
  11. android:id="@+id/imageView"
  12. android:layout_width="wrap_content"
  13. android:layout_height="wrap_content"
  14. android:layout_gravity="center_horizontal" />
  15. </LinearLayout>
  1. 修改`MainActivity`
  1. package com.example.cameraalbumtest
  2. import android.app.Activity
  3. import android.content.Intent
  4. import android.graphics.Bitmap
  5. import android.graphics.BitmapFactory
  6. import android.graphics.Matrix
  7. import android.media.ExifInterface
  8. import android.net.Uri
  9. import android.os.Build
  10. import androidx.appcompat.app.AppCompatActivity
  11. import android.os.Bundle
  12. import android.provider.MediaStore
  13. import android.widget.Button
  14. import android.widget.ImageView
  15. import androidx.core.content.FileProvider
  16. import java.io.File
  17. class MainActivity : AppCompatActivity() {
  18. val takePhoto = 1
  19. lateinit var imageUri: Uri
  20. lateinit var outputImage: File
  21. override fun onCreate(savedInstanceState: Bundle?) {
  22. super.onCreate(savedInstanceState)
  23. setContentView(R.layout.activity_main)
  24. val takePhotoBtn: Button = findViewById(R.id.takePhotoBtn)
  25. takePhotoBtn.setOnClickListener {
  26. // 创建File对象,用于存储拍照后的图片
  27. outputImage = File(externalCacheDir, "output_image.jpg")
  28. if (outputImage.exists()) {
  29. outputImage.delete()
  30. }
  31. outputImage.createNewFile()
  32. imageUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
  33. FileProvider.getUriForFile(this, "com.example.cameraalbumtest.fileprovider", outputImage)
  34. } else {
  35. Uri.fromFile(outputImage)
  36. }// 启动相机程序
  37. val intent = Intent("android.media.action.IMAGE_CAPTURE")
  38. intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
  39. startActivityForResult(intent, takePhoto)
  40. }
  41. }
  42. override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  43. super.onActivityResult(requestCode, resultCode, data)
  44. val imageView: ImageView = findViewById(R.id.imageView)
  45. when (requestCode) {
  46. takePhoto -> {
  47. if (resultCode == Activity.RESULT_OK) {// 将拍摄的照片显示出来
  48. val bitmap = BitmapFactory.decodeStream(contentResolver.
  49. openInputStream(imageUri))
  50. imageView.setImageBitmap(rotateIfRequired(bitmap))
  51. }
  52. }
  53. }
  54. }
  55. private fun rotateIfRequired(bitmap: Bitmap): Bitmap {
  56. val exif = ExifInterface(outputImage.path)
  57. val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
  58. ExifInterface.ORIENTATION_NORMAL)
  59. return when (orientation) {
  60. ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(bitmap, 90)
  61. ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(bitmap, 180)
  62. ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(bitmap, 270)
  63. else -> bitmap
  64. }
  65. }
  66. private fun rotateBitmap(bitmap: Bitmap, degree: Int): Bitmap {
  67. val matrix = Matrix()
  68. matrix.postRotate(degree.toFloat())
  69. val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height,
  70. matrix, true)
  71. bitmap.recycle() // 将不再需要的Bitmap对象回收
  72. return rotatedBitmap
  73. }
  74. }

点击左侧三角按钮展开

一点点来解析,首先是主要逻辑部分。

主要逻辑

  1. override fun onCreate(savedInstanceState: Bundle?) {
  2. super.onCreate(savedInstanceState)
  3. setContentView(R.layout.activity_main)
  4. take_photo.setOnClickListener {
  5. //创建File对象,用于存储拍照后的照片,存储位置是SD卡的应用关联缓存目录下,6.0后读写SD卡是危险权限,使用关联目录cache可以跳过这一步。Android10.0后使用作用域存储。
  6. outputimage = File(externalCacheDir, "output_image.jpg")
  7. //如果File已经存在,删掉,并调用createNewFile创建新文件
  8. if (outputimage.exists()) {
  9. outputimage.delete()
  10. }
  11. outputimage.createNewFile()
  12. // 把图片转化为uri
  13. imageuri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
  14. //如果系统版本大于7.0(Build.VERSION_CODES.N),本地真实路径uri不安全,会抛出异常。 FileProvider.getUriForFile可以将File对象转换为一个封装后的uri对象。
  15. //getUriForFile接收三个参数,一个是context,另一个是任意字符串,第三个是File对象。FileProvider使用类似ContentProvider的机制进行保护,提高程序安全性。
  16. FileProvider.getUriForFile(this, "com.example.camera.fileprovider", outputimage)
  17. } else {
  18. //如果系统版本低于7.0,调用Uri.fromFile将File对象转换为Uri对象,这个对象是本地真实路径
  19. Uri.fromFile(outputimage)
  20. }
  21. //Intent的action进行指定,Intent的putExtra指定图片的输出地址,刚刚得到uri对象
  22. val intent = Intent("android.media.action.IMAGE_CAPTURE")
  23. intent.putExtra(MediaStore.EXTRA_OUTPUT, imageuri)
  24. //启动Activity,隐式的。调用之后返回到onActivityResult方法。
  25. startActivityForResult(intent, takePhoto)
  26. }
  27. }
  28. override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  29. super.onActivityResult(requestCode, resultCode, data)
  30. when (requestCode) {
  31. // 这里takePhoto = 1,也就是requestCode = 1的时候就显示照片
  32. takePhoto -> {
  33. //将拍摄的照片显示出来,拍照成功的话使用BitmapFactory.decodeStream将图片解析为bitmap对象
  34. val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(imageuri))
  35. //最后设置到ImageView当中,再加上照片旋转的处理。
  36. image_view.setImageBitmap(rotateIfRequired(bitmap))
  37. }
  38. }
  39. }

旋转图片

调用照相机程序去拍照有可能会在一些手机上发生照片旋转的情况。这是因为这些手机认为打开摄像头进行拍摄时手机就应该是横屏的,因此回到竖屏的情况下就会发生90度的旋转。为此,这里我们又加上了判断图片方向的代码:

  1. private fun rotateIfRequired(bitmap: Bitmap): Bitmap {
  2. val exif = ExifInterface(outputImage.path)
  3. val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
  4. ExifInterface.ORIENTATION_NORMAL)
  5. return when (orientation) {
  6. ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(bitmap, 90)
  7. ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(bitmap, 180)
  8. ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(bitmap, 270)
  9. else -> bitmap
  10. }
  11. }
  12. private fun rotateBitmap(bitmap: Bitmap, degree: Int): Bitmap {
  13. val matrix = Matrix()
  14. matrix.postRotate(degree.toFloat())
  15. val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height,
  16. matrix, true)
  17. bitmap.recycle() // 将不再需要的Bitmap对象回收
  18. return rotatedBitmap
  19. }
  1. 这里就不细讲了,`rotateBitmap`方法用于旋转图片,`bitmap`对象就是图片,`degree`是需要旋转的度数;`rotateIfRequired`用于判断图片需要旋转多少度,把旋转过的图片旋转回来,返回的是一个图片对象`bitmap`

注册ContentProvider

上文中还提到了ContentProvider的保护机制,所以还需要在AndroidManifest里面注册:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
  3. package="com.example.cameraalbumtest">
  4. <application
  5. android:allowBackup="true"
  6. android:icon="@mipmap/ic_launcher"
  7. android:label="@string/app_name"
  8. android:roundIcon="@mipmap/ic_launcher_round"
  9. android:supportsRtl="true"
  10. android:theme="@style/Theme.CameraAlbumTest">
  11. <activity
  12. android:name=".MainActivity"
  13. android:exported="true">
  14. <intent-filter>
  15. <action android:name="android.intent.action.MAIN" />
  16. <category android:name="android.intent.category.LAUNCHER" />
  17. </intent-filter>
  18. </activity>
  19. <!--注册ContentProvider-->
  20. <provider
  21. android:name="androidx.core.content.FileProvider"
  22. android:authorities="com.example.cameraalbumtest.fileprovider"
  23. android:exported="false"
  24. android:grantUriPermissions="true">
  25. <meta-data
  26. android:name="android.support.FILE_PROVIDER_PATHS"
  27. android:resource="@xml/file_paths" />
  28. </provider>
  29. </application>
  30. </manifest>
  1. `android:name`属性的值是**固定的**,而`android:authorities`属性的值必须和刚才`FileProvider.getUriForFile() ` **第二个参数一致**。 另外,这里还在<provider>标签的内部使用<meta-data>指定Uri的共享路径,并引用了一个`@xml/file_paths`资源。<br />这个资源现在还是不存在的,接下来要创建它。右击`res目录→New→Directory`,创建一个`xml`目录,接着右击`xml目录→New→File`,创建一个`file_paths.xml`文件。然后修改`file_paths.xml`文件中的内容,如下所示:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <paths xmlns:android="http://schemas.android.com/apk/res/android">
  3. <external-path name="my_images" path="/" />
  4. </paths>

image.png

拍摄图像的位置

image.png

相册中选择图片

添加按钮

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:orientation="vertical"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent" >
  5. <Button
  6. android:id="@+id/takePhotoBtn"
  7. android:layout_width="match_parent"
  8. android:layout_height="wrap_content"
  9. android:text="Take Photo" />
  10. <!--添加图片按钮-->
  11. <Button
  12. android:id="@+id/fromAlbumBtn"
  13. android:layout_width="match_parent"
  14. android:layout_height="wrap_content"
  15. android:text="From Album" />
  16. <ImageView
  17. android:id="@+id/imageView"
  18. android:layout_width="wrap_content"
  19. android:layout_height="wrap_content"
  20. android:layout_gravity="center_horizontal" />
  21. </LinearLayout>

添加主要逻辑

  1. package com.example.cameraalbumtest
  2. import android.app.Activity
  3. import android.content.Intent
  4. import android.graphics.Bitmap
  5. import android.graphics.BitmapFactory
  6. import android.graphics.Matrix
  7. import android.media.ExifInterface
  8. import android.net.Uri
  9. import android.os.Build
  10. import androidx.appcompat.app.AppCompatActivity
  11. import android.os.Bundle
  12. import android.provider.MediaStore
  13. import android.widget.Button
  14. import android.widget.ImageView
  15. import androidx.core.content.FileProvider
  16. import java.io.File
  17. class MainActivity : AppCompatActivity() {
  18. val takePhoto = 1
  19. // 新增选择图片求情码
  20. // -----开始-----
  21. val fromAlbum = 2
  22. // -----结束------
  23. lateinit var imageUri: Uri
  24. lateinit var outputImage: File
  25. override fun onCreate(savedInstanceState: Bundle?) {
  26. super.onCreate(savedInstanceState)
  27. setContentView(R.layout.activity_main)
  28. val takePhotoBtn: Button = findViewById(R.id.takePhotoBtn)
  29. takePhotoBtn.setOnClickListener {
  30. // 创建File对象,用于存储拍照后的图片
  31. outputImage = File(externalCacheDir, "output_image.jpg")
  32. if (outputImage.exists()) {
  33. outputImage.delete()
  34. }
  35. outputImage.createNewFile()
  36. imageUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
  37. FileProvider.getUriForFile(this, "com.example.cameraalbumtest.fileprovider", outputImage)
  38. } else {
  39. Uri.fromFile(outputImage)
  40. }// 启动相机程序
  41. val intent = Intent("android.media.action.IMAGE_CAPTURE")
  42. intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
  43. startActivityForResult(intent, takePhoto)
  44. }
  45. // 新增打卡相册选择图片事件
  46. // --------------开始-------------
  47. val fromAlbumBtn:Button = findViewById(R.id.fromAlbumBtn)
  48. fromAlbumBtn.setOnClickListener {
  49. // 打开文件选择器
  50. val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
  51. intent.addCategory(Intent.CATEGORY_OPENABLE)
  52. // 指定只显示图片
  53. intent.type = "image/*"
  54. startActivityForResult(intent, fromAlbum)
  55. }
  56. // -------------结束------------------
  57. }
  58. override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  59. super.onActivityResult(requestCode, resultCode, data)
  60. val imageView: ImageView = findViewById(R.id.imageView)
  61. when (requestCode) {
  62. takePhoto -> {
  63. if (resultCode == Activity.RESULT_OK) {// 将拍摄的照片显示出来
  64. val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri))
  65. imageView.setImageBitmap(rotateIfRequired(bitmap))
  66. }
  67. }
  68. // 新增显示图片
  69. // -----------开始------------
  70. fromAlbum -> {
  71. if (resultCode == Activity.RESULT_OK && data != null) {
  72. data.data?.let { uri ->
  73. // 将选择的图片显示
  74. val bitmap = getBitmapFromUri(uri)
  75. imageView.setImageBitmap(bitmap)
  76. }
  77. }
  78. }
  79. // -----------结束------------
  80. }
  81. }
  82. // 新增图片转化uri方法
  83. // ---------------开始------------------
  84. private fun getBitmapFromUri(uri: Uri) = contentResolver.openFileDescriptor(uri, "r")?.use {
  85. BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
  86. }// --------------结束------------------
  87. private fun rotateIfRequired(bitmap: Bitmap): Bitmap {
  88. val exif = ExifInterface(outputImage.path)
  89. val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
  90. ExifInterface.ORIENTATION_NORMAL)
  91. return when (orientation) {
  92. ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(bitmap, 90)
  93. ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(bitmap, 180)
  94. ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(bitmap, 270)
  95. else -> bitmap
  96. }
  97. }
  98. private fun rotateBitmap(bitmap: Bitmap, degree: Int): Bitmap {
  99. val matrix = Matrix()
  100. matrix.postRotate(degree.toFloat())
  101. val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height,
  102. matrix, true)
  103. bitmap.recycle() // 将不再需要的Bitmap对象回收
  104. return rotatedBitmap
  105. }
  106. }