Android 自定义相机获取照片(屏幕适配)

时间:2022-09-10 20:36:32

    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()方法间执行。


    到这里,自定义相机获取图片就介绍完了。