Android 多媒体之Camera

Camera顾名思义,就是照相机、摄像机的意思。在Android中使用这个类可以做拍照和录像的功能。但是在Android5.0中这个类已经不推荐使用了,5.0之后使用更强大的Camera2替换他。在这篇文章中我们使用的是Camera,虽然已经不推荐使用了,但是原理都差不多,Camera会用了,相信Camera2肯定也不会太难。本文主要介绍Camera的简单使用以及使用过程中一些常见的问题,绝对的干货,也主要是针对刚学这一块的同学。

Android中关于相机的实现,是基于SurfaceView的,如果还有不知道SurfaceView的请查看这篇文章Android中SurfaceView的使用详解 ,不知道SurfaceView的务必要看一下那篇文章。
我相信初学者都不喜欢看太过的理论知识,如果有一个小Demo再配几幅图是再好不过了,所以本文就是基于一个小Demo进行讲解的。

还是先来看一下最后的效果是怎么样的。


CameraDemo

从这幅图中可以看到在右上角有一个切换摄像头的按钮,在左下角有一个图片框,这里主要是用来展示拍照之后得到的图片,并且点击这个按钮会进入自定义的图库可进行照片的选择,界面如下图


CameraDemo

中间即是拍照的按钮了,右下角是进入到录像的界面(PS:本文没去讲解录像功能和图库选择的实现,但是在文章最后给出的源码中有实现,感兴趣的小伙伴可以去下载,如果发现有问题欢迎提出)
好了,到现在基本上就只知道下面要干什么了,我们正式开始吧!

我们来看看这个主界面的布局文件:

<pre>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/FrameLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:background="#282828"
    android:paddingRight="20dp">

    <ImageView
        android:layout_width="35dp"
        android:layout_height="35dp"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:onClick="changeCameraFacing"
        android:src="@drawable/icon_camear_change" />
</RelativeLayout>

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:orientation="vertical">

    <ren.solid.camerademo.view.CameraSurfaceView
        android:id="@+id/cameraSurfaceView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

<RelativeLayout
    android:id="@+id/rl_bottom"
    android:layout_width="match_parent"
    android:layout_height="80dp"
    android:background="#282828"
    android:padding="10dp">
    <!-- 拍照按钮 -->
    <ImageView
        android:id="@+id/takepicture"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_centerInParent="true"
        android:onClick="btnOnclick"
        android:src="@drawable/icon_take_photo" />

    <ImageView
        android:id="@+id/iv_photo"
        android:layout_width="45dp"
        android:layout_height="45dp"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:background="@drawable/bg_photo_done"
        android:onClick="openPhotoAlbum" />

    <ImageView
        android:id="@+id/iv_switch"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:onClick="ivSwitchClick"
        android:background="@drawable/icon_pushmsg_video" />
</RelativeLayout>

</LinearLayout>
</pre>

细心小伙伴肯定注意到这几行代码了<pre><ren.solid.camerademo.view.CameraSurfaceView
android:id="@+id/cameraSurfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent" /></pre>从代码中我们可以看到我们是使用的一个自定义的CameraSurfaceView,现在我们就来看看这个CameraSurfaceView是怎么实现的(PS:其实本文的核心东西就是这个自定义SurfaceView)。
看下面红框里面的内容,可以看出CameraSurfaceView是继承于SurfaceView实现的,说白了CameraSurfaceView就是对SurfaceView与Camera的封装。


这里实现了SurfaceView类的三个构造方法,并且前两个构造方法最后都指向有三个参数的,这样写的好处就是不用在三个构造方法中都去写一次init()方法(已经知道的无视这句)。下载我们再来看看init()方法中都做了些什么

第一句就是得到摄像头的数量,可以用这个去判断是否有前置摄像头。getCamera()是打开系统摄像头实现如下图


这里面最核心的就是mSurfaceHolder.addCallback(new SurfaceCallback())这一句。为SurfaceHolder添加一个回调。各种功能的切换就是在这个回调里面实现的,下面我们来看看SurfaceCallback这个类的实现

这个类中重写了三个方法
1.surfaceChanged:拍照状态改变时调用,也就是你的手机竖屏和横屏相互切换时会调用这个方法,我试过当手机横屏的时候拍照,效果十分不好(拉伸十分严重),用了很多方法也没解决这个问题,我也看了下我系统自带的相机,也没有在横屏下拍照的功能,所以我还是建议最好把拍照界面固定为竖屏,在竖屏状态下拍照。
2.surfaceCreated:就是当surface被创建的时候调用。在这里我们就在surface被创建的时候就开始拍照的预览。我们再来看看startPreview方法是怎么实现的。

从图中可以看到我们调用了camera的三个api,至于作用,注释写的很清楚了,这里我就不再重复了。
3.surfaceDestroyed:这里主要是在surface被销毁的时候做一些回收的工作。熟悉SurfaceView的同学肯定知道,当SurfaceView界面不可见时这个方法就会被调用。因为SurfaceView是很占CPU的。

最后我们再来看看是怎么拍照的吧


