1. 程式人生 > >Android 自定義相機獲取照片(螢幕適配)

Android 自定義相機獲取照片(螢幕適配)

  1.在應用程式中自定義相機拍照的大體步驟如下:

1.檢測和訪問相機:檢測裝置是否支援拍照,然後發出訪問裝置相機請求。

       2.建立一個預覽類:建立一個繼承自類SurfaceView和實現介面SurfaceHolder介面的相機預覽類,這個類用來預覽從相機得到的實時的圖片.。

       3.新建一個預覽佈局:一旦建立了相機預覽類,還需要建立一個可以包含預覽介面且可以操作控制的檢視佈局。

       4.註冊事件的監聽器:為了響應使用者的動作,需要註冊拍照或者錄視訊的監聽器。

       5.捕捉和儲存檔案:將得到的照片或者視訊儲存起來。

       6.釋放相機:為了讓其它的應用程式使用照相機,在使用完之後,一定要釋放。

注意:在使用完相機之後,要釋放相機,可以通過Camera物件的Camera.release()釋放。如果沒有將相機合適的釋放掉,不管是自己的應用程式還是其他的應用程式訪問相機都將失敗。

2.步驟的簡單程式碼介紹:

 1.檢測相機硬體

 如果你的應用程式沒有特別的在Manifest檔案中宣告使用相機的許可權,你應該檢查相機是否在執行時可用。可以通過PackageManager.hasSystemFeature()方法來檢測,下面為示例程式碼:

/** Check if this device has a camera */
private boolean checkCameraHardware(Context context) {
    if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){
        // this device has a camera
        return true;
    } else {
        // no camera on this device
        return false;
    }
}
宣告使用相機的許可權:<uses-permission android:name="android.permission.CAMERA"/>;宣告使用裝置相機:<uses-feature
        android:name="android.hardware.camera"  android:required="false"/>;如果需要讓相機自動對焦,還需要宣告:<uses-feature android:name="android.hardware.camera.autofocus"/>。

注意:Android裝置可能有多個(0,1,2,3)攝像頭,例如前置攝像頭和後置攝像頭。在Android 2.3(API Level 9)版本以上,可以使用Camera.getNumberOfCameras()方法來檢測裝置攝像頭的個數。備註:在測試的機型中,努比亞手機通過該方法得到相機個數為3。

    2.訪問照相機

       如果你已經決定在你裝置的應用程式中使用照相機,你可以通過得到Camera物件來請求訪問它(或者通過使用intent來訪問,關於通過intent呼叫系統相機獲取圖片,請檢視部落格Android 呼叫系統相機拍照的返回結果)。下面為訪問相機示例程式碼:

/** A safe way to get an instance of the Camera object. */
public static Camera getCameraInstance(){
    Camera c = null;
    try {
        c = Camera.open(); // attempt to get a Camera instance
    }
    catch (Exception e){
        // Camera is not available (in use or does not exist)
    }
    return c; // returns null if camera is unavailable
}
注意:當使用Camera.open()或者Camera.open(int)方法時,每次都要檢查是否有異常。如果沒有檢查異常,當相機在被使用或者不存在的時候,將造成你的應用程式強制被系統關閉。

當你的應用程式執行在Android 2.3(API Level 9)版本以上時,可以通過Camera.open(int cameraId)方法來訪問前置或者後置攝像頭。訪問後置攝像頭:cameraId=Camera.CameraInfo.CAMERA_FACING_BACK = 0;訪問前置攝像頭:cameraId=Camera.CameraInfo.CAMERA_FACING_FRONT = 1。如果通過Camera.open()方法,預設訪問的是後置攝像頭。另外,使用Camera.open(int)方法訪問攝像頭時,要注意在某些裝置上執行該方法需要花費很長一段時間,所以最好將該方法放在工作執行緒中(可以使用AsyncTask類)以防止主執行緒被阻塞。

