Android程式設計權威指南(第二版)學習筆記(十六)—— 第16章 使用 intent 拍照

Kniost發表於2017-01-12

本章主要講了如何使用 intent 拍照,儲存照片和展示照片

GitHub 地址:
完成16章,但未完成挑戰
完成16章挑戰1
完成16章挑戰2

1. 外部儲存

相機照片動輒幾 MB 大小,直接儲存在資料庫中肯定不現實。很自然,大家會想到直接使用裝置的檔案系統。
一般來講,應用都應該使用私有儲存空間儲存各類檔案。還記得嗎?在前面章節中,我們在私有儲存空間儲存過 SQLite 資料檔案。使用類似 Context.getFileStreamPath(String)和 Context.getFilesDir()這樣的方法,我們也可以實現這樣的儲存目標,下表所示:

Context 類提供的方法 使用目的
File getFilesDir() 獲取/data/data//files 目錄
FileInputStream openFileInput(String name) 開啟現有檔案進行讀取
FileOutputStream openFileOutput(String name, int mode) 開啟檔案進行寫入,如不存在就建立它
File getDir(String name, int mode) 獲取/data/data//目錄的子目錄(如不存在就先建立它)
String[] fileList() 獲取/data/data//files 目錄下的檔案列表。可與其他方法配合使用,例如 openFileInput(String)
File getCacheDir() 獲取/data/data//cache 目錄。應注意及時清理該目錄,並節約使用空間

如果想儲存的檔案僅供應用內部使用,使用上表中的各類方法就可以了。而如果想共享檔案給其他應用或是接收其他應用的檔案(如相機應用拍攝的照片)時,路只有一條:使用外部儲存儲存檔案。
外部儲存有兩類:主外部儲存和其他各類儲存介質。所有的 Android 裝置至少應有一個主外部儲存地。使用Environment.getExternalStorageDirectory()可以返回這個外部儲存目錄。 以前,這個儲存地通常是指 SD 卡,但現在都已基本整合至了裝置內部。即使現在還有裝置使用擴充套件外部儲存,也應算作其他各類儲存介質這一類了。
Context 也提供了一些訪問外部儲存空間要用到的方法,如下表所示。

方法 使用目的
File getExternalCacheDir() 獲取主外部儲存上的快取檔案目錄。用法類似 getCacheDir()方法,但要注意,Android 一般不會自動清理該目錄
File[] getExternalCacheDirs() 獲取多個外部儲存上的快取檔案目錄
File getExternalFilesDir(String) 獲取主外部儲存上存放常規檔案的檔案目錄。通過 String 引數,可訪問特定內容型別的子目錄。內容型別常量以 DIRECTORY_為字首,定義在 Environment 中 。 例如 , 用於 影象 檔案 的 Environment.DIRECTORY_ PICTURES
File[] getExternalFilesDirs(String) 類似 getExternalFilesDir(String)方法,但該方法可獲取指定型別的所有檔案目錄
File[] getExternalMediaDirs() 獲取 Android 儲存圖片、視訊和音樂檔案的所有外部檔案目錄。和 getExternalFilesDir(Environment.DIRECTORY_PICTURES) 方法 區別 在於,呼叫該方法,多媒體掃描器會自動掃描目標目錄,並將存放的多媒體檔案暴露給能夠播放音樂、瀏覽視訊和圖片的應用。也就是說, getExternalMediaDirs()方法返回目錄中存放的任何檔案都會自動出現在多媒體應用中

1.1 指定照片存放位置

首先,一張照片的檔名我們用一個 Crime 的 ID 來標識,所以在 Crime.java 中加入了獲取檔名的方法:

public String getPhotoFileName() {
    return "IMG_" + getId().toString() + ".jpg";
}

然後在 CrimeLab.java 中加入獲取路徑檔案的函式:

public File getPhotoFile(Crime crime) {
    File externalFilesDir = mContext
            .getExternalFilesDir(Environment.DIRECTORY_PICTURES);

    if (externalFilesDir == null) {
        return null;
    }

    return new File(externalFilesDir, crime.getPhotoFileName());
}

1.2 外部儲存使用許可權

讀寫外部儲存需要獲得許可權,一般在AndroidManifest.xml中使用<uses-permission>標籤來使用。而對於 API 19(Android 4.4)及以後的新版系統來說,應用不需要再申請 Context.getExternalFilesDir(String) 所需要的許可權了,所以這個許可權申請是這麼寫的:

<uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="18"/>

