iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 3
0
Software Development

英國研究顯示,連續30天用Kotlin開發Android將有益於身心健康系列 第 3

Android Kotlin 實作 Day 3 : Image picker ( 使用 Permission+ FileProvider + Intent)

使用語言

  • Kotlin

使用元件

  • ImageView
  • Button

xml資源檔配置

  • 自定義 Button 樣式
<shape xmlns:android="http://schemas.android.com/apk/res/android"

    //設置shape為長方形
    android:shape="rectangle">
    
    //設置圓角半徑
    <corners android:radius="20dp" />
    
    //設置文字與Button邊界距離
    <padding
        android:bottom="10dp"
        android:left="10dp"
        android:right="10dp"
        android:top="10dp" />
        
    //設置Button顏色
    <solid android:color="#48CFAD" />
</shape>

Method


Permission

取得相機、讀取/寫入外部儲存裝置的權限

  1. 在 AndroidManifest.xml 中加入下列 uses-permission
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  1. 官方文件 可以找到這三個權限的 Protection level 皆為:

    Protection level: dangerous

    因此必須在 App 開啟時向使用者 請求權限

    fun permission(){
        ActivityCompat.requestPermissions(this, arrayOf(
        android.Manifest.permission.CAMERA,
        android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
        android.Manifest.permission.READ_EXTERNAL_STORAGE), 0)
    }
    
  2. 可以用下列方法檢查使用者目前為拒絕還是允許該權限

    checkSelfPermission (context: Context, permission: String)

    ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA)
    

    此方法會得到兩種可能結果

    • PackageManager.PERMISSION_GRANTED 允許

    • PackageManager.PERMISSION_DENIED 拒絕


FileProvider

API 24 版本以上,Android 不再允許在 app 中透漏 file://Uri 給其他 app ( 官方文件 )

因此 google 提供了 FileProvider 來生成 content://Uri 取代 file://Uri