3.建立一個預覽類:

       為了讓使用者更加高效地拍照和錄視訊,他們必須看見照相機能夠看見的東西。一個照相機預覽類是一個能夠呈現來自出來自相機的實時影像資料的SurfaceView,因此使用者才能夠捕獲圖片和視訊。下面為示例程式碼:

/** A basic Camera preview class */
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
    private SurfaceHolder mHolder;
    private Camera mCamera;

    public CameraPreview(Context context, Camera camera) {
        super(context);
        mCamera = camera;

        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        mHolder = getHolder();
        mHolder.addCallback(this);
        // deprecated setting, but required on Android versions prior to 3.0
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

    public void surfaceCreated(SurfaceHolder holder) {
        // The Surface has been created, now tell the camera where to draw the preview.
        try {
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        } catch (IOException e) {
            Log.d(TAG, "Error setting camera preview: " + e.getMessage());
        }
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // empty. Take care of releasing the Camera preview in your activity.
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        // If your preview can change or rotate, take care of those events here.
        // Make sure to stop the preview before resizing or reformatting it.

        if (mHolder.getSurface() == null){
          // preview surface does not exist
          return;
        }

        // stop preview before making changes
        try {
            mCamera.stopPreview();
        } catch (Exception e){
          // ignore: tried to stop a non-existent preview
        }

        // set preview size and make any resize, rotate or
        // reformatting changes here

        // start preview with new settings
        try {
            mCamera.setPreviewDisplay(mHolder);
            mCamera.startPreview();

        } catch (Exception e){
            Log.d(TAG, "Error starting camera preview: " + e.getMessage());
        }
    }
}

如果你想在你的預覽類中設定一個特定的大小,只需在surfaceChanged()方法中設定即可。當設定預覽大小的時候,你必須使用來自getSupportedPreviewSizes()方法得到的值,不能隨便使用setPreviewSize(int width,int height)方法設定一個值,如果隨便設定一個預覽的值,會導致預覽失敗。如果自定義的預覽大小在getSupportedPreviewSizes()方法得到的值中不存在,那麼我們就需要找到一個與我們自定義預覽大小最相近的一個值。下面提供我在專案中的解決方案:

    思路:首先,設定的預覽大小應該是大於或者等於自定義的預覽大小的。因此,在獲得所有的預覽值大小之後,可以將每一組值與自定義的預覽大小值做比較,得到寬的差的絕對值和高的差的絕對值之和,然後從中選出“寬的差的絕對值和高的差的絕對值之和”中最小的值,並且該組的寬和高都是大於或者等於自定義預覽大小的寬和高的值。 

    首先,需要一個包裹預覽大小的類:

/**
 * Created by wangjiang on 2016/1/28.用於儲存得到的相機預覽大小的值,以及自定義預覽大小與相機預覽大小差的絕對值,以便從相機預覽大小所有值中找出大於或者等於自定義預覽大小最合適的值。
 */
public class WrapCameraSize implements Comparable<WrapCameraSize> {
    private int width;//寬
    private int height;//高
    private int d;//寬的差的絕對值和高的差的絕對值之和

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getD() {
        return d;
    }

    public void setD(int d) {
        this.d = d;
    }