从图中我们可以看到这里提供了一个takePictue方法,在里面调用了camera的带有三个参数的takePicture方法,其中第一个参数是按下快门的回调,第二个参数是原始图片的回调,第三个参数是经压缩处理后比较小的jpeg图片的回调。

下面再来看看MyCameraPictureCallback的实现吧


其实这里简单,就是把拍照得到的照片保存到sd卡中去。
这里需要知道一点:当每次拍完照之后,相机都会自动释放资源,所以这里需要重新开启预览

对了我们再来看看切换摄像头是怎么实现的吧


到这里这个自定义的SurfaceView就差不多了。详细代码见最后。

最后我们再来看看这个类怎么使用吧


还是很简单吧,我们只需几句话就能搞定拍照了。以后哪里有需要的地方直接拿过去用就OK了!
这里一定要记得与Activity的生命周期同步,不然可以在界面切换的时候出现现一些错误。


附详细代码:
<pre>
/**

  • Created by _SOLID

  • Date:2016/3/23

  • Time:9:43

  • <p/>

  • <uses-permission android:name="android.permission.CAMERA" />

  • <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />

  • <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

  • <p/>

  • 注:应用程序同时只能存在一个激活的 camera

  • <p/>

  • 关于图片拉伸变形的问题,要把SurfaceView的宽高比例和图片的预览尺寸的宽高比例设置相同
    */
    public class CameraSurfaceView extends SurfaceView {

    private static String TAG = "CameraSurfaceViewTAG";
    private SurfaceHolder mSurfaceHolder;
    private Camera camera;
    private Camera.Parameters parameters = null;
    private Context mContext;
    private int mCameraCount;
    private int mCurrentCameraFacing = Camera.CameraInfo.CAMERA_FACING_BACK;
    private String mPictureSaveDir;
    private String mPictureSavePath;
    private OnSavePictureListener mOnSavePictureListener;

    public CameraSurfaceView(Context context) {
    this(context, null);
    }

    public CameraSurfaceView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public CameraSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mContext = context;
    init();
    }

private void init() {
    mCameraCount = Camera.getNumberOfCameras();//得到摄像头数量
    mSurfaceHolder = getHolder();
    getCamera();
    mSurfaceHolder.setKeepScreenOn(true);// 屏幕常亮
    mSurfaceHolder.addCallback(new SurfaceCallback());//为SurfaceView的Holder添加一个回调函数

    setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            if (camera != null) {
                camera.autoFocus(null);
            }
        }
    });

}

/***
 * 得到系统相机
 */
private void getCamera() {
    if (camera == null)
        camera = Camera.open(mCurrentCameraFacing); // 打开后置摄像头
}


private final class SurfaceCallback implements SurfaceHolder.Callback {
    // 拍照状态变化时调用该方法
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
                               int height) {
        Log.i(TAG, "surfaceChanged    " + "width:" + width + "|" + "height:" + height);
        if (camera != null) {
            camera.stopPreview();
            startPreview(holder);
            setCameraParameters(width, height);
        }
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Log.i(TAG, "surfaceCreated");
        startPreview(holder);
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        Log.i(TAG, "surfaceDestroyed");
        releaseCamera();
    }

}

/***
 * 切换相机摄像头
 */
public void changCameraFacing() {
    if (mCameraCount > 1) {
        mCurrentCameraFacing = (mCurrentCameraFacing == Camera.CameraInfo.CAMERA_FACING_BACK) ?
                Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK;
        releaseCamera();
        startPreview(mSurfaceHolder);
    } else {
        //手机不支持前置摄像头
    }
}

/***
 * 设置相机参数
 *
 * @param width
 * @param height
 */
private void setCameraParameters(int width, int height) {
    mSurfaceHolder.setFixedSize(width, height);//照片的大小
    parameters = camera.getParameters(); // 获取相机参数
    parameters.setPictureFormat(ImageFormat.JPEG); // 设置图片格式

    parameters.setPreviewSize(width, height); // 设置预览大小
    parameters.setPictureSize(width, height); // 设置保存的图片尺寸

    parameters.setPreviewFpsRange(4, 10);//fps
    parameters.setJpegQuality(100); // 设置照片质量
    parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);//自动对焦
    //parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);//连续对焦
    //camera.cancelAutoFocus();//如果要实现连续的自动对焦,这一句必须加上
    camera.setParameters(parameters);
}

/**
 * 开始预览
 */
private void startPreview(SurfaceHolder surfaceHolder) {
    getCamera();//防止Camera实例为空
    try {
        camera.setPreviewDisplay(surfaceHolder); // 设置用于显示预览的SurfaceHolder对象

// if (mContext.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE) {
// // parameters.set("orientation", "portrait");
// camera.setDisplayOrientation(90);//在2.2以上可以使用
// } else {
// // parameters.set("orientation", "landscape");
// camera.setDisplayOrientation(0);//在2.2以上可以使用
// }
camera.setDisplayOrientation(getPreviewDegree());//设置预览的旋转角度,这句很关键,不然预览拉伸很严重
camera.startPreview(); // 开始预览
} catch (Exception e) {
e.printStackTrace();
}
}