FileProvider 將隱藏真實的共享檔案路徑,並將路徑轉換為 content:// Uri

  • 在 AndroidManifest.xml 中加上 provider

    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.example.aria.day3_image_pickerintentimageview.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/provider_paths" />
    </provider>
    
    • android:authorities:

      用來標識 provider 的唯一標誌,同一台手機中一個 authority 名稱只能被一個 app 使用

    • android:exported:必須設為 false,因為 FileProvider 不應被設置為公開

    • android:grantUriPermissions:控制共享文件的訪問權限

    • android:resource:於 xml 路徑下設置的 provider_paths.xml

  • 在 res/xml/ 下新增 provider_paths.xml

    <?xml version="1.0" encoding="utf-8"?>
    <paths xmlns:android="http://schemas.android.com/apk/res/android">
         <external-path name="external_files" path="."/>
    </paths>
    

    此檔案為 file://Uri 轉換為 content://Uri 的規則

    • 寫法: <files-path path="images/" name="my_images" />

    • files-path:

      共享文件路徑的子元素,一份 provider_paths.xml 需含一至多個下列元素

      文件路徑下的子元素 對應路徑
      files-path 內部儲存空間應用私有目錄下的 files/ 目錄,等同 Context.getFilesDir() 獲取的路徑
      cache-path 內部儲存空間應用私有目錄下的 cache/ 目錄,等同 Context.getCacheDir() 獲取的路徑
      external-path 外部儲存空間的根目錄,等同 Environment.getExternalStorageDirectory() 獲取的路徑
      external-files-path 外部儲存空間應用私有目錄下的 files/ 目錄,等同 Context.getExternalFilesDir(null) 獲取的路徑
      external-cache-path 外部儲存空間應用私有目錄下的 cache/ 目錄,等同 Context.getExternalCacheDir() 獲取的路徑
      • path:指定當前子元素目錄下需要共享的子目錄名稱

      • name:用來取代上述 path 指定的目錄名稱的別名

      • 上述寫法表示:
        將 Context.getFilesDir() 獲取的路徑下的目錄 images/ 取代為別名 my_images

    • 轉換示意圖(圖片來源:參考資料 1

  • 取得 content://Uri

    1. 定義 File

      File( pathname : String )

      File( parent-pathname : String, child-pathname : String)

    2. 取得 uri

      FileProvider.getUriForFile(context, authorities, file)

      #此 authorities 和 AndroidManifest.xml 的 provider 中設定的 authorities 相同即可

  • 以上圖片來源及參考資料:參考資料 1參考資料 2


componion object

設定 componion object 參數作為 intent 回傳的 requestCode,供 onActivityResult 判斷

private companion object {
    val PHOTO_FROM_GALLERY = 1
    val PHOTO_FROM_CAMERA = 2
}

Intent

  • intent to Album

    要從其他應用程式擷取檔案有兩種方式:

    • Intent.ACTION_GET_CONTENT

      intent 必須 setType ,透過指定的 MIME Type 取得相對應類型的檔案

      val intent = Intent(Intent.ACTION_GET_CONTENT)
      intent.setType("image/*")
      
    • Intent.ACTION_PICK

      intent 不一定要 setType,但須設置 Uri 並透過指定的 Uri 來取得該目錄下的檔案

      val uri = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_Uri
      val intent = Intent(Intent.ACTION_PICK, uri)
      

    設定完 intent 後 startActivityForResult

    startActivityForResult ( intent : Intent , requestCode : Int)

    • requestCode:設置供後續回傳 result 時判斷用
    startActivityForResult(intent, PHOTO_FROM_GALLERY)
    
  • intent to Camera

    • MediaStore . ACTION_IMAGE_CAPTURE

      val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
      
    • 設置並提供 Camera 存放照片的 Uri

      lateinit var saveUri: Uri  //外部定義變數
      
      val tmpFile = File(Environment.getExternalStorageDirectory().toString(), System.currentTimeMillis().toString() + ".jpg")
      val uriForCamera = FileProvider.getUriForFile(this, "com.example.aria.day3_image_pickerintentimageview.fileprovider", tmpFile)
      saveUri = uriForCamera  //將 Uri 存進變數供後續 onActivityResult 使用
      intent.putExtra(MediaStore.EXTRA_OUTPUT, uriForCamera)
      
      • MediaStore.EXTRA_OUTPUT

        透過 intent.putExtra 的方式,通知相機新照片的儲存位置(Uri)

        KEY 為官方設定好的 MediaStore.EXTRA_OUTPUT

        #若 intent 至相機時""提供 Uri:
         照片會以 intent.putextra("data", bitmap) 的方式回傳至 result

        #若 intent 至相機時""提供 Uri:
         則 result 回傳的 data 會為 null , 故需自行另存 Uri 以供後續讀圖使用

    設定完 intent 後 startActivityForResult

    startActivityForResult(intent, PHOTO_FROM_CAMERA)
    

onActivityResult  

  • onActivityResult( requestCode: Int, resultCode: Int, data: Intent? )

    取得 Intent 回來的結果並處理事件

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            PHOTO_FROM_GALLERY -> {
                when (resultCode) {
                    Activity.RESULT_OK -> { }
                    Activity.RESULT_CANCELED -> { }
                }
            }
    
            PHOTO_FROM_CAMERA -> {
                when (resultCode) {
                    Activity.RESULT_OK -> { }
                    Activity.RESULT_CANCELED -> { }
                }
            }
        }
    }
    
    • requestCode:前面設置用來辨別是哪個 intent 回傳的結果

    • resultCode:確認 intent 成功與否

    • data :intent 帶回的資料

  • 相簿回傳處理

    val uri = data!!.data                       
    imageView.setImageURI(uri)
    

    #相簿會以 Intent( action: String, uri: Uri) 的方式回傳 Uri,
     須用 data.data 的方式取得 Uri

    #data.data = data.getData()

  • 相機回傳處理

    為避免產生 OOM 問題,使用 Glide 套件載入圖片至 imageView

    欲使用 Glide 套件需先在 Gradle 中加入

    repositories {
        mavenCentral()
        google()
    }
    
    dependencies {
        implementation 'com.github.bumptech.glide:glide:4.8.0'
        annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0'
    }
    

    透過 Glide 將前面存好的 Uri 載入至 imageView

    Glide.with(this).load(saveUri).into(imageView)
    

    #若前面無提供 Uri 給相機,
     會以 intent.putextra("data", bitmap) 的方式回傳 bitmap
     須用 data.extra.get("data") 方式取得

實作成果

查看詳細 Code > GitHub

tags: Album Camera Permission FileProvider onActivityResult Uri Glide

上一篇
Android Kotlin 實作 Day 2 : Discount ( 使用 Seekbar + Keyboard )
下一篇
Android Kotlin 實作 Day 4 : Scalable_ImageView( 使用MotionEvent + 座標系統 )
系列文
英國研究顯示,連續30天用Kotlin開發Android將有益於身心健康30

尚未有邦友留言

立即登入留言