    @Override
    public int compareTo(WrapCameraSize another) {
        if (this.d > another.d) {
            return 1;
        } else if (this.d < another.d) {
            return -1;
        }
        return 0;
    }
}
下面為從相機預覽大小中找到最適合的預覽大小值的方法:
 /*
    * 設定相機Preview Size 和 Picture Size,找到裝置所支援的最匹配的相機預覽和圖片大小
    * */
    private void setCameraSize(Camera.Parameters parameters, int width, int height) {
        Map<String, List<Camera.Size>> allSizes = new HashMap<>();
        String typePreview = "typePreview";
        String typePicture = "typePicture";
        allSizes.put(typePreview, parameters.getSupportedPreviewSizes());
        allSizes.put(typePicture, parameters.getSupportedPictureSizes());
        Iterator iterator = allSizes.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, List<Camera.Size>> entry = (Map.Entry<String, List<Camera.Size>>) iterator.next();
            List<Camera.Size> sizes = entry.getValue();
            if (sizes == null || sizes.isEmpty()) continue;
            ArrayList<WrapCameraSize> wrapCameraSizes = new ArrayList<>(sizes.size());
            for (Camera.Size size : sizes) {
                WrapCameraSize wrapCameraSize = new WrapCameraSize();
                wrapCameraSize.setWidth(size.width);
                wrapCameraSize.setHeight(size.height);
                wrapCameraSize.setD(Math.abs((size.width - width)) + Math.abs((size.height - height)));
                if (size.width == width && size.height == height) {
                    if (typePreview.equals(entry.getKey())) {
                        parameters.setPreviewSize(size.width, size.height);
                    } else if (typePicture.equals(entry.getKey())) {
                        parameters.setPictureSize(size.width, size.height);
                    }
                    Log.d(TAG, "best size: width=" + size.width + ";height=" + size.height);
                    break;
                }
                wrapCameraSizes.add(wrapCameraSize);
            }
            Log.d(TAG, "wrapCameraSizes.size()=" + wrapCameraSizes.size());
            Camera.Size resultSize = null;
            if (typePreview.equals(entry.getKey())) {
                resultSize = parameters.getPreviewSize();
            } else if (typePicture.equals(entry.getKey())) {
                resultSize = parameters.getPictureSize();
            }
            if (resultSize != null) {
                if (resultSize.width != width && resultSize.height != height) {
                    //找到相機Preview Size 和 Picture Size中最適合的大小
                    WrapCameraSize minCameraSize = Collections.min(wrapCameraSizes);
                    while (!(minCameraSize.getWidth() >= width && minCameraSize.getHeight() >= height)) {
                        wrapCameraSizes.remove(minCameraSize);
                        minCameraSize = null;
                        minCameraSize = Collections.min(wrapCameraSizes);
                    }
                    Log.d(TAG, "best min size: width=" + minCameraSize.getWidth() + ";height=" + minCameraSize.getHeight());
                    if (typePreview.equals(entry.getKey())) {
                        parameters.setPreviewSize(minCameraSize.getWidth(), minCameraSize.getHeight());
                    } else if (typePicture.equals(entry.getKey())) {
                        parameters.setPictureSize(minCameraSize.getWidth(), minCameraSize.getHeight());
                    }
                }
            }
            iterator.remove();
        }
    }

注意:一般情況下,大多數裝置開啟的相機預設的預覽介面是橫屏的,所以得到相機預覽的寬和高和豎屏的寬和高相反,例如:裝置是480*800,那麼得到的預設的相機預覽大小可能是800*480,因此如果你在手機豎屏自定義的預覽大小的寬和高分別是480和800,那麼在傳入setCameraSize()方法中的寬和高應該是800和480。

    4.將預覽放進主佈局裡面

       為了讓使用者介面可以拍照和錄視訊,必須將相機預覽放進Activity的佈局裡面。下面的佈局程式碼將提供一個能夠展示相機預覽的非常基礎的檢視,其中的FramLayout是相機預覽的容器。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
  <FrameLayout
    android:id="@+id/camera_preview"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:layout_weight="1"
    />

  <Button
    android:id="@+id/button_capture"
    android:text="Capture"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    />
</LinearLayout>

在大多數裝置中,預設的預覽介面的方向是橫屏的,因此上面的佈局示例程式碼特定設定為水平的方向。為了簡化渲染相機預覽,你應該在Manifest檔案中將Activity的方向改變為橫屏方向。
<activity android:name=".CameraActivity"
          android:label="@string/app_name"

          android:screenOrientation="landscape">
          <!-- configure this activity to use landscape orientation -->

          <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

