Kotlin 拍照狈定、獲取相冊圖片

安卓7.0以后苫拍,google修改了文件權(quán)限睦袖,不再允許app透露file://Uri來給其他app珊肃,轉(zhuǎn)而使用FileProvider通過content://Uri來取代file://Uri

1.使用FileProvider必須在manifest文件中注冊provider:

<application>
... ...
<provider
        android:authorities="${applicationId}.provider"
        android:name="android.support.v4.content.FileProvider"
        android:exported="false"
        android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepath"/>
    </provider>
</application>
  • android:authorities : FileProvider唯一標識
  • android:exported : 必須設(shè)置為false FileProvider不能公開
  • android:grantUriPermissions : 控制文件權(quán)限
  • android:resource : xml路徑設(shè)置的filepath.xml

2.res/xml/新增 filepath.xml

<?xml version="1.0" encoding="utf-8"?>
<paths>
  <!--拍照存儲路徑-->
  <external-path
    name="pocket"
    path="pocket/picture/" />
  <!--訪問相冊路徑-->
  <external-path
    name="external"
    path="." />
</paths>
  • path: 子目錄名稱
  • name: 取代path的別名

即原來路徑名為file://xxxx/pocket/picture/x.jpg -> content://xxxx/pocket/picture/x.jpg

3.由于項目結(jié)果使用的是單activity多fragment,項目中的每個fragment都間接繼承了PermissionCheckerDelegate,因此伦乔,項目所有的權(quán)限都放在此類中進行申請(先貼代碼厉亏,后續(xù)再講適配中出現(xiàn)的一些Bug解決方案):

abstract class PermissionCheckerDelegate : BaseDelegate() {

  //給子類用于顯示的相片地址
  private lateinit var photoUri: Uri
  private lateinit var imagePath: String
  //供裁剪使用
  private lateinit var oriUri: Uri
  private val cropFile = File(Environment.getExternalStorageDirectory().absolutePath,
        "/pocket/picture/" + "crop_photo.jpg")

  companion object {
    const val WRITE_EXTERNAL_STORAGE = 1
    const val OPEN_CAMERA = 2
    const val OPEN_ALBUM = 3
    const val CROP_IMAGE = 4
  }

  /**
   * 相機讀寫權(quán)限申請
   */
  fun applyCameraPermission() {
    applyWritePermission(OPEN_CAMERA) {
        openCamera()
    }
  }

  /**
   * 相冊讀寫權(quán)限申請
   */
  fun applyOpenAlbumPermission() {
    applyWritePermission(OPEN_ALBUM) {
        openAlbum()
    }
  }

  /**
   * 獲取相機拍下的uri并轉(zhuǎn)為bitmap
   */
  fun getBitmapByCamera() = BitmapFactory
        .decodeStream(context!!.contentResolver.openInputStream(photoUri))!!

