Android 相机使用教程(二)

上一篇介绍了如何使用系统相机简单、快速的进行拍照,本篇将介绍如何使用框架提供的API直接控制摄像机硬件。

你还在为开发中频繁切换环境打包而烦恼吗?快来试试 Environment Switcher 吧!使用它可以在app运行时一键切换环境,而且还支持其他贴心小功能,有了它妈妈再也不用担心频繁环境切换了。https://github.com/CodeXiaoMai/EnvironmentSwitcher

控制相机

直接控制设备相机比从现有相机应用程序中请求图片或视频要复杂很多。但是,如果我们要创建专门的相机应用程序或完全集成到应用程序的界面中,就不得不通过这种方式实现。

为应用程序创建自定义相机的一般步骤如下:

  • 检测和访问摄像头 - 检查相机是否存在并请求访问它。
  • 创建一个预览控件 - 创建一个类继承 SurfaceView 并实现SurfaceHolder接口,这个控件用来预览相机的实时图像。
  • 创建预览布局 - 当相机预览控件创建成功后,创建一个布局文件,其中包含预览控件和所需的用户界面控件。
  • 为拍摄设置监听 - 响应用户的操作(如按下按钮)启动图像或视频捕获。
  • 拍摄并保存文件 - 拍摄照片或视频,并保存为文件。
  • 释放相机 - 相机使用完毕后,必须正确释放以供其他应用程序使用。

注意:当我们的应用程序完成使用 Camera 后,一定要记住通过调用Camera.release()来释放Camera对象!如果我们的应用程序未正确释放相机,则所有后续尝试访问相机的操作(包括我们自己的应用程序)都将失败,并可能导致我们的程序或其他应用程序关闭。

相机硬件是一个共享资源,必须小心管理,以便我们的应用程序不会与其他可能需要使用相机的应用程序发生冲突。下面将介绍如何检测相机硬件,如何请求访问相机,如何捕获图片或视频以及如何在相机使用结束后释放相机。

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;
    }
}

现在的Android设备基本都有多个摄像头,例如用于摄影的后置摄像头和用于视频通话的前置摄像头。 Android 2.3(API等级9)及更高版本的系统允许我们使用Camera.getNumberOfCameras()方法检查设备上可用的摄像机数量。

2. 访问相机

如果已经确定运行应用程序的设备有相机,则必须通过获取Camera的实例来请求访问它(除非我们使用 Intent 启动相机)。

要访问主摄像头,应该使用Camera.open()方法,并确保捕获任何异常,如以下代码所示:

/** A safe way to get an instance of the Camera object. */
public static Camera getCameraInstance(){
    Camera camera = 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 camera ; // returns null if camera is unavailable
}

注意:使用Camera.open()时,必需检查异常情况。如果相机正在使用或不存在,且没有检查异常将导致我们的应用程序被系统关闭。

在运行Android 2.3(API Level 9)或更高版本的设备上,我们可以使用Camera.open(int)访问特定的摄像机。上面的示例代码将访问具有多个摄像头的设备上的第一个后置摄像头。

获取摄像机的权限后,我们可以使用Camera.getParameters()方法获取有关其功能的更多信息,并检查返回的Camera.Parameters对象以获得支持的功能。当使用API​​ Level 9或更高版本时,可以使用Camera.getCameraInfo()来确定当前相机是在设备的正面还是背面,以及图像的方向。

3. 创建相机预览控件

为了让用户有效地拍照或录像,他们必须能够看到设备的相机所“看”到的内容。相机预览控件是可以显示来自相机的实时图像数据的SurfaceView,因此用户可以对图片或视频进行捕获。

以下示例代码演示了如何创建可以包含在View布局中的基本相机预览类。该类实现了SurfaceHolder.Callback接口,以监听创建和销毁视图的回调事件。

public class CameraPreview extends ViewGroup implements SurfaceHolder.Callback {

    private SurfaceView mSurfaceView;
    private SurfaceHolder mHolder;
    private Camera mCamera;

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

        mSurfaceView = new SurfaceView(context);
        addView(mSurfaceView);

