玩转Android Camera开发(一):Surfaceview预览Camera,基础拍照功能完整demo
是在2012年的除夕之夜仓促完成,后来很多人指出了一些问题,琐事缠身一直没有进行升级。后来随着我自己的使用,越来越发现不出个升级版的demo是不行了。有时候就连我自己用这个demo测一些性能、功能点,用着都不顺手。当初代码是在linux下写的,弄到windows里下全是乱码。还要自己改几分钟才能改好。另外,很多人说不能正常预览,原因是我在布局里把Surfaceview的尺寸写死了。再有就是initCamera()的时候设参数失败,直接黑屏退出,原因也是我把预览尺寸和照片尺寸写死了。再有就是照片变形的问题。为此,今天出一个升级版的demo,争取全面适配所有机型。
上图为此次的代码结构,activity包里就是放CameraActivity,日后添加图库浏览功能再加GalleryActivity。为了使Camera的逻辑和界面的UI耦合度降至最低,封装了CameraInterface类,里面操作Camera的打开、预览、拍照、关闭。preview包里是自定义的Surfaceview。在util包里放着CamParaUtil是专门用来设置、打印Camera的PreviewSize、PictureSize、FocusMode的,并能根据Activity传进来的长宽比(主要是16:9 或 4:3两种尺寸)自动寻找适配的PreviewSize和PictureSize,消除变形。默认的是全屏,因为一些手机全屏时,屏幕的长宽比不是16:9或4:3所以在找尺寸时也是存在一些偏差的。其中有个值,就是判断两个float是否相等,这个参数比较关键,里面设的0.03.经我多个手机测试,这个参数是最合适的,否则的话有些奇葩手机得到的尺寸拍出照片变形。下面上源码:
一、布局 activity_camera.xml
<span style="font-family:Comic Sans MS;font-size:18px;"><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".CameraActivity" > <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" > <org.yanzi.camera.preview.CameraSurfaceView android:id="@+id/camera_surfaceview" android:layout_width="0dip" android:layout_height="0dip" /> </FrameLayout> <ImageButton android:id="@+id/btn_shutter" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/btn_shutter_background" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="10dip"/> </RelativeLayout> </span>
二、AndroidManifest.xml
<span style="font-family:Comic Sans MS;font-size:18px;"><?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.yanzi.playcamera" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="17" /> <!-- 增加文件存储和访问摄像头的权限 --> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher_icon" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name="org.yanzi.activity.CameraActivity" android:label="@string/app_name" android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" android:screenOrientation="portrait"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> </span>
三、下面是java代码
1、CameraActivity.java
<span style="font-family:Comic Sans MS;font-size:18px;">package org.yanzi.activity; import org.yanzi.camera.CameraInterface; import org.yanzi.camera.CameraInterface.CamOpenOverCallback; import org.yanzi.camera.preview.CameraSurfaceView; import org.yanzi.playcamera.R; import org.yanzi.util.DisplayUtil; import android.app.Activity; import android.graphics.Point; import android.os.Bundle; import android.view.Menu; import android.view.SurfaceHolder; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup.LayoutParams; import android.widget.ImageButton; public class CameraActivity extends Activity implements CamOpenOverCallback { private static final String TAG = "yanzi"; CameraSurfaceView surfaceView = null; ImageButton shutterBtn; float previewRate = -1f; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Thread openThread = new Thread(){ @Override public void run() { // TODO Auto-generated method stub CameraInterface.getInstance().doOpenCamera(CameraActivity.this); } }; openThread.start(); setContentView(R.layout.activity_camera); initUI(); initViewParams(); shutterBtn.setOnClickListener(new BtnListeners()); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.camera, menu); return true; } private void initUI(){ surfaceView = (CameraSurfaceView)findViewById(R.id.camera_surfaceview); shutterBtn = (ImageButton)findViewById(R.id.btn_shutter); } private void initViewParams(){ LayoutParams params = surfaceView.getLayoutParams(); Point p = DisplayUtil.getScreenMetrics(this); params.width = p.x; params.height = p.y; previewRate = DisplayUtil.getScreenRate(this); //默认全屏的比例预览 surfaceView.setLayoutParams(params); //手动设置拍照ImageButton的大小为120dip×120dip,原图片大小是64×64 LayoutParams p2 = shutterBtn.getLayoutParams(); p2.width = DisplayUtil.dip2px(this, 80); p2.height = DisplayUtil.dip2px(this, 80);; shutterBtn.setLayoutParams(p2); } @Override public void cameraHasOpened() { // TODO Auto-generated method stub SurfaceHolder holder = surfaceView.getSurfaceHolder(); CameraInterface.getInstance().doStartPreview(holder, previewRate); } private class BtnListeners implements OnClickListener{ @Override public void onClick(View v) { // TODO Auto-generated method stub switch(v.getId()){ case R.id.btn_shutter: CameraInterface.getInstance().doTakePicture(); break; default:break; } } } } </span>
2、CameraInterface.java
<span style="font-family:Comic Sans MS;font-size:18px;">package org.yanzi.camera; import java.io.IOException; import java.util.List; import org.yanzi.util.CamParaUtil; import org.yanzi.util.FileUtil; import org.yanzi.util.ImageUtil; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.PixelFormat; import android.hardware.Camera; import android.hardware.Camera.PictureCallback; import android.hardware.Camera.ShutterCallback; import android.hardware.Camera.Size; import android.util.Log; import android.view.SurfaceHolder; public class CameraInterface { private static final String TAG = "yanzi"; private Camera mCamera; private Camera.Parameters mParams; private boolean isPreviewing = false; private float mPreviwRate = -1f; private static CameraInterface mCameraInterface; public interface CamOpenOverCallback{ public void cameraHasOpened(); } private CameraInterface(){ } public static synchronized CameraInterface getInstance(){ if(mCameraInterface == null){ mCameraInterface = new CameraInterface(); } return mCameraInterface; } /**打开Camera * @param callback */ public void doOpenCamera(CamOpenOverCallback callback){ Log.i(TAG, "Camera open...."); mCamera = Camera.open(); Log.i(TAG, "Camera open over...."); callback.cameraHasOpened(); } /**开启预览 * @param holder * @param previewRate */ public void doStartPreview(SurfaceHolder holder, float previewRate){ Log.i(TAG, "doStartPreview..."); if(isPreviewing){ mCamera.stopPreview(); return; } if(mCamera != null){ mParams = mCamera.getParameters(); mParams.setPictureFormat(PixelFormat.JPEG);//设置拍照后存储的图片格式 CamParaUtil.getInstance().printSupportPictureSize(mParams); CamParaUtil.getInstance().printSupportPreviewSize(mParams); //设置PreviewSize和PictureSize Size pictureSize = CamParaUtil.getInstance().getPropPictureSize( mParams.getSupportedPictureSizes(),previewRate, 800); mParams.setPictureSize(pictureSize.width, pictureSize.height); Size previewSize = CamParaUtil.getInstance().getPropPreviewSize( mParams.getSupportedPreviewSizes(), previewRate, 800); mParams.setPreviewSize(previewSize.width, previewSize.height); mCamera.setDisplayOrientation(90); CamParaUtil.getInstance().printSupportFocusMode(mParams); List<String> focusModes = mParams.getSupportedFocusModes(); if(focusModes.contains("continuous-video")){ mParams.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); } mCamera.setParameters(mParams); try { mCamera.setPreviewDisplay(holder); mCamera.startPreview();//开启预览 } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } isPreviewing = true; mPreviwRate = previewRate; mParams = mCamera.getParameters(); //重新get一次 Log.i(TAG, "最终设置:PreviewSize--With = " + mParams.getPreviewSize().width + "Height = " + mParams.getPreviewSize().height); Log.i(TAG, "最终设置:PictureSize--With = " + mParams.getPictureSize().width + "Height = " + mParams.getPictureSize().height); } } /** * 停止预览,释放Camera */ public void doStopCamera(){ if(null != mCamera) { mCamera.setPreviewCallback(null); mCamera.stopPreview(); isPreviewing = false; mPreviwRate = -1f; mCamera.release(); mCamera = null; } } /** * 拍照 */ public void doTakePicture(){ if(isPreviewing && (mCamera != null)){ mCamera.takePicture(mShutterCallback, null, mJpegPictureCallback); } } /*为了实现拍照的快门声音及拍照保存照片需要下面三个回调变量*/ ShutterCallback mShutterCallback = new ShutterCallback() //快门按下的回调,在这里我们可以设置类似播放“咔嚓”声之类的操作。默认的就是咔嚓。 { public void onShutter() { // TODO Auto-generated method stub Log.i(TAG, "myShutterCallback:onShutter..."); } }; PictureCallback mRawCallback = new PictureCallback() // 拍摄的未压缩原数据的回调,可以为null { public void onPictureTaken(byte[] data, Camera camera) { // TODO Auto-generated method stub Log.i(TAG, "myRawCallback:onPictureTaken..."); } }; PictureCallback mJpegPictureCallback = new PictureCallback() //对jpeg图像数据的回调,最重要的一个回调 { public void onPictureTaken(byte[] data, Camera camera) { // TODO Auto-generated method stub Log.i(TAG, "myJpegCallback:onPictureTaken..."); Bitmap b = null; if(null != data){ b = BitmapFactory.decodeByteArray(data, 0, data.length);//data是字节数据,将其解析成位图 mCamera.stopPreview(); isPreviewing = false; } //保存图片到sdcard if(null != b) { //设置FOCUS_MODE_CONTINUOUS_VIDEO)之后,myParam.set("rotation", 90)失效。 //图片竟然不能旋转了,故这里要旋转下 Bitmap rotaBitmap = ImageUtil.getRotateBitmap(b, 90.0f); FileUtil.saveBitmap(rotaBitmap); } //再次进入预览 mCamera.startPreview(); isPreviewing = true; } }; } </span>
3、CameraSurfaceView.java
<span style="font-family:Comic Sans MS;font-size:18px;">package org.yanzi.camera.preview; import org.yanzi.camera.CameraInterface; import android.content.Context; import android.graphics.PixelFormat; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback { private static final String TAG = "yanzi"; CameraInterface mCameraInterface; Context mContext; SurfaceHolder mSurfaceHolder; public CameraSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub mContext = context; mSurfaceHolder = getHolder(); mSurfaceHolder.setFormat(PixelFormat.TRANSPARENT);//translucent半透明 transparent透明 mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); mSurfaceHolder.addCallback(this); } @Override public void surfaceCreated(SurfaceHolder holder) { // TODO Auto-generated method stub Log.i(TAG, "surfaceCreated..."); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // TODO Auto-generated method stub Log.i(TAG, "surfaceChanged..."); } @Override public void surfaceDestroyed(SurfaceHolder holder) { // TODO Auto-generated method stub Log.i(TAG, "surfaceDestroyed..."); CameraInterface.getInstance().doStopCamera(); } public SurfaceHolder getSurfaceHolder(){ return mSurfaceHolder; } } </span>
4、CamParaUtil.java
<span style="font-family:Comic Sans MS;font-size:18px;">package org.yanzi.util; import java.util.Collections; import java.util.Comparator; import java.util.List; import android.hardware.Camera; import android.hardware.Camera.Size; import android.util.Log; public class CamParaUtil { private static final String TAG = "yanzi"; private CameraSizeComparator sizeComparator = new CameraSizeComparator(); private static CamParaUtil myCamPara = null; private CamParaUtil(){ } public static CamParaUtil getInstance(){ if(myCamPara == null){ myCamPara = new CamParaUtil(); return myCamPara; } else{ return myCamPara; } } public Size getPropPreviewSize(List<Camera.Size> list, float th, int minWidth){ Collections.sort(list, sizeComparator); int i = 0; for(Size s:list){ if((s.width >= minWidth) && equalRate(s, th)){ Log.i(TAG, "PreviewSize:w = " + s.width + "h = " + s.height); break; } i++; } if(i == list.size()){ i = 0;//如果没找到,就选最小的size } return list.get(i); } public Size getPropPictureSize(List<Camera.Size> list, float th, int minWidth){ Collections.sort(list, sizeComparator); int i = 0; for(Size s:list){ if((s.width >= minWidth) && equalRate(s, th)){ Log.i(TAG, "PictureSize : w = " + s.width + "h = " + s.height); break; } i++; } if(i == list.size()){ i = 0;//如果没找到,就选最小的size } return list.get(i); } public boolean equalRate(Size s, float rate){ float r = (float)(s.width)/(float)(s.height); if(Math.abs(r - rate) <= 0.03) { return true; } else{ return false; } } public class CameraSizeComparator implements Comparator<Camera.Size>{ public int compare(Size lhs, Size rhs) { // TODO Auto-generated method stub if(lhs.width == rhs.width){ return 0; } else if(lhs.width > rhs.width){ return 1; } else{ return -1; } } } /**打印支持的previewSizes * @param params */ public void printSupportPreviewSize(Camera.Parameters params){ List<Size> previewSizes = params.getSupportedPreviewSizes(); for(int i=0; i< previewSizes.size(); i++){ Size size = previewSizes.get(i); Log.i(TAG, "previewSizes:width = "+size.width+" height = "+size.height); } } /**打印支持的pictureSizes * @param params */ public void printSupportPictureSize(Camera.Parameters params){ List<Size> pictureSizes = params.getSupportedPictureSizes(); for(int i=0; i< pictureSizes.size(); i++){ Size size = pictureSizes.get(i); Log.i(TAG, "pictureSizes:width = "+ size.width +" height = " + size.height); } } /**打印支持的聚焦模式 * @param params */ public void printSupportFocusMode(Camera.Parameters params){ List<String> focusModes = params.getSupportedFocusModes(); for(String mode : focusModes){ Log.i(TAG, "focusModes--" + mode); } } } </span>
5、DisplayUtil.java
<span style="font-family:Comic Sans MS;font-size:18px;">package org.yanzi.util; import android.content.Context; import android.graphics.Point; import android.util.DisplayMetrics; import android.util.Log; public class DisplayUtil { private static final String TAG = "DisplayUtil"; /** * dip转px * @param context * @param dipValue * @return */ public static int dip2px(Context context, float dipValue){ final float scale = context.getResources().getDisplayMetrics().density; return (int)(dipValue * scale + 0.5f); } /** * px转dip * @param context * @param pxValue * @return */ public static int px2dip(Context context, float pxValue){ final float scale = context.getResources().getDisplayMetrics().density; return (int)(pxValue / scale + 0.5f); } /** * 获取屏幕宽度和高度,单位为px * @param context * @return */ public static Point getScreenMetrics(Context context){ DisplayMetrics dm =context.getResources().getDisplayMetrics(); int w_screen = dm.widthPixels; int h_screen = dm.heightPixels; Log.i(TAG, "Screen---Width = " + w_screen + " Height = " + h_screen + " densityDpi = " + dm.densityDpi); return new Point(w_screen, h_screen); } /** * 获取屏幕长宽比 * @param context * @return */ public static float getScreenRate(Context context){ Point P = getScreenMetrics(context); float H = P.y; float W = P.x; return (H/W); } } </span>
6、FileUtil.java
<span style="font-family:Comic Sans MS;font-size:18px;">package org.yanzi.util; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import android.graphics.Bitmap; import android.os.Environment; import android.util.Log; public class FileUtil { private static final String TAG = "FileUtil"; private static final File parentPath = Environment.getExternalStorageDirectory(); private static String storagePath = ""; private static final String DST_FOLDER_NAME = "PlayCamera"; /**初始化保存路径 * @return */ private static String initPath(){ if(storagePath.equals("")){ storagePath = parentPath.getAbsolutePath()+"/" + DST_FOLDER_NAME; File f = new File(storagePath); if(!f.exists()){ f.mkdir(); } } return storagePath; } /**保存Bitmap到sdcard * @param b */ public static void saveBitmap(Bitmap b){ String path = initPath(); long dataTake = System.currentTimeMillis(); String jpegName = path + "/" + dataTake +".jpg"; Log.i(TAG, "saveBitmap:jpegName = " + jpegName); try { FileOutputStream fout = new FileOutputStream(jpegName); BufferedOutputStream bos = new BufferedOutputStream(fout); b.compress(Bitmap.CompressFormat.JPEG, 100, bos); bos.flush(); bos.close(); Log.i(TAG, "saveBitmap成功"); } catch (IOException e) { // TODO Auto-generated catch block Log.i(TAG, "saveBitmap:失败"); e.printStackTrace(); } } } </span>
7、ImageUtil.java
<span style="font-family:Comic Sans MS;font-size:18px;">package org.yanzi.util; import android.graphics.Bitmap; import android.graphics.Matrix; public class ImageUtil { /** * 旋转Bitmap * @param b * @param rotateDegree * @return */ public static Bitmap getRotateBitmap(Bitmap b, float rotateDegree){ Matrix matrix = new Matrix(); matrix.postRotate((float)rotateDegree); Bitmap rotaBitmap = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), matrix, false); return rotaBitmap; } } </span>
几点说明:
1、包括我之前的博文在内的大量网上链接,都是在Surfaceview create的时候进行打开Camera的操作,在Surfaceview Changed的时候进行开预览。而Surfaceview create的时候一定是在setContentView之后,Surfaceview实例化之后。为了优化开启Camera时间,我再setContentView之前new了一个线程专门去Open Camera。经过测试,但就执行Camera.open()这句话一般需要140ms左右。如果放在主线程里无疑是一种浪费。而在140ms之后,Surfaceview里因为无需触发关于Camera的操作,所以加载的特别快。也就是说Open完后,Surfaceview一定完成了实例化。所以我设置了CamOpenOverCallback回调,在Camera打开完毕后通知Activity立即执行开预览的操作。
2、开预览因为用Surfaceview预览,需传递Surfaceview的SurfaceHolder。
3、CameraInterface是个单例模式,所有关于Camera的流程性操作一律封装在这里面。
4、Activity设置了全屏无标题且强制竖屏,像这种操作能再xml写就不要再java代码里弄。
图片资源上,杂家还真是一番精心挑选,对比了Camera360、相机360、美颜相机,UI上总的来说三个app感觉都很垃圾,都整的太复杂了,图片也不好看。最后勉强用了相机360里的一个button,自己想用PS把按键点击时的图标色彩P亮点,一个没留神,还给P的更暗了色彩。汗,不过按键的对比效果更明显了。日后,会将一些OpenCV4Android的一些小demo都整合到PlayCamera系列。
下为效果图:
------------本文系原创,转载请注明作者yanzi1225627
版本号:PlayCamera_V1.0.0[2014-6-22].zip
CSDN下载链接:http://download.csdn.net/detail/yanzi1225627/7540873