《Android 編程權威指南》學習筆記 : 第16章 使用 intent 拍照

第16章 使用 intent 拍照

方案分析

文件存儲

相機拍攝的照片動輒幾MB大小,保存在SQLite數據庫中肯定不現實。顯然,它們需要在設備文件系統的某個地方保存。
設備上就有這麼一個地方:私有存儲空間,像照片這樣的文件也可以這麼保存。
因爲要處理的照片都是私有文件,只有你自己的應用能讀寫,
如果其他應用要讀寫你的文件,雖然Context.MODE_WORLD_READABLE可以傳入openFileOutput(String, Int)函數,但這個flag已經廢棄了。即使強制使用,在新系統設備上也不是很可靠。以前,還可以通過公共外部存儲轉存,但出於安全考慮,這條路在新版本系統上也被堵住了。

ContentProvider

如果想共享文件給其他應用,或是接收其他應用的文件(比如相機應用拍攝的照片),可以通過ContentProvider把要共享的文件暴露出來。
ContentProvider允許你暴露內容URI給其他應用。這樣,這些應用就可以從內容URI下載或向其中寫入文件。當然,主動權在你手上,你可以控制讀或寫。
如果只想從其他應用接收一個文件,自己實現ContentProvider簡直是費力不討好的事。
Google早已想到這點,因此提供了一個名爲FileProvider的便利類。這個類能幫你搞定一切,而你只要做做參數配置就行了。

使用 FileProvider

聲明FileProvider爲ContentProvider,並給予一個指定的權限。在AndroidManifest.xml中添加一個FileProvider聲明

代碼清單: 添加FileProvider聲明(manifests/AndroidManifest.xml)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.criminalintent">

    <application .../>
        <activity
          .../>
          ...
        </activity>
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.example.criminalintent.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/files"/>
        </provider>
    </application>

</manifest>

其中:

  • com.example.criminalintent.fileprovider:是授權字符串,後面會用到。
  • exported="false": 處了自己和授權的人,其它任何人不允許使用你的 FileProvider
  • android:grantUriPermissions="true":授權其它應用,允許它們向你指定位置的URI寫入文件
  • resource="@xml/files":指定要暴露的文件:執行一個xml資源文件。

右鍵點擊 app/res 目錄,New -> Android resource file, 資源類型選擇xml,文件名輸入 files,確定創建文件,替換爲如下內容
代碼清單: res/xm/files.xml

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path name="crime_photos" path="."/>
</paths>

照片存放位置

代碼清單:Crime.kt

@Entity
data class  Crime(...) {
    val photoFileName get() = "IMG_$id.jpg"
}

代碼清單:CrimeRepository.kt

class CrimeRepository private constructor(context: Context) {
     ...
     private var filesDir = context.applicationContext.filesDir
     fun getPhotoFile(crime: Crime):File = File(filesDir, crime.photoFileName)
     ...
}
  • filesDir:返回的目錄:/data/user/0/com.example.criminalintent/files
    在 Device File Exploer 的位置見下圖:

直接在真機的文件管理中是無法查看的,真機的操作系統已經將其隱藏,只能在 Android Studio 的 Device File Exploer 查看。

  • getPhotoFile返回圖片地址,比如:/data/user/0/com.example.criminalintent/files/IMG_454f516f-4812-42dc-a06a-479ceb5c359b.jpg

代碼清單:CrimeRepository.kt

class CrimeDetailViewModel : ViewModel() {
    ...
    fun getPhotoFile(crime: Crime): File {
        return crimeRepository.getPhotoFile(crime)
    }
}

使用相機 intent

佈局文件,添加 拍照按鈕的ImageButton 和顯示照片的ImageView
代碼清單:res/layout/fragment_crime.xml

 <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/crime_photo"
                android:layout_width="80dp"
                android:layout_height="80dp"
                android:scaleType="centerInside"
                android:cropToPadding="true"
                android:background="@android:color/darker_gray"/>

            <ImageButton
                android:id="@+id/crime_camera"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:src="@android:drawable/ic_menu_camera"/>

        </LinearLayout>

        <LinearLayout 
        ...
        </LinearLayout>
        ...
    </LinearLayout>

代碼清單:CrimeFragment.kt