        mHolder = mSurfaceView.getHolder();
        mHolder.addCallback(this);
        // 已弃用的设置,但在3.0之前的Android版本上需要此设置
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // Surface 已创建,现在告诉相机在哪里绘制预览。
        try {
             mCamera.setPreviewDisplay(holder);
             mCamera.startPreview();
        } catch (IOException e) {
             Log.e(TAG, "Error setting camera preview: " + e.getMessage());
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            // 如果允许预览可以更改或旋转,可以在这里处理这些事件
            // 但是要确保在调整大小或重新格式化之前停止预览。
            if (mHolder.getSurface == null) {
                return;
            }

            // 在进行更改之前停止预览
            try {
                mCamera.stopPreview();
            } catch (Exception e) {
                // 忽略:试图停止不存在的预览
            }

            ```
            可以设置调整预览为任意大小,旋转或在这里重新格式化变更
            如果要设置相机预览的特定尺寸,必须使用 getSupportedPreviewSizes() 中的值。
            不要在setPreviewSize() 方法中设置任意值。
            ```

            // 用新设置开始预览
            try {
                mCamera.setPreviewDisplay(mHolder);
                mCamera.startPreview();
            } catch (Exception e) {
                Log.d(TAG, "Error starting camera preview: " + e.getMessage());
            }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
          // 不需要实现。但需要注意在我们的 Activity 中释放相机预览。
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    }
}

提示:随着 Android 7.0(API级别24)及更高版本中的多窗口功能的引入,即使在调用 setDisplayOrientation()之后,我们也不能再假设预览的宽高比与我们的 activity 相同。

4. 将预览控件放置到布局中

现在预览控件已经创建好了,我们要想拍摄照片或视频,就必须将相机预览控件(如上一节所示的示例)与其他用户界面控件结合在一起放在 Activity 的布局中。本节介绍如何为预览构建基本布局和活动。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

  <packageName.CameraPreview
    android:id="@+id/camera_preview"
    android:layout_width="match_parent"
    android:layout_height="match_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>

现在可以将创建好的布局设置到相机视图的 Activity 中。此外还必须确保在暂停或关闭相机时释放相机。

public class CameraActivity extends Activity {

    private Camera mCamera;
    private CameraPreview mPreview;

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

        mPreview = (CameraPreview) findViewById(R.id.camera_preview);

        // 创建一个 Camera 实例
        mCamera = getCameraInstance();
        if (mCamera != null) {
            mPreview.setCamera(mCamera);
        }
    }
}

Ok,现在可以运行我们的程序,看看预览效果了。


图片1

咦?!为什么所有的东西都是旋转了90度呢?(并不是我故意把手机旋转的)其实这是因为在大多数设备上,相机预览的默认方向为横向,所以我们看到预览会感觉很别扭。在Android 2.2(API 8级)之前,相机预览必须处于横向模式,而从Android 2.2(API 8级)开始,可以使用 Camera.setDisplayOrientation()方法旋转预览图像。为了在用户更改手机的方向时更改预览方向,在预览类的 surfaceChanged()方法中,首先使用Camera.stopPreview()停止预览,然后再次使用Camera.startPreview( ) 重新开始预览。

既然原因找到了,那就解决一下吧。

因为Android 2.2(API 8级)之前,相机预览必须处于横向模式,所以我们可以通过将用来做相机预览的 Activity 的方向更改为横向,方法是将以下内容添加到清单中。

<activity android:name=".CameraActivity"
          android:screenOrientation="landscape">
</activity>

而从Android 2.2(API 8级)开始,可以使用 Camera.setDisplayOrientation()方法旋转预览图像。我们可以将Activity代码修改为:

public class CameraActivity extends Activity {

    private Camera mCamera;
    private CameraPreview mPreview;

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

        mPreview = (CameraPreview) findViewById(R.id.camera_preview);
        // 创建一个 Camera 实例
        mCamera = getCameraInstance();

        if (mCamera != null) {
            // 旋转方向
            mCamera.setDisplayOrientation(90);
            mPreview.setCamera(mCamera);
        }
    }
}

Ok,我们再运行一下,看看效果。

图2

哈哈,这次方向对了吧,可这只是预览,你一定迫不及待的想要拍下属于你自己的相机的第一张图片吧,不要着急,接下来就拍照。

5. 拍摄照片或视频

终于到了激动人心的时刻了,不管你激不激动,反正我是激动了。

现在预览控件和一个视图布局都已经创建好了,马上就可以开始使用应用程序捕获图像和视频了。在代码中,我们必须设置用户界面控件的监听器,以便响应用户操作。