2. 使用相機 intent

實現拍照功能只需要使用一個隱式 intent,分為下面幾步:

  • 獲取儲存圖片的檔案儲存位置
  • 處理拍照按鈕,實現觸發拍照,其實就是傳送一個帶有 MediaStore.ACTION_IMAGE_CAPTURE的 intent 即可。

對於 intent 的操作,我們需要定義在 MediaStore 類中的ACTION_CAPTURE_IMAGE。MediaStore 類定義了一些公共介面,可用於處理影象、視訊以及音樂這些常見的多媒體任務。當然,這也包括觸發相機應用的拍照 intent。

如果只用ACTION_IMAGE_CAPTURE開啟相機應用,預設只能拍攝縮圖這樣的低解析度照片,而且照片會儲存在 onActivityResult(…)返回的 Intent 物件裡。要想獲得全尺寸照片,就要讓它使用檔案系統儲存照片。這可以通過傳入儲存在 MediaStore.EXTRA_OUTPUT 中的指向儲存路徑的 Uri 來完成。
編寫用於拍照的隱式 intent,拍攝的照片應該儲存在 mPhotoFile 指定的地方。同時,別忘了檢查裝置上是否安裝有相機應用,以及是否有地方儲存照片。

mPhotoButton = (ImageButton) v.findViewById(R.id.crime_camera);
// 首先建立一個用於拍照的 Intent 物件
final Intent captureImage = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 檢查是否有可拍照的應用
boolean canTakePhoto = mPhotoFile != null &&
        captureImage.resolveActivity(packageManager) != null;
mPhotoButton.setEnabled(canTakePhoto);

if (canTakePhoto) {
    // 建立訪問照片目錄的 Uri
    Uri uri = Uri.fromFile(mPhotoFile);
    // 將該 Uri 放入 intent 物件中
    captureImage.putExtra(MediaStore.EXTRA_OUTPUT, uri);
}

mPhotoButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        // 使用 startActivityForResult 是為了拍完照後重新整理檢視
        startActivityForResult(captureImage, REQUEST_PHOTO);
    }
});

3. 縮放和顯示點陣圖

有了照片,接下來就是找到並載入它,然後展示給使用者看。在技術實現上,這需要載入照片到大小合適的 Bitmap 物件中。而要從檔案生成 Bitmap 物件,我們需要 BitmapFactory 類:
Bitmap bitmap = BitmapFactory.decodeFile(mPhotoFile.getPath());

Bitmap 是個簡單物件,它只儲存實際畫素資料。也就是說,即使原始照片已壓縮過,但存入 Bitmap 物件時,檔案並不會同樣壓縮。因此,如果有一個16萬畫素24位已壓縮為5Mb 大小的 JPG 照片檔案,一旦載入 Bitmap 物件,就會立即膨脹至48Mb 大小!
這個問題可以設法解決,但需要手工縮放點陣圖照片。具體做法就是,首先確認檔案到底有多大,然後考慮按照給定區域大小合理縮放檔案。最後,重新讀取縮放後的檔案,建立 Bitmap 物件。
既然需要處理影象檔案,我們建立一個通用的工具類,名為 PictureUtils.java。在其中新增 getScaledBitmap(String, int, int)縮放方法,

public class PictureUtils {
    public static Bitmap getScaledBitmap(String path, int destWidth, int destHeight) {
        // Read in the dimensions of the image on disk
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(path, options);

        float srcWidth = options.outWidth;
        float srcHeight = options.outHeight;

        // Figure out how much to scale down by
        int inSampleSize = 1;
        if (srcHeight > destHeight || srcWidth > destWidth) {
            if (srcWidth > srcHeight) {
                inSampleSize = Math.round(srcHeight / destHeight);
            } else {
                inSampleSize = Math.round(srcWidth / destWidth);
            }
        }

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

        // Read in and create final bitmap
        return BitmapFactory.decodeFile(path, options);
    }
}

上述方法中,inSampleSize 值很關鍵。它決定著縮圖畫素的大小。假設這個值是1的話,就表明縮圖和原始照片的水平畫素大小一樣。如果是2的話,它們的水平畫素比就是1∶2。因此,inSampleSize 值為2時,縮圖的畫素數就是原始檔案的四分之一。
問題總是接踵而來。解決了縮放問題,又冒出了新問題:fragment 剛啟動時,PhotoView 究竟有多大無人知道。onCreate(…)、onStart()和 onResume()方法啟動後,才會有首個例項化佈局出現。也就在此時,顯示在螢幕上的檢視才會有大小尺寸。這也是出現新問題的原因。
解決方案有兩個:要麼等佈局例項化完成並顯示,要麼乾脆使用保守估算值。特定條件下, 儘管估算比較主觀,但確實是一個切實可行的辦法。再新增一個 getScaledBitmap(String, Activity)靜態 Bitmap 估算方法。