class CrimeFragment : Fragment()
    ...
    private lateinit var photoFile: File
    private lateinit var photoUri: Uri

    private lateinit var photoButton: ImageButton
    private lateinit var photoView: ImageView

    // 啓動照相機 activity 的intent
    private val captureImageIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
    // 啓動照相機 activity 的啓動器
    private lateinit var captureActivityResultLauncher: ActivityResultLauncher<Intent>
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        // 照相機 Activity啓動器
        captureActivityResultLauncher =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
                if (result.resultCode == Activity.RESULT_OK
                    && result.data != null) {
                    updatePhotoView()
                    //撤銷權限
                    requireActivity().revokeUriPermission(photoUri,
                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
                }
            }
   }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        ...
        photoButton = view.findViewById(R.id.crime_camera) as ImageButton
        photoView = view.findViewById(R.id.crime_photo) as ImageView

        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        crimeDetailViewModel.crimeLiveData.observe(
            viewLifecycleOwner,
            Observer { crime ->
                crime?.let {
                    this.crime = crime

                    // 照片文件:路徑 + 文件名
                    photoFile = crimeDetailViewModel.getPhotoFile(crime)
                    Log.d(TAG,"圖片文件:$photoFile")
                    // 把照片文件封裝成 Uri
                    photoUri = FileProvider.getUriForFile(
                        requireActivity(), // 當前的Activity
                        "com.example.criminalintent.fileprovider",  // 授權字符串,與 AndroidManifest.xml 文件裏的一致
                        photoFile  //照片文件
                    )
                    updateUI()
                }
            }
        )
    }

    override fun onStart() {
        ...
        photoButton.apply {
            val packageManager: PackageManager = requireActivity().packageManager

            // 檢查是否有相機應用
            var resolvedActivity: ResolveInfo? =
                packageManager.resolveActivity(captureImageIntent, PackageManager.MATCH_DEFAULT_ONLY)
            if (resolvedActivity == null) {
                isEnabled = false
            }

            setOnClickListener {
                //設置照片存儲的Uri
                captureImageIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)

                val cameraActivities: List<ResolveInfo> =
                    packageManager.queryIntentActivities(
                        captureImageIntent,
                        PackageManager.MATCH_DEFAULT_ONLY)

                // 授予所有的照相機在photoUri指定的位置寫入文件的權限
                for (cameraActivity in cameraActivities) {
                    requireActivity().grantUriPermission(
                        cameraActivity.activityInfo.packageName,
                        photoUri,
                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                    )
                }

                // 啓動照相機 activity
                captureActivityResultLauncher.launch(captureImageIntent)
            }
        }
    }

    private fun updatePhotoView() {
        if (photoFile.exists()) {
            val bitmap = getScaledBitmap(photoFile.path, requireActivity() )
            photoView.setImageBitmap(bitmap)
        }else {
            photoView.setImageBitmap(null)
        }
    }   

    private fun updateUI() {
        ...
        updatePhotoView()
    }

    override fun onDetach() {
        super.onDetach()
        //撤銷權限
        requireActivity().revokeUriPermission(photoUri,
            Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
    }
}
  • Intent(MediaStore.ACTION_IMAGE_CAPTURE): 啓動照相機 activity 的intent,action名稱:MediaStore.ACTION_IMAGE_CAPTURE

  • captureActivityResultLauncher : 啓動照相機 activity 的啓動器

  • 設置照片存儲的Uri:captureImageIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri):
    設置照相機的拍照後照片的存儲位置Uri:photoUri,並使用授權字符進行標記 ,見如下代碼:

                      // 把照片文件封裝成 Uri
                      photoUri = FileProvider.getUriForFile(
                          requireActivity(), // 當前的Activity
                          "com.example.criminalintent.fileprovider",  // 授權字符串,與 AndroidManifest.xml 文件裏的一致
                          photoFile  //照片文件:路徑+文件名
                      )
    
  • Uri授權:授予所有的照相機在photoUri指定的位置寫入文件的權限:

    requireActivity().grantUriPermission(
                          cameraActivity.activityInfo.packageName,
                          photoUri,
                          Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                      )
    
  • 寫入授權:Intent.FLAG_GRANT_WRITE_URI_PERMISSION :

  • 啓動照相機 activity: captureActivityResultLauncher.launch(captureImageIntent)

  • 相機存儲完照片,返回後調用 updatePhotoView() 更新 imageView。

  • 並且用完記得在相機回調函數,和 onDetach()撤銷權限 Intent.FLAG_GRANT_WRITE_URI_PERMISSION

  • 日誌:

    • 圖片位置的日誌:
      圖片文件:/data/user/0/com.example.criminalintent/files/IMG_454f516f-4812-42dc-a06a-479ceb5c359b.jpg
      
    • 圖片原始寬高和轉換後的寬高:
       2022-06-03 17:29:42.474 27366-27366/com.example.criminalintent D/PictureUtils: destWidth:1080, srcWidth:4000.0
       2022-06-03 17:29:42.475 27366-27366/com.example.criminalintent D/PictureUtils: destHeight:2276, srcHeight:3000.0
      
  • 直接在真機的文件管理中是無法查看的,真機的操作系統已經將其隱藏,只能在 Android Studio 的 Device File Exploer 查看。

  • 因爲每個 crime的圖片都是 IMG+crime.id 組成,每次拍照得到的文件名相同,保存會覆蓋掉舊的照片,故,每一個crime即使是拍照多次, 只有對應的一張圖片