5.1 拍摄照片

要想拍摄图片,我们可以使用Camera.takePicture()方法,该方法需要接收三个参数。为了以 JPEG 格式接收数据,我们必须实现一个 Camera.PictureCallback 接口来接收图像数据并将其写入文件。下面的代码就是一个实现 Camera.PictureCallback 接口,来保存从相机接收到的图像的示例 。

private PictureCallback mPictureCallBack = new PictureCallback() {

    @Override
    public void onPictureTaken(byte[] data, Camera camera) {

        File pictureFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);
        if (pictureFile == null){
            Log.d(TAG, "Error creating media file, check storage permissions: " +
                e.getMessage());
            return;
        }
        try {
            FileOutputStream fos = new FileOutputStream(pictureFile);
            fos.write(data);
            fos.close();
        } catch (FileNotFoundException e) {
            Log.d(TAG, "File not found: " + e.getMessage());
        } catch (IOException e) {
            Log.d(TAG, "Error accessing file: " + e.getMessage());
        }
    }
};

如果想在拍照结束后处理图片,如下:

File pictureFile = null;
try {
     pictureFile = getOutPutMediaFile();
} catch (IOException e) {
     e.printStackTrace();
}

if (pictureFile == null) {
    CustomToast.show("图片保存路径不存在");
    return;
}

Bitmap resource = BitmapFactory.decodeByteArray(data, 0, data.length);
Matrix matrix = new Matrix();
// 拍出来的照片默认是横向的
matrix.setRotate(90f);

// 裁剪
Bitmap bitmap = Bitmap.createBitmap(resource, startX, startY, newWidth, newHeight, matrix, false);
resource.recycle();

// 缩放 1.5 倍
float scale = 1.5f;
Matrix matrix2 = new Matrix();
matrix2.setScale(scale, scale);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix2, false);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream);

FileOutputStream fileOutputStream = null;
try {
    fileOutputStream = new FileOutputStream(pictureFile);
    fileOutputStream.write(byteArrayOutputStream.toByteArray());
    fileOutputStream.close();
    bitmap.recycle();
    resource.recycle();
} catch (IOException e) {
     e.printStackTrace();
} finally {
    if (fileOutputStream != null) {
try {
    fileOutputStream.close();
} catch (IOException e) {
     e.printStackTrace();
}
public File getOutPutMediaFile() throws IOException {
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        String imageName = "JPEG_" + timeStamp + "_";
        File storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);

        return File.createTempFile(
                imageName,
                ".jpg",
                storageDir
        );
    }

通过调用Camera.takePicture()方法捕获图像。以下示例代码显示了如何通过按钮的 View.OnClickListener 调用此方法。

// 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, mPictureCallBack);
        }
    }
);

这里 takePicture() 方法中的第一个参数传入 null,效果是按下快门键的时候无任何声音;而如果传入一个 Camera.ShutterCallback 接口的实现类,就会默认有“咔擦”一声,如下。

mCamera.takePicture(new Camera.ShutterCallback() {
       @Override
       public void onShutter() {
       }
}, null, mPictureCallback);

再次警告:当我们的应用程序使用相机结束时,一定要记住通过调用Camera.release()来释放Camera对象!有关如何释放相机的具体操作,后面会介绍。

5.2 拍摄视频

使用Android框架进行视频捕获需要仔细管理Camera对象并与 MediaRecorder 类进行协调。使用Camera录制视频时,除调用 Camera.open()和Camera.release()之外,还必须管理 Camera.lock()和Camera.unlock()的调用以允许MediaRecorder访问摄像机硬件。

注意:从Android 4.0(API级别14)开始,Camera.lock()和 Camera.unlock()调用将自动进行管理。