  /**
   * 獲取相冊的圖片轉(zhuǎn)為bitmap
   */
  fun getBitmapByAlbum(data: Intent): Bitmap {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        handleImageAfterKitKat(data)
    } else {
        handleImageBeforeKitKat(data)
    }
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        //7.0適配
        oriUri = FileProvider.getUriForFile(context!!, "com.dididi.pocket.provider", File(imagePath))
    }
    return MediaStore.Images.Media.getBitmap(context!!.contentResolver, oriUri)
  }

  /**
   * 打開相機
   */
  private fun openCamera() {
    //創(chuàng)建file于sdcard/pocketPicture/ 以當前時間命名的jpg圖像
    File(Environment.getExternalStorageDirectory().absolutePath,
            "/pocket/picture/" + System.currentTimeMillis() + ".jpg").apply {
        parentFile.mkdirs()
        photoUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            //android7.0之后,不再允許app透露file://Uri給其他app
            //轉(zhuǎn)而使用FileProvider來生成content://Uri取代file://Uri
            FileProvider
                    .getUriForFile(context!!, "com.dididi.pocket.provider", this)
        } else {
            //7.0之前 直接獲取Uri
            Uri.fromFile(this)
        }
    }
    Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
        //將uri存進intent烈和,供相機回調(diào)使用 data.getData中獲取
        putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
        startActivityForResult(this, OPEN_CAMERA)
    }
  }

  /**
   * 打開相冊
   */
  private fun openAlbum() {
    Intent(Intent.ACTION_GET_CONTENT).apply {
        type = "image/*"
        startActivityForResult(this, OPEN_ALBUM)
    }
  }

  /**
   * 裁剪Uri
   * @param oriUri 原始Uri
   * @param desUri 目標Uri
   */
  fun cropImageUri(oriUri: Uri, desUri: Uri, aspectX: Int, aspectY: Int, width: Int, height: Int) {
    Intent("com.android.camera.action.CROP").apply {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        }
        setDataAndType(oriUri, "image/*")
        putExtra("crop", "true")
        putExtra("aspectX", aspectX)
        putExtra("aspectY", aspectY)
        putExtra("outputX", width)
        putExtra("outputY", height)
        putExtra("scale", true)
        //將剪切的圖片保存到目標Uri中
        putExtra(MediaStore.EXTRA_OUTPUT, desUri)
        putExtra("return-data", false)
        putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString())
        putExtra("noFaceDetection", true)
        this@PermissionCheckerDelegate.startActivityForResult(this, CROP_IMAGE)
    }
  }

  override fun onRequestPermissionsResult(requestCode: Int,
                                        permissions: Array<out String>,
                                        grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    //權(quán)限請求結(jié)果
    when (requestCode) {
        WRITE_EXTERNAL_STORAGE -> {
            permissionHint(grantResults, "沒有讀寫權(quán)限") {}
        }
        OPEN_CAMERA -> {
            permissionHint(grantResults, "沒有讀寫權(quán)限") {
                openCamera()
            }
        }
        OPEN_ALBUM -> {
            permissionHint(grantResults, "沒有讀寫權(quán)限") {
                openAlbum()
            }
        }
        else -> {
            Toast.makeText(context, "沒有權(quán)限", Toast.LENGTH_SHORT).show()
        }
    }
  }

  /**
   * 權(quán)限結(jié)果處理lambda函數(shù)
   * @param grantResults 請求結(jié)果
   * @param msg toast內(nèi)容
   * @param target 權(quán)限拿到要做什么
   */
  private fun permissionHint(grantResults: IntArray, msg: String, target: () -> Unit) {
    if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        target()
    } else {
        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
    }
  }

  /**
   * 請求讀寫權(quán)限
   * @param requestCode 請求碼
   * @param target 要做什么
   */
  private fun applyWritePermission(requestCode: Int, target: () -> Unit) {
    val permissions = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
    //android6.0之后爱只,需要動態(tài)申請讀寫權(quán)限
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        //讀寫是否已經(jīng)授權(quán)
        val check = ContextCompat.checkSelfPermission(context!!, permissions[0])
        if (check == PackageManager.PERMISSION_GRANTED) {
            target()
        } else {
            //如果未發(fā)現(xiàn)授權(quán),則請求權(quán)限
            requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                    requestCode)
        }
    } else {
        target()
    }
  }

  /**
   * android4.4之后招刹,需要解析獲取圖片真實路徑
   */
  @TargetApi(Build.VERSION_CODES.KITKAT)
  private fun handleImageAfterKitKat(data: Intent) {
    val uri = data.data
    //document類型的Uri
    when {
        DocumentsContract.isDocumentUri(context, uri) -> {
            //通過documentId處理
            val docId = DocumentsContract.getDocumentId(uri)
            when (uri?.authority) {
                "com.android.externalstorage.documents" -> {
                    val type = docId.split(":")[0]
                    if ("primary".equals(type, ignoreCase = true)) {
                        imagePath = Environment.getExternalStorageDirectory()
                                .toString() + "/" + docId.split(":")[1]
                    }
                }
                //media類型解析
                "com.android.providers.media.documents" -> {
                    val id = docId.split(":")[1]
                    val type = docId.split(":")[0]
                    val contentUri: Uri? = when (type) {
                        "image" -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
                        "video" -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
                        "audio" -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
                        else -> null
                    }
                    val selection = "_id=?"
                    val selectionArgs: Array<String> = arrayOf(id)
                    imagePath = getImagePath(contentUri!!, selection, selectionArgs)!!
                }
                //downloads文件解析
                "com.android.providers.downloads.documents" -> {
                    ContentUris.withAppendedId(
                            Uri.parse("content://downloads/public_downloads"), docId.toLong()
                    ).apply {
                        imagePath = getImagePath(this, null, null)!!
                    }
                }
                else -> {
                }
            }
        }
        "content".equals(uri?.scheme, ignoreCase = true) ->
            //content類型數(shù)據(jù)不需要解析恬试,直接傳入生成即可
            imagePath = getImagePath(uri!!, null, null)!!
        "file".equals(uri?.scheme, ignoreCase = true) ->
            //file類型的uri直接獲取圖片路徑即可
            imagePath = uri!!.path!!
    }
  }

  /**
   * android4.4之前可直接獲取圖片真實uri
   */
  private fun handleImageBeforeKitKat(data: Intent) {
    val uri = data.data
    imagePath = getImagePath(uri!!, null, null)!!
  }

  /**
   * 解析uri及selection
   * 獲取圖片真實路徑
   */
  private fun getImagePath(uri: Uri, selection: String?, selectionArgs: Array<String>?): String? {
    var cursor: Cursor? = null
    try {
        cursor = context!!.contentResolver.query(uri, null, selection, selectionArgs, null)
        if (cursor?.moveToFirst()!!) {
            return cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA))
        }
    } finally {
        cursor?.close()
    }
    return null
  }
}
  1. 子類ChatDelegate間接繼承PermissionCheckerDelegate,因此疯暑,子類只需要復(fù)寫onActivityResult()方法即可训柴,具體代碼省略如下:

     ... ...
     //上拉頁面的按鈕
     //打開相機
     val moreCamera = morePagerView?.findViewById<MoreButtonItem>(R.id.item_msg_chat_more_camera)
     moreCamera?.setOnClickListener {
         applyCameraPermission()
     }
     //打開相冊
     val moreOpenAlbum = morePagerView?.findViewById<MoreButtonItem>(R.id.item_msg_chat_more_album)
     moreOpenAlbum?.setOnClickListener {
         applyOpenAlbumPermission()
     }
     ... ...
     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
       super.onActivityResult(requestCode, resultCode, data)
       when (requestCode) {
         OPEN_CAMERA -> {
             if (resultCode == ISupportFragment.RESULT_OK) {
                 insertBitmapToList(getBitmapByCamera())
             }
         }
         OPEN_ALBUM -> {
             if (resultCode == ISupportFragment.RESULT_OK) {
                 insertBitmapToList(getBitmapByAlbum(data!!))
             }
         }
       }
     }
    

