Android實踐:《簡易照相機》的詳細實現步驟及知識梳理

本文實現的簡易的照相機,包括拍照、選圖、分享三個功能,主要涉及到Intent隱式啓動、Intent動作、Uri、危險權限讀取、圖像處理、手機旋轉等知識。

首先,設計程序佈局,僅有一個活動界面,如下所示:


設計程序的詳細步驟、所涉知識整理如下:

/**
 * 項目目標:實現簡易照相機
 * 實現方式:啓動系統照相機
 * 學習目標:Intent隱式啓動、簡單模仿相機的圖片處理
 *
 * 實現步驟:
 * 1、設計程序佈局
 * 2、使用Intent啓動系統照相機
 * 3、圖片處理:
 *   (1)、權限獲取
 *   (2)、保存圖片
 *   (3)、顯示圖片
 *   (4)、手機旋轉,圖像的處理
 * 4、使用Intent瀏覽並選取照片
 * 5、分享照片
 *
 * 詳細實現過程及知識整理:
 * 一、使用Intent啓動系統照相機
 * Intent it = new Intent("android.media.action.IMAGE_CAPTURE");
 * startActivityForResult(it,100);
 * 涉及動作:android.media.action.IMAGE_CAPTURE,意爲拍照。
 * 啓動方式:要求動作完成後返回數據。
 * 100作爲自定義標誌符,用於識別。即處理多個返回的數據時,可以確定是哪一個Intent。
 *
 * 二、圖片處理
 * 1、權限獲取
 *  若要保存圖片,必然涉及寫操作,需要寫權限纔可以,這在android系統中是一個危險權限,
 *  實現讀寫操作,需要在AndroidManifest.xml中加入以下權限:
 *  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 *  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 *  同時,還需要向用戶請求獲取這些權限,因爲上述兩個權限屬於STORAGE權限組,所以只要其中一個
 *  權限被允許,其他同類權限也會被自動允許,故程序中可以只添加其中一個權限。
 *
 * 以寫權限爲例:
 * if (ActivityCompat.checkSelfPermission(this,
 * Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
 * PackageManager.PERMISSION_GRANTED) {         //檢查是否已獲得寫入權限
 *     ActivityCompat.requestPermissions(this,  //向用戶要求允許寫入權限
 *     new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},200);
 * }
 * 在向用戶請求權限的代碼中,方法的第二個參數必須爲數組形式,可以一次請求多個權限,第三個參數200
 * 代表自定義的識別碼,用戶無論是否同意權限,程序都可以通過onRequestPermissionsResult方法接收
 * 結果,此時可以作爲識別之用。如下代碼所示:
 * public void onRequestPermissionsResult(int requestCode, String[] permissions,
 * int[] grantResults){
 *     if (requestCode == 200){
 *         if (grantResults[0] == PackageManager.PERMISSION_GRANTED){ //用戶允許權限
 *             savePhoto();  //獲取權限後纔可以進行保存圖片操作
 *         }else{       //用戶拒絕權限
 *             Toast.makeText(this,"程序需要寫入權限才能運行",Toast.LENGTH_SHORT).show();
 *         }
 *     }
 * }
 * 其中grantResults數組中存放的就是用戶允許還是拒絕權限的結果,可以同時有多個,與要求權限時的
 * 權限排列順序、數量相同。
 *
 * 2、保存圖片
 * 因爲資源的存取只能通過內容提供者,利用URI的傳遞,以便同意管控權限而避免不當的存取。
 * 首先使用getContentResolver()獲得系統的內容提供者,然後通過它在手機共享圖像文件路徑裏新增
 * 一個文件,並傳回該文件的Uri對象,然後將該對象保存。
 * 綜上所述,獲取權限後,啓動系統照相機,啓動時將Uri對象加入額外數據中,如下代碼所示:
 * private void savePhoto(){
 *     imgUri = getContentResolver().insert(
 *              MediaStore.Images.Media.EXTERNAL_CONTENT_URI,new ContentValues());
 *     Intent it = new Intent("android.media.action.IMAGE_CAPTURE");
 *     it.putExtra(MediaStore.EXTRA_OUTPUT, imgUri);
 *     startActivityForResult(it, 100);
 * }
 * 其中putExtra方法中,通過內容提供者獲取的對象就是imgUri,名字爲MediaStore.EXTRA_OUTPUT,
 * 相機程序會用這個名字讀取未來拍照存檔的URI。
 *
 * 3、顯示圖片
 * (1)、用BitmapFactory來讀取圖像。
 * Bitmap bmp = BitmapFactory.decodeStream(getContentResolver().
 *              openInputStream(imgUri), null, null);//讀取圖像內容並存儲爲Bitmap對象
 * imv.setImageBitmap(bmp);         //將Bitmap對象顯示在ImageView中
 * 如此一來就會把圖片顯示出來了,但是這樣做有一個弊端,當圖片過大的時候,很有可能會因爲內存不
 * 足而導致程序崩潰,所以需要對顯示的圖片進行約束,合理顯示,避免過大。
 * (2)、用BitmapFactory.Options設置加載圖像文件的選項
 * BitmapFactory.Options option = new BitmapFactory.Options(); //新建選項對象
 * option.inJustDecodeBounds = true;     //設置選項:只讀取圖片文件信息而不加載圖片文件
 * BitmapFactory.decodeStream(imgUri.getPath(),option);//讀取圖像文件信息存入option中
 * iw = option.outWidth();      //從option中讀取圖像文件的寬
 * ih = option.outHeight();     //從option中讀取圖像文件的高
 * 按比例縮小圖像:
 * vw = imv.getWidth();    //獲取 ImageView 的寬度
 * vh = imv.getHeight();   //獲取 ImageView 的高度
 * int scaleFactor = Math.min(iw/vw, ih/vh);   // 計算縮小比例
 * option.inJustDecodeBounds = false;  //關閉只加載圖片文件信息的選項
 * option.inSampleSize = scaleFactor;  //設置縮小比例,例如2則長寬都將縮小爲原來的1/2
 * 這時圖像的處理之後的信息都在Options對象中,載入圖片:
 * Bitmap bmp = BitmapFactory.decodeStream(
 *              getContentResolver().openInputStream(imgUri), null, option);
 * 顯示圖片:imv.setImageBitmap(bmp); //顯示圖片
 *
 * 4、手機旋轉,圖像的處理。
 * 如果手機開啓自動旋轉屏幕,則剛纔拍攝的照片在顯示中,會因爲屏幕的旋轉而消失。
 * 解決方式有兩種:
 * 方式一:在發生旋轉時,立即將所顯示照片的Uri存儲起來,等旋轉完並重新啓動Activity後,在按
 * 照存儲的Uri將照片顯示出來,另外,還可以按照旋轉的方向載入不用的界面Layout文件來顯示。
 * 方式二:關閉手機界面的自動旋轉功能。
 * 用戶在拍攝時會有橫拍或豎拍,所以最好由程序決定要不要旋轉屏幕,而不是隨手機的旋轉而旋轉。
 * (1)、關閉自動旋轉功能並設置屏幕爲直向顯示
 * //設置屏幕不隨手機旋轉
 * setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
 * /設置屏幕直向顯示/
 * setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
 * (2)、處理橫拍和豎拍時,圖像寬高的縮小比例
 * Boolean needRotate;                         //用來存儲是否需要旋轉
 * int scaleFactor;
 * if(iw < ih) {    //如果圖片的寬度小於高度
 *     needRotate = false;                     //不需要旋轉
 *     scaleFactor = Math.min(iw/vw, ih/vh);   // 計算縮小比例
 * }
 * else {
 *     needRotate = true;                      //需要旋轉
 *     scaleFactor = Math.min(iw/vh, ih/vw);// 將ImageView的寬高互換來計算縮小比例
 * }
 * (3)、用Matrix(android.graphics.Matrix)對象旋轉圖片
 * Matrix是一個旋轉矩陣,用Bitmap.createBitmap()創建新的Bitmap時,可以用它來旋轉圖片。
 * createBitmap(Bitmap src,int x,int y,int width,int height,Matrix m,boolean filter)
 * src爲要複製的來源Bitmap對象。
 * x、y指定要由來源Bitmap的那個位置開始複製(從左上角算起)。
 * width、height爲新的Bitmap的寬高。
 * m是旋轉矩陣,而最後一個參數filter設置了旋轉時需要傳入true。
 * 注:生成新的Bitmap後,原來的Bitmap對象就會被系統回收。
 * 具體代碼如下:
 * if(needRotate) { //如果需要旋轉
 *     Matrix matrix = new Matrix();   //新建 Matrix 對象
 *     matrix.postRotate(90);          //設置旋轉角度
 *     bmp = Bitmap.createBitmap(bmp , //用原來的 Bitmap 創建一個新的 Bitmap
 *           0, 0, bmp.getWidth(), bmp.getHeight(), matrix, true);
 * }
 *
 * 做了這麼多的圖像處理後,爲了更直觀的瞭解圖片從拍照到顯示的變化,可以加入以下代碼,用一個
 * 彈窗顯示出圖片的詳細信息。
 * new AlertDialog.Builder(this)
 *     .setTitle("圖像文件信息")
 *     .setMessage("圖像文件URI:" + imgUri.toString() +
 *         "\n原始尺寸:" + iw + "x" + ih +
 *         "\n載入尺寸:" + bmp.getWidth() + "x" + bmp.getHeight() +
 *         "\n顯示尺寸:" + vw + "x" + vh + (needRotate?"(旋轉)":""))
 *     .setNeutralButton("關閉",null)
 *     .show();
 * 窗口中詳細的展示了圖像的URI路徑、拍照尺寸、Bitmap載入的尺寸、ImageView顯示的尺寸,還
 * 包括是否做了旋轉操作。
 *
 *三、使用Intent瀏覽選擇圖片
 * Android系統中內建了一個內容提供者,存儲着各種數據和多媒體文件等共享數據的相關信息。
 * Android內建的圖庫程序就是從這個數據庫中讀取可共享的圖片文件信息,然後列出來供用戶選取。
 * 所以將拍攝的照片設爲系統共享文件,並用廣播Intent的方式通知系統。
 * //將imgUri所指的文件設爲系統共享媒體文件
 * Intent it = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, imgUri);
 * sendBroadcast(it);   //通知系統
 * 選取圖片:
 * Intent it = new Intent(Intent.ACTION_GET_CONTENT);   //動作設爲 "選取內容"
 * it.setType("image/*");            //設置要選取的媒體類型爲:所有類型的圖片
 * startActivityForResult(it, 101);  //啓動意圖, 並要求返回選取的圖片
 * 101是自定義的識別碼。上面的代碼中要求返回選取的圖片,所以在onActivityResult方法中可以
 * 根據識別碼判斷是哪一個Intent的動作,並獲取這個圖片的Uri信息,然後將其顯示。
 *
 * 四、分享圖片
 * 只要圖片不爲空,就可以使用動作ACTION_SEND分享圖片,代碼如下:
 * if(imgUri != null){
 *     Intent it = new Intent(Intent.ACTION_SEND);
 *     it.setType("image/*");
 *     it.putExtra(Intent.EXTRA_STREAM,imgUri);
 *     startActivity(it);
 * }
 *
 * */