与使用设备摄像头拍摄照片不同,拍摄视频需要非常特别的调用顺序。我们必须按照特定的执行顺序才能成功捕获视频,具体步骤如下。

  1. 打开相机 - 使用Camera.open()获取相机对象的实例。

  2. 连接预览 - 使用Camera.setPreviewDisplay()将SurfaceView 连接到摄像机,准备实时摄像机图像预览。

  3. 开始预览 - 调用Camera.startPreview()开始显示实时摄像头图像。

  4. 开始录制视频 - 为了成功录制视频,必须完成以下步骤:
    a. 解锁相机 - 通过调用Camera.unlock()解锁相机以供MediaRecorder使用。
    b. 配置MediaRecorder - 按以下顺序调用下面列出的MediaRecorder的方法。有关详细信息,请参阅 MediaRecorder
    参考文档。

    1. setCamera() - 使用应用程序当前的Camera实例,并将其设置为用于视频捕获。
    2. setAudioSource() - 使用MediaRecorder.AudioSource.CAMCORDER,设置音频源。
    3. setVideoSource() - 设置视频源,使用MediaRecorder.VideoSource.CAMERA。
    4. 设置视频输出格式和编码 - 对于Android 2.2(API 8)及更高版本,可以使用MediaRecorder.setProfile方法,并使用CamcorderProfile.get()获取配置文件实例。
    5. setOutputFile() - 设置输出文件,使用getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()方法,后面会介绍。
    6. setPreviewDisplay() - 使用我们为连接预览指定的相同对象,为应用程序指定 SurfaceView 预览布局元素。

    注意:我们必须按此顺序调用这些MediaRecorder配置方法,否则应用程序将遇到错误,并且录制将失败。

    c. **准备MediaRecorder **- 通过调用MediaRecorder.prepare(),为已提供的配置设置准备MediaRecorder。
    d. 启动MediaRecorder - 通过调用MediaRecorder.start()开始录制视频。

  5. 停止录制视频 - 按顺序调用以下方法,以成功完成录像:
    a. 停止MediaRecorder - 通过调用MediaRecorder.stop()停止录制视频。
    b. 重新设置MediaRecorder(可选) - 通过调用MediaRecorder.reset()从录像机中删除配置设置。
    c. **释放MediaRecorder ** - 通过调用MediaRecorder.release()来释放MediaRecorder。
    d. 锁定相机 - 锁定相机,以便将来的MediaRecorder会话可以通过调用Camera.lock()来使用它。从Android 4.0(API级别14)开始,除非MediaRecorder.prepare()调用失败,否则不需要调用它。

  6. 停止预览 - 当我们的页面完成使用相机后,要使用Camera.stopPreview()停止预览。

  7. 释放相机 - 释放相机,以便其他应用程序可以通过调用Camera.release()来使用相机。

提示:可以先使用MediaRecorder创建摄像机预览,并跳过此过程的前几个步骤。然而,由于我们通常更喜欢在开始录制之前便看到预览,所以这里就不讨论该过程。

技巧:如果我们的应用程序主要用于录制视频,可以在启动预览之前将setRecordingHint(boolean)设置为true。此设置可以帮助我们减少开始录制所需的时间。

5.2.1 配置 MediaRecorder

使用MediaRecorder录制视频时,必须按照特定顺序执行配置步骤,然后调用MediaRecorder.prepare()方法来检查和实现配置。
以下示例代码演示如何正确配置和准备MediaRecorder进行视频录制。

private boolean prepareVideoRecorder(){

    mCamera = getCameraInstance();
    mMediaRecorder = new MediaRecorder();

    // Step 1: Unlock and set camera to MediaRecorder
    mCamera.unlock();
    mMediaRecorder.setCamera(mCamera);

    // Step 2: Set sources
    mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
    mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

    // Step 3: Set a CamcorderProfile (requires API Level 8 or higher)
    mMediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));

    // Step 4: Set output file
    mMediaRecorder.setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString());

    // Step 5: Set the preview output
    mMediaRecorder.setPreviewDisplay(mPreview.getHolder().getSurface());

    // Step 6: Prepare configured MediaRecorder
    try {
        mMediaRecorder.prepare();
    } catch (IllegalStateException e) {
        Log.d(TAG, "IllegalStateException preparing MediaRecorder: " + e.getMessage());
        releaseMediaRecorder();
        return false;
    } catch (IOException e) {
        Log.d(TAG, "IOException preparing MediaRecorder: " + e.getMessage());
        releaseMediaRecorder();
        return false;
    }
    return true;
}

提示:这是假设我们的应用不支持 Android 2.2(API 8)之前的设备,因为 Android 2.2 之前的配置方法不同,具体方法可以到官网查看。

MediaRecorder的以下视频录制参数被赋予默认设置,我们可以通过调用以下方法调整应用程序的这些设置:

5.2.2 启动和停止MediaRecorder