碰到了以下幾個Error:

  1. java.lang.IllegalStateException: Couldn't read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it.
    注意檢查android4.4之后Uri的解析是否正確是否有遺漏項沒解析,參考如handleImageAfterKitKat()方法妇拯。
    2.java.io.FileNotFoundException: No content provider: /storage/emulated/0/.../xxx.jpg幻馁,這個問題出現(xiàn)在訪問相冊獲取相冊照片時,android7.0以上設(shè)備需要通過FileProvider來獲得訪問權(quán)限越锈,檢查一下filepath.xml文件仗嗦,如果是使用getExternalStorageDirectory()需要加上:

     <!--訪問相冊路徑-->
     <external-path
       name="external"
       path="." />
    

然后拿到處理后的imagePath之后,需要通過FileProvider來獲取圖片Uri甘凭,具體如方法getBitmapByAlbum():

/**
 * 獲取相冊的圖片轉(zhuǎn)為bitmap
 */
fun getBitmapByAlbum(data: Intent): Bitmap {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        handleImageAfterKitKat(data)
    } else {
        handleImageBeforeKitKat(data)
    }
    oriUri = Uri.parse(imagePath)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        //7.0適配(此處的authority為${applicationId}.provider)
        oriUri = FileProvider.getUriForFile(context!!, "com.dididi.pocket.provider", File(imagePath))
    }
    return MediaStore.Images.Media.getBitmap(context!!.contentResolver, oriUri)
}

至此稀拐。放張效果圖吧。丹弱。钩蚊。

sendPhoto.gif

PermissionCheckerDelegate.kt

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蹈矮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鸣驱,老刑警劉巖泛鸟,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異踊东,居然都是意外死亡北滥,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門闸翅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來再芋,“玉大人,你說我怎么就攤上這事坚冀〖檬辏” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長司训。 經(jīng)常有香客問我构捡,道長,這世上最難降的妖魔是什么壳猜? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任勾徽,我火速辦了婚禮,結(jié)果婚禮上统扳,老公的妹妹穿的比我還像新娘喘帚。我一直安慰自己,他們只是感情好咒钟,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布吹由。 她就那樣靜靜地躺著,像睡著了一般盯腌。 火紅的嫁衣襯著肌膚如雪溉知。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天腕够,我揣著相機與錄音级乍,去河邊找鬼。 笑死帚湘,一個胖子當著我的面吹牛玫荣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播大诸,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼捅厂,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了资柔?” 一聲冷哼從身側(cè)響起焙贷,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎贿堰,沒想到半個月后辙芍,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡羹与,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年故硅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纵搁。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡吃衅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出腾誉,到底是詐尸還是另有隱情徘层,我是刑警寧澤峻呕,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站惑灵,受9級特大地震影響山上,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜英支,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一佩憾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧干花,春花似錦妄帘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至肿仑,卻和暖如春致盟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背尤慰。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工馏锡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人伟端。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓杯道,卻偏偏與公主長得像,于是被迫代替她去往敵國和親责蝠。 傳聞我的和親對象是個殘疾皇子党巾,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • Android N系列適配---FileProvider Android 7.0的適配,主要包含方面: Andro...
    25a58172fbb5閱讀 7,092評論 3 32
  • 只簡述我發(fā)現(xiàn)問題的根源霜医,有些是適配了7.0齿拂,會報權(quán)限失敗問題,那是由于沒有動態(tài)授權(quán)導(dǎo)致肴敛,接下來我一步一步給大家實現(xiàn)...
    Wocus閱讀 2,358評論 4 5
  • Android7.0發(fā)布已經(jīng)有一個多月了署海,Android7.0在給用戶帶來一些新的特性的同時,也給開發(fā)者帶來了新的...
    東經(jīng)315度閱讀 1,360評論 0 14
  • 前言 在Android7.0系統(tǒng)上值朋,android框架強制執(zhí)行了 StrictMode API 政策禁止向你的應(yīng)用...
    Blizzard_liu閱讀 1,193評論 1 4
  • 如今的你. 過去,是我的一千個為什么 還有你笑著的回答 現(xiàn)在巩搏,是你的一通視頻電話 還有我煩躁的回話 過去昨登,是我望著...
    說書客閱讀 514評論 0 0