縮放和顯示位圖

Bitmap是個簡單對象,它只存儲實際像素數據。也就是說,即使原始照片已壓縮過,但存入Bitmap對象時,文件並不會同樣壓縮。因此,一張1600萬像素24位的相機照片(存爲JPG格式大約5 MB),一旦載入Bitmap對象,就會立即膨脹至48 MB!
這個問題可以設法解決,但需要手動縮放位圖照片。具體做法是,首先確認文件到底有多大,然後考慮按照給定區域大小合理縮放文件。最後,重新讀取縮放後的文件,創建Bitmap對象
代碼清單:PictureUtils.kt

package com.example.criminalintent

import android.app.Activity
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log

private const val TAG = "PictureUtils"

fun getScaledBitmap(path: String, activity: Activity): Bitmap {
    val metrics = activity.resources.displayMetrics
    return getScaledBitmap(path, metrics.widthPixels, metrics.heightPixels)
}

/**
 * 縮放位圖照片
 * */
fun getScaledBitmap(path: String, destWidth: Int, destHeight: Int): Bitmap {
    // 讀取目標文件
    var options = BitmapFactory.Options()
    options.inJustDecodeBounds = true
    BitmapFactory.decodeFile(path, options)

    val srcWidth = options.outWidth.toFloat()
    val srcHeight = options.outHeight.toFloat()

    Log.d(TAG, "destWidth:$destWidth, srcWidth:$srcWidth")
    Log.d(TAG, "destHeight:$destHeight, srcHeight:$srcHeight")

    /**計算縮放比例
     * 1:縮略圖與原始圖標的水平像素一樣
     * 2:表示水平像素比爲 1:2,即:縮略圖的像素是原始文件的 1/4
     */
    var inSampleSize = 1
    if (srcHeight > destHeight || srcWidth > destWidth) {
        val heightScale = srcHeight / destHeight
        val widthScale = srcWidth / destWidth

        val sampleScale = if (heightScale > widthScale) {
            heightScale
        }else{
            widthScale
        }

        inSampleSize = Math.round(sampleScale)
    }

    options = BitmapFactory.Options()
    options.inSampleSize = inSampleSize

    // 讀取和創建最終的BitMap
    return BitmapFactory.decodeFile(path, options)
}

功能聲明

應用的拍照功能用起來不錯,但還有一件事情要做:告訴潛在用戶應用有拍照功能。
假如應用要用到諸如相機、NFC,或者任何其他的隨設備走的功能時,都應該讓Android系統知道。這樣,假如設備缺少這樣的功能,類似Google Play商店的安裝程序就會拒絕安裝應用。
爲了聲明應用要使用相機,在AndroidManifest.xml中加入標籤,
代碼清單:添加標籤(manifest/AndroidManifest.xml)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.criminalintent">
    ...
    <uses-feature android:name="android.hardware.camera"
        android:required="false"/>
</manifest>

注意,我們在代碼中使用了android:required屬性。默認情況下,聲明要使用某個設備功能後,應用就無法支持那些無此功能的設備了,但這不適用於CriminalIntent應用。這是因爲,resolveActivity(...)函數可以判斷設備是否支持拍照。如果不支持,就直接禁用拍照按鈕。
無論如何,這裏設置android:required屬性爲false,Android系統因此就知道,儘管不帶相機的設備會導致應用功能缺失,但應用仍然可以正常安裝和使用

真機運行

運行 CriminalIntent,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章