使用MediaRecorder类启动和停止视频录制时,必须遵循以下列出的特定顺序。

  1. 用Camera.unlock()解锁相机
  2. 配置MediaRecorder,如上已节的代码示例所示
  3. 使用MediaRecorder.start()开始录制
  4. 录制视频
  5. 使用MediaRecorder.stop()停止录制
  6. 用MediaRecorder.release()释放媒体录音机
  7. 使用Camera.lock()锁定相机

以下示例代码演示如何使用相机和MediaRecorder 连接按钮以正确启动和停止视频录制。

注意:和拍照不同的是,完成录像时不要释放相机,否则将停止预览。

private boolean isRecording = false;

// 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) {
            if (isRecording) {
                // stop recording and release camera
                mMediaRecorder.stop();  // stop the recording
                releaseMediaRecorder(); // release the MediaRecorder object
                mCamera.lock();         // take camera access back from MediaRecorder

                // inform the user that recording has stopped
                setCaptureButtonText("Capture");
                isRecording = false;
            } else {
                // initialize video camera
                if (prepareVideoRecorder()) {
                    // Camera is available and unlocked, MediaRecorder is prepared,
                    // now you can start recording
                    mMediaRecorder.start();

                    // inform the user that recording has started
                    setCaptureButtonText("Stop");
                    isRecording = true;
                } else {
                    // prepare didn't work, release the camera
                    releaseMediaRecorder();
                    // inform user
                }
            }
        }
    }
);

注意:在上面的示例中,prepareVideoRecorder()方法指的是配置MediaRecorder 中的示例代码。此方法负责锁定相机,配置和准备MediaRecorder实例。

6. 释放相机

相机是设备上的所有应用程序共享的硬件资源。获取相机实例后,我们的应用程序可以使用相机,当应用程序停止使用和应用程序暂停(Activity.onPause())时,必须特别注意释放相机对象。如果我们的应用程序没有正确地释放相机,则所有后续访问相机的尝试(包括我们自己的应用程序)都将失败,并可能导致我们的应用或其他应用程序关闭。

要释放Camera对象的实例,应该使用Camera.release()方法,如下面的示例代码所示。

public class CameraActivity extends Activity {
    private Camera mCamera;
    private SurfaceView mPreview;
    private MediaRecorder mMediaRecorder;

    ...

    @Override
    protected void onPause() {
        super.onPause();
        releaseMediaRecorder();       // if you are using MediaRecorder, release it first
        releaseCamera();              // release the camera immediately on pause event
    }

    private void releaseMediaRecorder(){
        if (mMediaRecorder != null) {
            mMediaRecorder.reset();   // clear recorder configuration
            mMediaRecorder.release(); // release the recorder object
            mMediaRecorder = null;
            mCamera.lock();           // lock camera for later use
        }
    }

    private void releaseCamera(){
        if (mCamera != null){
            mCamera.release();        // release the camera for other applications
            mCamera = null;
        }
    }
}

7. 保存文件

用户创建的媒体文件(如图片和视频)应保存在设备的外部存储目录(SD卡)中,以节省系统空间,并允许用户在没有设备的情况下访问这些文件。有许多可能的目录位置来保存设备上的媒体文件,但是作为开发人员我们只能选择两个标准位置:

  • Environment.getExternalStoragePublicDirectory - 此方法返回的是Android系统推荐的用来保存图片和视频的标准的位置。该目录是共享的(public),所以其他应用程序可以轻松地读取,更改和删除保存在此位置的文件。如果我们的应用程序被用户卸载,保存到此位置的媒体文件将不会被删除。为了避免干扰用户现有的图片和视频,我们应该在该目录中为我们的应用程序创建一个子目录,如下面的代码示例所示。

  • Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) - 此方法返回的是用于保存与应用程序相关联的图片和视频的标准位置。如果我们的应用程序被卸载,则保存在此位置的所有文件都将被删除。保存在该位置的文件其他应用程序也可以读取,更改和删除。

以下示例代码演示如何为媒体文件创建一个文件或 Uri 位置,以便在使用Intent或调用设备的相机时使用。

public static final int MEDIA_TYPE_IMAGE = 1;
public static final int MEDIA_TYPE_VIDEO = 2;

/** Create a file Uri for saving an image or video */
private static Uri getOutputMediaFileUri(int type){
      return Uri.fromFile(getOutputMediaFile(type));
}