程序具體代碼如下所示:

package com.my.camera;

import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;

import java.io.IOException;

public class MainActivity extends AppCompatActivity {

    Uri imgUri;         //記錄圖片信息
    ImageView imv;      //視圖組件

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //設置屏幕不隨手機旋轉
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
        //設置屏幕直向顯示
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

        //關聯視圖組件
        imv = (ImageView)findViewById(R.id.imageView);
    }

    /**
     * 按鈕“拍照”的點擊事件
     * */
    public void onGet(View v){

        //檢查是否獲得寫入權限,未獲得則向用戶請求
        if(ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED){
            //未獲得,向用戶請求
            ActivityCompat.requestPermissions(this,new String[]
                    {Manifest.permission.WRITE_EXTERNAL_STORAGE},200);
        }else{
            //已經獲得權限
            savePhoto();
        }

    }

    /**
     * 按鈕“圖庫”的點擊事件
     * */
    public void onPick(View v){
        Intent it = new Intent(Intent.ACTION_GET_CONTENT);  //設置動作爲選取內容
        it.setType("image/*");                              //設置要選取的媒體類型:所有類型圖片
        startActivityForResult(it,101);                     //啓動Intent,並要求返回選取的圖像文件
    }

    /**
     * 按鈕“分享”的點擊事件
     * */
    public void onShare(View v){
        if(imgUri != null){
            Intent it = new Intent(Intent.ACTION_SEND);
            it.setType("image/*");
            it.putExtra(Intent.EXTRA_STREAM,imgUri);
            startActivity(it);
        }
    }

    /**
     * 返回用戶是否允許權限的結果,並處理
     * */
    public void onRequestPermissionsResult(int requestCode,String[] permissions, int[] grantResult){
        if(requestCode == 200){
            //用戶允許權限
            if(grantResult[0] == PackageManager.PERMISSION_GRANTED){
                savePhoto();    //允許寫權限纔可以保存圖片
            }else{              //用戶拒絕
                Toast.makeText(this,"程序需要寫入權限才能運行",Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void savePhoto(){
        //通過內容提供者新增一個圖像文件
        imgUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                new ContentValues());
        Intent it = new Intent("android.media.action.IMAGE_CAPTURE");   //指定動作,拍照
        it.putExtra(MediaStore.EXTRA_OUTPUT,imgUri);        //將uri加到拍照Intent的額外數據

        startActivityForResult(it,100);             //設置100爲識別碼,啓動活動
    }

    /**
     * 處理通過Intent啓動的活動的返回結果
     * 識別碼100,拍照活動返回的數據,將拍攝的圖片設爲共享,並通知系統
     * 識別碼101,選取圖片活動返回的數據,直接讀取數據並存儲到imgUri中
     * 調用showImg方法顯示圖片
     * 根據識別碼來顯示不同的出錯Toast信息
     * */
    protected void onActivityResult(int requestCode,int resultCode,Intent data){

        super.onActivityResult(requestCode,resultCode,data);

        if(resultCode == Activity.RESULT_OK){

            switch(requestCode){
                case 100:
                    //設爲系統共享媒體文件
                    Intent it = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,imgUri);
                    sendBroadcast(it);
                    break;
                case 101:
                    //選取圖片,直接獲取該圖片的數據
                    imgUri = data.getData();
                    break;
            }
            showImg();     //顯示圖片

        }else{  //根據識別碼的不同,顯示不同的沒有獲取到圖片的信息
            Toast.makeText(this,requestCode==100?"沒有拍到照片":"沒有選取照片",
                    Toast.LENGTH_SHORT).show();
        }

    }

    /**
     * 顯示圖片
     * 避免圖像過大,進行處理後顯示
     * 判斷圖片是橫拍還是直拍決定是否旋轉
     * 顯示圖片處理過程的詳細信息
     * */
    public void showImg(){

        int iw,ih,vw,vh;    //iw,ih爲圖片寬高,vw,vh爲ImageView組件的寬高
        boolean needRotate; //用來存儲是否需要旋轉

        Bitmap bmp = null;  //創建Bitmap對象

        BitmapFactory.Options option = new BitmapFactory.Options();  //創建選項對象

        option.inJustDecodeBounds = true;   //設置選項:只讀取圖像文件信息而不載入圖像文件

        try{
            //讀取圖像文件信息存入option中
            BitmapFactory.decodeStream(getContentResolver().openInputStream(imgUri),null,option);
        }catch(IOException e){
            Toast.makeText(this,"讀取照片信息時發生錯誤",Toast.LENGTH_SHORT).show();
            return;
        }

        iw = option.outWidth;   //從option中讀取圖像文件的寬度
        ih = option.outHeight;  //從option中讀取圖像文件的高度
        vw = imv.getWidth();    //獲取ImageView的寬度
        vh = imv.getHeight();   //獲取ImageView的高度

        int scaleFactor;
        if(iw < ih){
            needRotate = false; //不需要旋轉
            scaleFactor = Math.min(iw/vw,ih/vh);    //計算縮小比例
        }else{
            needRotate = true;
            scaleFactor = Math.min(ih/vw,iw/vh);    //改用旋轉後的圖像寬、高計算縮小比例
        }

        option.inJustDecodeBounds = false;  //關閉之加載圖像文件信息的選項
        option.inSampleSize = scaleFactor;  //設置縮小比例,若爲3,則長寬將縮小爲原來的1/3

        try{
            //加載圖片
            bmp = BitmapFactory.decodeStream(getContentResolver().openInputStream(imgUri),
                    null,option);
        }catch(IOException e){
            Toast.makeText(this,"無法取得照片",Toast.LENGTH_SHORT).show();
        }

        if(needRotate){
            Matrix matrix = new Matrix();   //創建Matrix對象
            matrix.postRotate(90);          //旋轉90°,順時針
            //用原來的圖像產生一個新的圖片
            bmp = Bitmap.createBitmap(bmp,0,0,bmp.getWidth(),bmp.getHeight(),matrix,true);
        }

        imv.setImageBitmap(bmp);            //顯示圖片

        //彈窗顯示圖片的信息
        new AlertDialog.Builder(this)
                .setTitle("圖像文件信息")
                .setMessage("圖像文件URI:" + imgUri.toString() +
                        "\n原始尺寸:" + iw + "x" + ih +
                        "\n載入尺寸:" + bmp.getWidth() + "x" + bmp.getHeight() +
                        "\n顯示尺寸:" + vw + "x" + vh + (needRotate?"(旋轉)":""))
                .setNeutralButton("關閉",null)
                .show();

    }

}
//                     總結參考:《Android App開發入門 第2版》 機械工業出版社 施威銘 著 2017.8 

程序的部分運行結果如下所示:


(哇,我在編輯的時候發現這個圖好大好大啊,不知道發佈出去之後會不會還這麼大。)


這個簡易照相機非常的簡單,使用起來非常的呆板,還需要不斷的完善。


發佈了26 篇原創文章 · 獲贊 43 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章