/**
 * 用于根据手机方向获得相机预览画面旋转的角度
 */
private int getPreviewDegree() {
    // 获得手机的方向
    WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    int rotation = windowManager.getDefaultDisplay()
            .getRotation();
    Log.i(TAG, "rotation:" + rotation);
    int degree = 0;
    // 根据手机的方向计算相机预览画面应该选择的角度
    switch (rotation) {
        case Surface.ROTATION_0:
            degree = 90;
            break;
        case Surface.ROTATION_90:
            degree = 0;
            break;
        case Surface.ROTATION_180:
            degree = 270;
            break;
        case Surface.ROTATION_270:
            degree = 180;
            break;
    }
    return degree;
}

/**
 * 将拍下来的照片存放在SD卡中
 *
 * @param data
 * @throws IOException
 */
private void saveToSDCard(byte[] data) throws IOException {
    Date date = new Date();
    SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss"); // 格式化时间
    String filename = format.format(date) + ".jpg";

    File fileFolder = new File(getPictureSaveDir());

    if (!fileFolder.exists()) { // 如果目录不存在,则创建一个名为"finger"的目录
        fileFolder.mkdir();
    }
    File jpgFile = new File(fileFolder, filename);
    FileOutputStream outputStream = new FileOutputStream(jpgFile); // 文件输出流

    //由于在预览的时候,我们调整了预览的方向,所以在保存的时候我们要旋转回来,不然保存的图片方向是不正确的
    Matrix matrix = new Matrix();
    if (mCurrentCameraFacing == Camera.CameraInfo.CAMERA_FACING_BACK) {
        matrix.setRotate(90);
    } else {
        matrix.setRotate(-90);
    }
    Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
    bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);

    outputStream.flush(); // 写入sd卡中
    outputStream.close(); // 关闭输出流
    mPictureSavePath = getPictureSaveDir() + File.separator + filename;
    if (mOnSavePictureListener != null) {
        mOnSavePictureListener.onSuccess(mPictureSavePath);
    }
    Toast.makeText(mContext.getApplicationContext(), "图片已保存至:" + mPictureSavePath,
            Toast.LENGTH_LONG).show();

    //这个的作用是让系统去扫描刚拍下的这个图片文件,以利于在MediaSore中能及时更新,
    // 可能会存在部分手机不用使用的情况(众所周知,现在国内的Rom厂商已把原生Rom改的面目全非)
    //mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + mPictureSavePath)));
    MediaScannerConnection.scanFile(mContext, new String[]{
                    mPictureSavePath},
            null, new MediaScannerConnection.OnScanCompletedListener() {
                public void onScanCompleted(String path, Uri uri) {
                    // Log.e(TAG, "扫描完成");
                }
            });


}


private final class MyCameraPictureCallback implements Camera.PictureCallback {
    @Override
    public void onPictureTaken(byte[] data, Camera camera) {
        try {
            saveToSDCard(data); // 保存图片到sd卡中
            camera.startPreview(); // 拍完照后,重新开始预览
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

/**
 * 拍照
 */
public void takePicture() {
    if (camera != null) {
        camera.takePicture(null, null, new MyCameraPictureCallback());//每次调用takePicture获取图像后,摄像头会停止预览
    } else {
        //TODO: 提示用户相机不存在
    }
}

/***
 * 释放相机资源
 */
public void releaseCamera() {
    if (camera != null) {
        camera.stopPreview();
        camera.release();
        camera = null;
    }
}

/**
 * 设置图片的保存路径
 *
 * @param pictureSavePath
 */
public void setPictureSavePath(String pictureSavePath) {
    mPictureSaveDir = pictureSavePath;
}

/***
 * 得到图片保存的目录
 *
 * @return
 */
public String getPictureSaveDir() {
    String path;
    if (mPictureSaveDir == null)
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
            path = Environment.getExternalStorageDirectory()
                    + "/SolidCamera/";
        else {
            path = mContext.getCacheDir().getAbsolutePath()
                    + "/SolidCamera/";
        }
    else {
        path = mPictureSaveDir;
    }
    mPictureSaveDir = path;
    return path;
}

public void onResume() {
    getCamera();
}

public void onPause() {
    releaseCamera();
}

/***
 * 设置拍照成功后的回调
 * @param onSavePictureListener
 */
public void setOnSavePictureListener(OnSavePictureListener onSavePictureListener) {
    mOnSavePictureListener = onSavePictureListener;
}

public interface OnSavePictureListener {
    void onSuccess(String filePath);
}

}
</pre>

其实录像和拍照其实差不多,只需添加一个MeadiaRecorder即可,关于录像源码中有。

源码下载

参考链接
使用Camera2 替代过时的Camera API
Android Camera多屏幕适配解决预览照片拉伸
Android Camera 使用小结
Android 相机自动对焦和定点对焦的一些总结
Android保存图片到图库,Android扫描文件到媒体库,Android保存图片到SD卡

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

推荐阅读更多精彩内容