/** Create a File for saving an image or video */
private static File getOutputMediaFile(int type){
    // To be safe, you should check that the SDCard is mounted
    // using Environment.getExternalStorageState() before doing this.

    File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
              Environment.DIRECTORY_PICTURES), "MyCameraApp");
    // This location works best if you want the created images to be shared
    // between applications and persist after your app has been uninstalled.

    // Create the storage directory if it does not exist
    if (! mediaStorageDir.exists()){
        if (! mediaStorageDir.mkdirs()){
            Log.d("MyCameraApp", "failed to create directory");
            return null;
        }
    }

    // Create a media file name
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
    File mediaFile;
    if (type == MEDIA_TYPE_IMAGE){
        mediaFile = new File(mediaStorageDir.getPath() + File.separator +
        "IMG_"+ timeStamp + ".jpg");
    } else if(type == MEDIA_TYPE_VIDEO) {
        mediaFile = new File(mediaStorageDir.getPath() + File.separator +
        "VID_"+ timeStamp + ".mp4");
    } else {
        return null;
    }

    return mediaFile;
}

注意:Environment.getExternalStoragePublicDirectory()在Android 2.2(API 8级)或更高版本中可用。如果使用较早版本的Android定位设备,应改用Environment.getExternalStorageDirectory()。有关详细信息,请参阅保存共享文件。

要使URI支持工作配置文件,首先将 file URI转换为 content URI。然后,将 content URI添加到Intent的EXTRA_OUTPUT中。

相机功能

Android支持各种相机功能,我们的应用程序可对相机进行控制,如图片格式,闪光模式,对焦设置等等。本节列出了常见的相机功能,并简要的讨论如何使用它们。通过Camera.Parameters对象可以访问和设置大多数摄像头功能。但是,有几个重要的功能需要在Camera.Parameters中的简单设置。以下部分将介绍这些功能:

  • 测量和聚焦
  • 面部识别
  • 长时间视频

有关如何使用通过Camera.Parameters控制的功能的信息,后面会进行详细介绍。有关如何使用通过摄像机参数对象控制的功能的更多详细信息,可以参考下面的功能列表中的链接到API参考文档。

Feature API Level Description
人脸检测 14 识别图片中的人脸,并将其用于对焦,测光和白平衡
测量区域 14 指定图像中的一个或多个区域以计算白平衡
聚焦区域 14 设置图像中的一个或多个区域以用于焦点
白平衡锁 14 停止或开始自动白平衡调整
曝光锁 14 停止或开始自动曝光调整
视频快照 14 拍摄视频时拍摄照片(框架抓取)
Time Lapse Video 11 记录设置延迟的帧以记录时间延迟视频
多个相机 9 支持设备上的多台摄像机,包括前置摄像头和后置摄像头
聚焦距离 9 报告相机和对象之间的距离
缩放 8 设置图像的缩放比例
曝光补偿 8 增加或减少曝光量
GPS数据 5 包含或省略地理位置数据与图像
白平衡 5 设置白平衡模式,影响拍摄图像中的颜色值
对焦模式 5 设置相机如何对焦于诸如自动,固定,宏或无限远的主题
场景模式 5 对特定类型的摄影情况(例如夜间,海滩,雪景或烛光场景)应用预设模式
JPEG质量 5 设置JPEG图像的压缩级别,可增加或减少图像输出文件的质量和大小
闪光模式 5 打开,关闭闪光灯,或使用自动设置
颜色效果 5 对拍摄的图像应用颜色效果,如黑白色,棕褐色调或阴性。
反捆扎 5 由于JPEG压缩,降低了色彩渐变的效果
Picture Format 1 指定图片的文件格式
图片尺寸 1 指定保存图片的像素尺寸

注意:由于硬件差异和软件实现不同,并非所有设备都支持这些功能。有关检查应用程序运行的设备上功能的可用性的下一节会介绍。

检查功能可用性

在Android设备上设置使用相机功能时,首先要了解的是,并非所有设备都支持所有相机功能。此外,支持特定功能的设备可能会将其支持到不同的级别或不同的选项。因此,我们要决定使用哪些相机功能以及支持到什么程序。做出决定后,我们应该先检查设备硬件是否支持这些功能,如果功能不可用,给用户提示。