注意:在Android 2.2 (API Level 8)版本以上,可以通過Camera的setDisplayOrientation(int degrees)方法來設定預覽影像的方向,如果可以讓使用者切換預覽的方向(橫屏或者豎屏),可以在預覽類的surfaceChanged()方法中操作,在改變方向前呼叫Camera.stopPreview()方法停止預覽,然後再呼叫Camera.startPreview()方法重新預覽。

另外,通常情況下,我們需要獲取相機預設的方向來設定應該旋轉的角度。在官方文件中提供了下面的方法來獲取相機的方向和設定旋轉的角度:

    public static void setCameraDisplayOrientation(Activity activity,
              int cameraId, android.hardware.Camera camera) {
          android.hardware.Camera.CameraInfo info =
                  new android.hardware.Camera.CameraInfo();
          android.hardware.Camera.getCameraInfo(cameraId, info);
          int rotation = activity.getWindowManager().getDefaultDisplay()
                  .getRotation();
          int degrees = 0;
          switch (rotation) {
              case Surface.ROTATION_0: degrees = 0; break;
              case Surface.ROTATION_90: degrees = 90; break;
              case Surface.ROTATION_180: degrees = 180; break;
              case Surface.ROTATION_270: degrees = 270; break;
          }
     
          int result;
          if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
              result = (info.orientation + degrees) % 360;
              result = (360 - result) % 360;  // compensate the mirror
          } else {  // back-facing
              result = (info.orientation - degrees + 360) % 360;
          }
          camera.setDisplayOrientation(result);
      }
接下來,在Activity中將預覽類新增到FramLayout,記住,在Activity進入pause或者stop或者destory的時候,一定要釋放相機。
public class CameraActivity extends Activity {

    private Camera mCamera;
    private CameraPreview mPreview;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // Create an instance of Camera
        mCamera = getCameraInstance();

        // Create our Preview view and set it as the content of our activity.
        mPreview = new CameraPreview(this, mCamera);
        FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
        preview.addView(mPreview);
    }
}

    5.捕獲照片

       設定監聽器,為了獲得JPEG格式圖片的資料,你必須實現Camera.PictureCallback介面。

private PictureCallback mPicture = new PictureCallback() {

    @Override
    public void onPictureTaken(byte[] data, Camera camera) {
       
      Bitmap resultBitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
        
    }
};

接下來只需呼叫Camera.takePicture()方法就可以獲得圖片:
// Add a listener to the Capture button
Button captureButton = (Button) findViewById(id.button_capture);
captureButton.setOnClickListener(
    new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // get an image from the camera
            mCamera.takePicture(null, null, mPicture);
        }
    }
);

注意:在從PictureCallback介面的回撥方法中得到的resultBitmp是並不是旋轉了的,而是設定相機旋轉之前的方向。因此,如果你想要儲存與你預覽的方向一致的圖片,你需要做一些旋轉圖片的操作。
  Matrix matrix = new Matrix();
                if (mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
                    matrix.postRotate(-mRotationDegrees);
                } else if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {
                    matrix.postRotate(mRotationDegrees);
                }
                Bitmap rotateBitmap = Bitmap.createBitmap(resultBitmap, 0, 0, resultBitmap.getWidth(), resultBitmap.getHeight(), matrix, true);

mRotationDegrees為上面提到的setCameraDisplayOrientation()方法中得到的旋轉的角度,另外,如果設定的pictureSize較大,還要注意引起記憶體溢位問題。

注意:如果在回撥方法onPictureTaken()裡面需要對圖片的資料做複雜的處理,最好在工作行程(子執行緒)裡面操作。另外,在執行Camera.takePicture()方法前,需預覽未被銷燬,且在startPreview()之後。當拍照之後,會呼叫stopPreview()方法,如果你要繼續拍照,再呼叫startPreview()方法就行了,但記住,該方法不應該在android.media.MediaRecorder的start()和stop()方法間執行。

    到這裡,自定義相機獲取圖片就介紹完了。