首页 > 代码库 > 相机开发(聚焦、横竖屏拍照、照片存储、连续拍照等)
相机开发(聚焦、横竖屏拍照、照片存储、连续拍照等)
最近项目用到了相机拍照的功能,于是想着封装好一些通用性较好的相机调用,从百度和谷歌上查找出来的资料真的印证了“天下文章一大抄”的道理,而且它们实现的拍照功能大都存在缺陷,如聚焦问题、重复拍照问题、照片存储问题、横竖屏转换问题。一大堆的问题,而且程序的扩展性和可重用性实在不敢恭维,排版级其混乱。
最后无奈,打开API文档camera相机类,从最基础的学起,然后自己进行改进,从这里也告诉我们一个道理,API文档才是学习起点,因为它会告诉你整个实现的原理和原因,能够对整个框架有一个整体的了解,看完API文档看其他的就有事半功倍的效果,吐槽完毕,下面来正式实现。
一.实现流程
这幅图是从API文档(最好是看英文版的)整理出来的,从这副图上面我们可以看出,主要是有6步,其中难点是创建相机预览类。
二.权限声明
这个不讲了,直接加入声明权限代码,不明白的可以网上查查看
<uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-feature android:name="android.hardware.camera.autofocus" /> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
三 检查相机和获取相机实例
新建CameraCheck类,主要有2个方法,代码如下
public class CameraCheck { public static boolean CheckCamera(Context mContext) { if (mContext.getPackageManager().hasSystemFeature( PackageManager.FEATURE_CAMERA)) { return true; } else { Toast.makeText(mContext, "相机不存在!", Toast.LENGTH_SHORT).show(); return false; } } /** A safe way to get an instance of the Camera object. */ public static Camera getCameraInstance(Context mContext) { Camera c = null; if (CheckCamera(mContext)) { try { c = Camera.open(); } catch (Exception e) { c=null; } } return c; // returns null if camera is unavailable } }
第一个方法用来检查相机是否存在,这个方法是来自API文档,使用方法
mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)
hasSystemFeature(String name)方法返回设备是否支持name功能的真假值;
通过方法getCameraInstance返回相机的实例,通过调用该方法,mContext能够获得该相机资源,只有获得了该相机资源才能够对相机进行操作。
四.创建相机预览类(重点)
我们在拍照之前需要对取景进行预览,这里我们需要使用SurfaceView控件,关于SurfaceView控件我们先简单的了解一下(别急,磨刀不误砍柴工)。
SurfaceView是View的子类,所以它拥有View的一切方法和属性,这一点我们从命名上面就可以看出来,比如绘制方法、大小等属性;它比View多了一个Surface的东西,Surface是专门用来绘制的类,而SurfaceView可以控制surface绘制的大小、位置等等;
可能有人会问,那为什么要专门这样一个类来绘制呢?不是有OnDraw()方法吗?相比于OnDraw()方法它有很多优势,如下总结:
(1)在频繁更新UI线程的情况下,可以使用封装好的surface来频繁的更新,因为surface可以使用后台线程对UI界面进行绘制,而OnDraw()等绘制方法很难做到(除非你频繁的调用handler来更新主界面,这得多麻烦啊!);
(2)SurfaceView可以用来绘制2D或者3D图形,绘制一些动态曲线等,它显示的速度会比一般的快很多,因为他是通过硬件加速的方式来绘制的。
(3)它可以用来接受硬件的数据来绘制图像。
所以,通过以上几点我们可以知道,用它来接受相机的预览是理所当然的。那么它的使用方法是怎么样的呢?下创建一个surfaceView的继承类一般需要实现如下几个方法:
(1)surfaceCreated(SurfaceHolderholder):在该类创建的时候调用,这里一般需要实现一些初始化的工作,SurfaceHodler用来设定surface的大小位置等等;
(2)surfaceChanged(SurfaceHolderholder, int format, int width,int height)在surface大小发生改变时候调用,这里实现图形的绘制;
(3)surfaceDestroyed(SurfaceHolderholder)在surface销毁时候调用,这里一般对资源进行释放;
(4)实现SurfaceHodler.CallBack回调方法,在surfaceView创建完成后自动调用类本身;
在实现之前我们先来看我们的需求,我们要实现的功能:预览、拍照、自动聚焦、触摸聚焦、连续拍照、照片存储。下面我们来创建一个SurfaceView类CameraPreview,它继承了SurfaceView,并实现接口SurfaceHolder.Callback
因此我们需要在surfaceCreated方法中创建一个camer实例,这个实例可以在这个类中进行调用,实现代码如下:
/** * 创建的时候自动调用该方法 */ @Override public void surfaceCreated(SurfaceHolder holder) { if (mCamera == null) { mCamera = CameraCheck.getCameraInstance(mContext); } try { if(mCamera!=null){ mCamera.setPreviewDisplay(holder); } } catch (IOException e) { if (null != mCamera) { mCamera.release(); mCamera = null; isPreview=false; } e.printStackTrace(); } }
这句代码 mCamera.setPreviewDisplay(holder)的意思是创建一个预览的hodler;我们在surfaceChanged中进行预览窗口的绘制调用的是startPreview()方法来开始绘制,代码如下:
/** * 当surface的大小发生改变的时候自动调用的 */ @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { if (mHolder.getSurface() == null) { return; } try { setCameraParms(); mCamera.setPreviewDisplay(holder); mCamera.startPreview(); reAutoFocus(); } catch (Exception e) { Log.d(TAG, "Error starting camera preview: " + e.getMessage()); } }
其中有2个比较关键的方法没有实现, setCameraParms()和reAutoFocus(),setCameraParms();函数用来设置预览图片的参数,其中关键的为预览图片的大小和拍照保存的尺寸大小,很多的网上实现的程序拍出来的照片很小模糊的原因就是没有设置好照片的尺寸,这个照片的尺寸是根据手机本身能够支持的尺寸有很大关系。reAutoFocus()是自动聚焦的方法,需要动态获取reAutoFocus()函数是自动聚焦的实现;
我们首先来看一下setCameraParms()方法的实现:
private void setCameraParms(){ Camera.Parameters myParam = mCamera.getParameters(); List<Camera.Size> mSupportedsizeList =myParam.getSupportedPictureSizes(); if(mSupportedsizeList.size() > 1) { Iterator<Camera.Size> itos = mSupportedsizeList.iterator(); while (itos.hasNext()){ Camera.Size curSize = itos.next(); int curSupporSize=curSize.width * curSize.height; int fixPictrueSize= setFixPictureWidth * setFixPictureHeight; if( curSupporSize>fixPictrueSize && curSupporSize <= maxPictureSize) { setFixPictureWidth = curSize.width; setFixPictureHeight = curSize.height; } } } myParam.setJpegQuality(100); mCamera.setParameters(myParam); if (myParam.getMaxNumDetectedFaces() > 0){ mCamera.startFaceDetection(); } }
通过myParam.getSupportedPictureSizes();获取到手机支持的所有尺寸的枚举,并设置最大的固定尺寸这里设置最大为maxPictureSize = 5000000
reAutoFocus()的实现为:
<span style="white-space:pre"> </span>/** * Call the camera to Auto Focus */ public void reAutoFocus() { if (isSupportAutoFocus) { mCamera.autoFocus(new AutoFocusCallback() { @Override public void onAutoFocus(boolean success, Camera camera) { } }); } }
使用回调函数autoFocus来实现自动聚焦
五.拍照
拍照方法有一个难点是横竖屏拍照的转换和存储,网上大都实现的是默认的横屏拍照,一旦换成竖屏后预览就会出现问题,而且存储的照片也有问题,因此为了解决这个问题,我们需要时刻监听方向传感器的变化,得到当前的旋转角度,我么可以通过调用OrientationEventListener系统监听类来得到当前角度,自定义MyOrientationDetector代码如下:
/** * 方向变化监听器,监听传感器方向的改变 * @author zw.yan * */ public class MyOrientationDetector extends OrientationEventListener{ int Orientation; public MyOrientationDetector(Context context ) { super(context ); } @Override public void onOrientationChanged(int orientation) { Log.i("MyOrientationDetector ","onOrientationChanged:"+orientation); this.Orientation=orientation; Log.d("MyOrientationDetector","当前的传感器方向为"+orientation); } public int getOrientation(){ return Orientation; } }
在预览类中我们定义拍照方法TakePhone(),代码如下:
/** * 调整照相的方向,设置拍照相片的方向 */ private void takePhoto() { cameraOrientation = new MyOrientationDetector(mContext); if (mCamera != null) { int orientation = cameraOrientation.getOrientation(); Camera.Parameters cameraParameter = mCamera.getParameters(); cameraParameter.setRotation(90); cameraParameter.set("rotation", 90); if ((orientation >= 45) && (orientation < 135)) { cameraParameter.setRotation(180); cameraParameter.set("rotation", 180); } if ((orientation >= 135) && (orientation < 225)) { cameraParameter.setRotation(270); cameraParameter.set("rotation", 270); } if ((orientation >= 225) && (orientation < 315)) { cameraParameter.setRotation(0); cameraParameter.set("rotation", 0); } mCamera.setParameters(cameraParameter); mCamera.takePicture(shutterCallback, pictureCallback, mPicture); } }
在角度范围内自动调整旋转图片的角度,具体旋转的方式如代码,从而使存储的图片能够正常显示。
六.图片保存
在拍照时需要对图片进行保存,但是不能影响图片的下一次拍照,因此我们需要采用异步线程的方式,可以使用AsyncTask类,在拍照完成时进行调用如下代码:
public class SavePictureTask extends AsyncTask<byte[], String, String> { @SuppressLint("SimpleDateFormat") @Override protected String doInBackground(byte[]... params) { File pictureFile = FileUtil.getOutputMediaFile(MEDIA_TYPE_IMAGE, mContext); if (pictureFile == null) { Toast.makeText(mContext, "请插入存储卡!", Toast.LENGTH_SHORT).show(); return null; } try { FileOutputStream fos = new FileOutputStream(pictureFile); fos.write(params[0]); fos.flush(); fos.close(); } catch (FileNotFoundException e) { Log.d(TAG, "File not found: " + e.getMessage()); } catch (IOException e) { Log.d(TAG, "Error accessing file: " + e.getMessage()); } return null; } }
这是基本对文件异步线程的IO操作有什么不明白的可以去看对应的API文档。
下面我将整个类贴出来:
/** * sufaceView 的预览类,其中SurfaceHolder.CallBack用来监听Surface的变化, * 当Surface发生改变的时候自动调用该回调方法 * 通过调用方SurfaceHolder.addCallBack来绑定该方法 * @author zw.yan * */ public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { private String TAG = "CameraPreview"; /** * Surface的控制器,用来控制预览等操作 */ private SurfaceHolder mHolder; /** * 相机实例 */ private Camera mCamera = null; /** * 图片处理 */ public static final int MEDIA_TYPE_IMAGE = 1; /** * 预览状态标志 */ private boolean isPreview = false; /** * 设置一个固定的最大尺寸 */ private int maxPictureSize = 5000000; /** * 是否支持自动聚焦,默认不支持 */ private Boolean isSupportAutoFocus = false; /** * 获取当前的context */ private Context mContext; /** * 当前传感器的方向,当方向发生改变的时候能够自动从传感器管理类接受通知的辅助类 */ MyOrientationDetector cameraOrientation; /** * 设置最适合当前手机的图片宽度 */ int setFixPictureWidth = 0; /** * 设置当前最适合的图片高度 */ int setFixPictureHeight = 0; @SuppressWarnings("deprecation") public CameraPreview(Context context) { super(context); this.mContext = context; isSupportAutoFocus = context.getPackageManager().hasSystemFeature( PackageManager.FEATURE_CAMERA_AUTOFOCUS); mHolder = getHolder(); //兼容android 3.0以下的API,如果超过3.0则不需要设置该方法 if(Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB){ mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); } mHolder.addCallback(this);//绑定当前的回调方法 } /** * 创建的时候自动调用该方法 */ @Override public void surfaceCreated(SurfaceHolder holder) { if (mCamera == null) { mCamera = CameraCheck.getCameraInstance(mContext); } try { if(mCamera!=null){ mCamera.setPreviewDisplay(holder); } } catch (IOException e) { if (null != mCamera) { mCamera.release(); mCamera = null; isPreview=false; } e.printStackTrace(); } } /** * 当surface的大小发生改变的时候自动调用的 */ @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { if (mHolder.getSurface() == null) { return; } try { setCameraParms(); mCamera.setPreviewDisplay(holder); mCamera.startPreview(); reAutoFocus(); } catch (Exception e) { Log.d(TAG, "Error starting camera preview: " + e.getMessage()); } } private void setCameraParms(){ Camera.Parameters myParam = mCamera.getParameters(); List<Camera.Size> mSupportedsizeList =myParam.getSupportedPictureSizes(); if(mSupportedsizeList.size() > 1) { Iterator<Camera.Size> itos = mSupportedsizeList.iterator(); while (itos.hasNext()){ Camera.Size curSize = itos.next(); int curSupporSize=curSize.width * curSize.height; int fixPictrueSize= setFixPictureWidth * setFixPictureHeight; if( curSupporSize>fixPictrueSize && curSupporSize <= maxPictureSize) { setFixPictureWidth = curSize.width; setFixPictureHeight = curSize.height; } } } myParam.setJpegQuality(100); mCamera.setParameters(myParam); if (myParam.getMaxNumDetectedFaces() > 0){ mCamera.startFaceDetection(); } } @Override public void surfaceDestroyed(SurfaceHolder holder) { mCamera.stopPreview(); mCamera.release(); mCamera = null; } /** * Call the camera to Auto Focus */ public void reAutoFocus() { if (isSupportAutoFocus) { mCamera.autoFocus(new AutoFocusCallback() { @Override public void onAutoFocus(boolean success, Camera camera) { } }); } } /** * 自动聚焦,然后拍照 */ public void takePicture() { if (mCamera != null) { mCamera.autoFocus(autoFocusCallback); } } private AutoFocusCallback autoFocusCallback = new AutoFocusCallback() { public void onAutoFocus(boolean success, Camera camera) { // TODO Auto-generated method stub if (success) { Log.i(TAG, "autoFocusCallback: success..."); takePhoto(); } else { Log.i(TAG, "autoFocusCallback: fail..."); if (isSupportAutoFocus) { takePhoto(); } } } }; /** * 调整照相的方向,设置拍照相片的方向 */ private void takePhoto() { cameraOrientation = new MyOrientationDetector(mContext); if (mCamera != null) { int orientation = cameraOrientation.getOrientation(); Camera.Parameters cameraParameter = mCamera.getParameters(); cameraParameter.setRotation(90); cameraParameter.set("rotation", 90); if ((orientation >= 45) && (orientation < 135)) { cameraParameter.setRotation(180); cameraParameter.set("rotation", 180); } if ((orientation >= 135) && (orientation < 225)) { cameraParameter.setRotation(270); cameraParameter.set("rotation", 270); } if ((orientation >= 225) && (orientation < 315)) { cameraParameter.setRotation(0); cameraParameter.set("rotation", 0); } mCamera.setParameters(cameraParameter); mCamera.takePicture(shutterCallback, pictureCallback, mPicture); } } private ShutterCallback shutterCallback = new ShutterCallback() { @Override public void onShutter() { // TODO Auto-generated method stub } }; private PictureCallback pictureCallback = new PictureCallback() { @Override public void onPictureTaken(byte[] arg0, Camera arg1) { // TODO Auto-generated method stub } }; private PictureCallback mPicture = new PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { new SavePictureTask().execute(data); mCamera.startPreview();//重新开始预览 } }; public class SavePictureTask extends AsyncTask<byte[], String, String> { @SuppressLint("SimpleDateFormat") @Override protected String doInBackground(byte[]... params) { File pictureFile = FileUtil.getOutputMediaFile(MEDIA_TYPE_IMAGE, mContext); if (pictureFile == null) { Toast.makeText(mContext, "请插入存储卡!", Toast.LENGTH_SHORT).show(); return null; } try { FileOutputStream fos = new FileOutputStream(pictureFile); fos.write(params[0]); fos.flush(); fos.close(); } catch (FileNotFoundException e) { Log.d(TAG, "File not found: " + e.getMessage()); } catch (IOException e) { Log.d(TAG, "Error accessing file: " + e.getMessage()); } return null; } } @Override public boolean onTouchEvent(MotionEvent event) { reAutoFocus(); return false; } }
文件的布局和调用如下:
public class CameraActivity extends Activity{ private CameraPreview mPreview; public static final int MEDIA_TYPE_IMAGE = 1; public static final int MEDIA_TYPE_VIDEO = 2; private String TAG="CameraActivity"; private FrameLayout preview; private ImageButton captureButton; @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(R.layout.camera_preview); mPreview = new CameraPreview(this); preview = (FrameLayout) findViewById(R.id.camera_preview); preview.addView(mPreview); captureButton = (ImageButton) findViewById(R.id.button_capture); captureButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mPreview.takePicture(); } }); } @Override protected void onDestroy() { // TODO Auto-generated method stub super.onDestroy(); } }
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#000"> <FrameLayout android:id="@+id/camera_preview" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" /> <ImageButton android:id="@+id/button_capture" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:src=http://www.mamicode.com/"@drawable/camera_icon">
最终效果如下:注意,里面有一些类没有具体说明,具体可以查看后面的代码下载链接,有什么不明白的可以发邮件,邮件地址为dali_yan@yeah.net
转载请注明出处,代码下载地址:下载地址(别走!请留下你的评论)相机开发(聚焦、横竖屏拍照、照片存储、连续拍照等)