我们可以通过获取相机参数对象的实例并检查相关方法来检查相机功能的可用性。以下代码示例显示如何获取Camera.Parameters对象并检查相机是否支持自动对焦功能:

// get Camera parameters
Camera.Parameters params = mCamera.getParameters();

List<String> focusModes = params.getSupportedFocusModes();
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
  // Autofocus mode is supported
}

我们可以使用上面的方法来获得大多数相机功能。 Camera.Parameters对象提供了一个getSupported ...(),is... Supported()或getMax ...()方法来确定是否(以及在什么程度上)功能被支持。如果我们的应用程序需要某些相机功能才能正常运行,则可以通过添加应用程序清单来要求它们。当我们声明使用特定的相机功能(例如闪光灯和自动对焦)时,Google Play将应用程序限制为不支持这些功能的设备上。有关可以在应用程序清单中声明的​​摄像头功能的列表,请参阅清单功能的 API Features Reference

使用相机功能

大多数相机功能都是使用Camera.Parameters对象进行激活和控制的。首先我们可以通过获取Camera对象的实例,调用getParameters()方法,更改返回的参数对象,然后将其设置回相机对象获取的此对象,如以下示例代码所示

// get Camera parameters
Camera.Parameters params = mCamera.getParameters();
// set the focus mode
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
// set Camera parameters
mCamera.setParameters(params);

这种方法适用于几乎所有的相机功能,大多数参数可以在获取Camera对象的实例后随时更改。在应用程序的相机预览中,用户对参数的更改通常是可见的。在软件方面,参数更改可能需要几帧才能实际生效,因为相机硬件处理新指令,然后发送更新的图像数据。

重要提示:某些相机功能无法随意更改。特别是更改相机预览的大小或方向需要先停止预览,更改预览大小,然后重新启动预览。从Android 4.0(API级别14)开始,预览方向可以更改,无需重新启动预览。

其他相机功能需要更多的代码才能实现,其中包括:

  • 测量和聚焦
  • 面部识别
  • 长时间视频
    以下部分提供了如何实现这些功能的快速概述。

计量和重点领域

在某些摄影场景中,自动对焦和测光可能无法产生预期的效果。从Android 4.0(API 14级)开始,相机应用程序可以提供额外的控件,允许我们的应用或用户指定图像中用于确定焦点或光线级别设置的区域,并将这些值传递到相机硬件以用于捕获图像或视频。

用于测光和对焦的区域与其他摄像机功能非常相似,我们可以通过Camera.Parameters对象中的方法来控制它们。以下代码演示了为相机实例设置两个测光区域:

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

// set Camera parameters
Camera.Parameters params = mCamera.getParameters();

if (params.getMaxNumMeteringAreas() > 0){ // check that metering areas are supported
    List<Camera.Area> meteringAreas = new ArrayList<Camera.Area>();

    Rect areaRect1 = new Rect(-100, -100, 100, 100);    // specify an area in center of image
    meteringAreas.add(new Camera.Area(areaRect1, 600)); // set weight to 60%
    Rect areaRect2 = new Rect(800, -1000, 1000, -800);  // specify an area in upper right of image
    meteringAreas.add(new Camera.Area(areaRect2, 400)); // set weight to 40%
    params.setMeteringAreas(meteringAreas);
}

mCamera.setParameters(params);

Camera.Area对象包含两个数据参数:分别是用于指定相机视野内的区域的Rect对象和权重值,该值指示相机在光测量或对焦计算中应该给出该区域的重要程度。

Camera.Area 对象中的Rect字段描述了映射到2000 x 2000单位格网格的矩形。坐标-1000,-1000表示相机图像的左上角,坐标1000,1000表示相机图像的右下角,如下图所示。

camera-area-coordinates.png(图片来自 Android 官网)

图中红线表示在相机预览中指定Camera.Area的坐标系。蓝色框显示相机区域的位置和形状,其中Rect值为333,333,667,667。

该坐标系的边界总是对应于在相机预览中可见的图像的外边缘,并且不随着缩放级别而缩小或扩大。同样地,使用Camera.setDisplayOrientation()的图像预览的旋转不会重新映射坐标系。

面部检测

对于包含人物的图片,脸部通常是图片中最重要的部分,并且应在捕获图像时用于确定焦点和白平衡。Android 4.0(API Level 14)框架提供用于识别人脸和使用脸部识别技术计算图像设置的API。