public static Bitmap getScaledBitmap(String path, Activity activity) {
    Point size = new Point();
    activity.getWindowManager().getDefaultDisplay()
            .getSize(size);

    return getScaledBitmap(path, size.x, size.y);
}

4. 功能宣告

應用的拍照功能用起來不錯,但還有件事情要做:告訴目標使用者應用具有拍照功能。

假如應用要用到諸如相機、NFC,或者任何其他的隨裝置走的功能時,都應該要讓 Android 系統知道。否則,假如裝置缺少這樣的功能,類似 Google Play 商店的安裝程式就會拒絕安裝應用。
為宣告需要使用相機,在 AndroidManifest.xml 中加入<uses-feature>標籤:

<uses-feature
    android:name="android.hardware.camera2"
    android:required="false"/>

5. 佈局檔案中的 <include> 標籤

如果有重複的佈局可以使用,那麼可以採用 include 標籤,直接在不同的 layout 中引用。
然而,經驗表明,佈局檔案的優點是可靠又好用。例如,直接檢視佈局檔案內容,就可以快速準確地知道應用檢視是如何構建的。然而,一旦用了 include 標籤,一切就不好說了。還想明白檢視構成的話,就得仔細翻看佈局主檔案以及所有 include 的佈局檔案。這種非直觀的感覺,極易讓人失去耐心。
使用者介面是應用改動相對頻繁的部分。既然這樣,不顧一切地追求複用原則很可能會適得其反。因此,在檢視層開發時,我們一定要多多考量,儘量做到審慎、合理地使用 include 標籤。

6. 挑戰練習

6.1 優化照片顯示

新建一個 GlancePictureFragment,繼承自 DialogFragment,程式碼如下:

public class GlancePictureFragment extends DialogFragment {

    private static final String ARG_PATH = "path";

    private ImageView mImage;

    // 由於檔案比較大,所以將檔案路徑傳入即可
    public static GlancePictureFragment newInstance(String path) {
        Bundle args = new Bundle();
        args.putString(ARG_PATH, path);
        GlancePictureFragment fragment = new GlancePictureFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // 使用 getArguments() 方法取出照片檔案路徑
        String path = getArguments().getString(ARG_PATH);

        // 這個新的 style 其實就做了一件事,那就是使視窗全屏
        // 注意如果繼承了 @android:Theme.Dialog 的話,視窗
        // 大小就限定了,所以我沒有繼承
        final Dialog dialog = new Dialog(getActivity(), R.style.CustomDialogTheme);
        // 這個 layout 中只有一個 ImageView
        dialog.setContentView(R.layout.dialog_image_glance);

        mImage = (ImageView) dialog.findViewById(R.id.glance_image);
        // 仍然使用 PictureUtils 類的工具來獲得縮放的 Bitmap
        mImage.setImageBitmap(
                PictureUtils.getScaledBitmap(path, getActivity()));
        mImage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            // 點選圖片則退出該 dialog
                dialog.dismiss();
            }
        });
        return dialog;
    }
}

然後在圖片的點選事件中宣告即可

6.2 優化縮圖載入

首先修改更新檢視的函式,接受高寬的指定畫素:

private void updatePhotoView(int width, int height) {
    if (mPhotoFile == null || !mPhotoFile.exists()) {
        mPhotoView.setImageDrawable(null);
    } else {
        Bitmap bitmap = PictureUtils.getScaledBitmap(
                mPhotoFile.getPath(), width, height);
        mPhotoView.setImageBitmap(bitmap);
    }
}

之後,先獲取 mPhotoView 的 ViewTreeObserver,然後設定 OnGlobalLayoutListener 監聽器,在監聽器中即可獲取檢視的高度和寬度,然後進行圖片顯示。

mPhotoObserver = mPhotoView.getViewTreeObserver();
mPhotoObserver.addOnGlobalLayoutListener(
        new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        updatePhotoView(
                mPhotoView.getWidth(),
                mPhotoView.getHeight());
        Log.i("CrimeFragment", "onGlobalLayout: Observed");
    }
});

相關文章