注意:当脸部检测功能正在运行时,setWhiteBalance(String),setFocusAreas(List <Camera.Area>)和setMeteringAreas(List <Camera.Area>)没有任何作用。

使用相机应用程序中的人脸检测功能需要几个常规步骤:

  • 检查设备是否支持人脸检测
  • 创建一个面部检测监听器
  • 将面部检测侦听器添加到您的相机对象
  • 预览后启动脸部检测(每次重启预览后)

并不是所有设备都支持人脸检测功能。您可以通过调用getMaxNumDetectedFaces()来检查此功能是否受支持。此检查的示例显示在下面的startFaceDetection()示例方法中。

为了得到通知并对脸部的检测做出响应,您的相机应用程序必须设置一个用于人脸检测事件的侦听器。为此,您必须创建一个监听器类,实现Camera.FaceDetectionListener接口,如下面的示例代码所示。

class MyFaceDetectionListener implements Camera.FaceDetectionListener {

    @Override
    public void onFaceDetection(Face[] faces, Camera camera) {
        if (faces.length > 0){
            Log.d("FaceDetection", "face detected: "+ faces.length +
                    " Face 1 Location X: " + faces[0].rect.centerX() +
                    "Y: " + faces[0].rect.centerY() );
        }
    }
}

创建此类之后,然后将其设置到应用程序的Camera对象中,如下面的示例代码所示:

mCamera.setFaceDetectionListener(new MyFaceDetectionListener());

每次启动(或重启)相机预览时,应用程序都必须启动人脸检测功能。创建一种开始面部检测的方法,以便您可以根据需要调用它,如下面的示例代码所示。

public void startFaceDetection(){
    // Try starting Face Detection
    Camera.Parameters params = mCamera.getParameters();

    // start face detection only *after* preview has started
    if (params.getMaxNumDetectedFaces() > 0){
        // camera supports face detection, so can start it:
        mCamera.startFaceDetection();
    }
}

每次启动(或重新启动)相机预览时,都必须开始面部检测。如果使用“创建预览”类中显示的预览类,请在预览类中将startFaceDetection()方法添加到surfaceCreated()和surfaceChanged()方法,如下面的示例代码所示。

public void surfaceCreated(SurfaceHolder holder) {
    try {
        mCamera.setPreviewDisplay(holder);
        mCamera.startPreview();

        startFaceDetection(); // start face detection feature

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

public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {

    if (mHolder.getSurface() == null){
        // preview surface does not exist
        Log.d(TAG, "mHolder.getSurface() == null");
        return;
    }

    try {
        mCamera.stopPreview();

    } catch (Exception e){
        // ignore: tried to stop a non-existent preview
        Log.d(TAG, "Error stopping camera preview: " + e.getMessage());
    }

    try {
        mCamera.setPreviewDisplay(mHolder);
        mCamera.startPreview();

        startFaceDetection(); // re-start face detection feature

    } catch (Exception e){
        // ignore: tried to stop a non-existent preview
        Log.d(TAG, "Error starting camera preview: " + e.getMessage());
    }
}

提示:调用startPreview()后,不要忘记调用这个方法。不要在相机应用程序的主要活动的onCreate()方法中尝试开始人脸检测,因为在应用程序的执行过程中预览不可用。

Time lapse video

时间延迟视频允许用户创建视频剪辑,组合几秒或几分钟之间拍摄的照片。此功能使用MediaRecorder记录时间序列的图像。

要使用MediaRecorder录制时间延迟视频,您必须配置录像机对象,就像录制正常视频一样,将每秒捕获的帧数设置为低数字,并使用其中一个时间间隔质量设置,如代码示例所示下面。

// Step 3: Set a CamcorderProfile (requires API Level 8 or higher)
mMediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_TIME_LAPSE_HIGH));
...
// Step 5.5: Set the video capture rate to a low number
mMediaRecorder.setCaptureRate(0.1); // capture a frame every 10 seconds

这些设置必须作为MediaRecorder的较大配置过程的一部分完成。有关完整配置代码示例,请参阅Configuring MediaRecorder。配置完成后,您开始录像,就像录制正常视频剪辑一样。有关配置和运行MediaRecorder的更多信息,请参阅捕获视频。

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容