前段时间在做face++相关的功能,对于照相机也是进行了一番研究,小有收获,很感谢有一些大神已经写了相应的博客,让我在他们的项目上进行完善和优化,修复了一些bug,并对机型适配做了一些处理,目前已经保证了团队里面十多部安卓手机的完美适配,具体项目资源可以在http://download.csdn.net/detail/shan286/9799622这个网址上下载。好的,话不多说,直接上代码。
1、首先是关于照相机的预览功能,这里就要说到SurfaceView这个控件,我在布局最底层放了一个自定义的MySurfaceView,然后在它的上层放一个自定义的TakePhotoView,这个TakePhotoView主要是用于当用户想要在拍照的时候绘制自己想要的图片时,就可以在这个View中实现,这里我是画了一个带橙色框框的图片放在了里面。MySurfaceView的代码如下:
import android.content.Context; import android.graphics.PixelFormat; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; /** * Created by xueli on 2016/11/16. * * 1、注意点:surfaceview变得可见时,surface被创建;surfaceview隐藏前,surface被销毁 * 2、实现过程:继承SurfaceView并实现SurfaceHolder.Callback接口 * ----> SurfaceView.getHolder()获得SurfaceHolder对象 * ----> SurfaceHolder.addCallback(callback)添加回调函数 * ----> SurfaceHolder.lockCanvas()获得Canvas对象并锁定画布 * ----> Canvas绘画 * ----> SurfaceHolder.unlockCanvasAndPost(Canvas canvas)结束锁定画图,并提交改变,将图形显示。 */ public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback { private static final String TAG = "MySurfaceView"; private Context mContext; private SurfaceHolder mSurfaceHolder; public MySurfaceView(Context context, AttributeSet attrs) { super(context, attrs); 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) { //在创建时激发,一般在这里调用画图的线程。 Log.i(TAG, "surfaceCreated..."); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { //在surface的大小发生改变时激发 Log.i(TAG, "surfaceChanged..."); } @Override public void surfaceDestroyed(SurfaceHolder holder) { //销毁时激发,一般在这里将画图的线程停止、释放。 Log.i(TAG, "surfaceDestroyed..."); CameraInterface.getInstance().doStopCamera(); } public SurfaceHolder getSurfaceHolder() { // SurfaceHolder当作surface的控制器,用来操纵surface return mSurfaceHolder; } }
TakePhotoView的代码如下:
import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.util.AttributeSet; import android.view.View; import com.sherry.ui.R; import com.sherry.util.DeviceInfoUtil; /** * Created by xueli on 2016/11/16. * * 自定义拍照框View * */ public class TakePhotoView extends View { private Context mContext; private Paint mPaint; public TakePhotoView(Context context, AttributeSet attrs) { super(context, attrs); this.mContext = context; mPaint = new Paint(); mPaint.setAntiAlias(false); } @Override protected void onDraw(Canvas canvas) { // 拍照框预览的宽高设置 Rect frame = new Rect(0, 0, DeviceInfoUtil.getScreenWidth(mContext), DeviceInfoUtil.getScreenHeight(mContext) * 17 / 25); mPaint.setColor(Color.GRAY); Rect faceRect = new Rect(); faceRect.left = frame.left; faceRect.right = frame.right; faceRect.top = frame.top; faceRect.bottom = frame.bottom; canvas.drawBitmap(((BitmapDrawable) (getResources().getDrawable(R.drawable.take_photo_bg))).getBitmap(), null, faceRect, mPaint); } }
2、照相机处理类------CameraInterface
(1)在CameraInterface这个类里面对照相机进行一些处理,包括照相机的开启、预览、拍照、销毁等方法,这里参考博客http://blog.csdn.net/yanzi1225627/article/details/33028041,可以说给我提供了很大的帮助。不过我在它的实现上进行了优化和改进,解决了部分手机的黑屏、拍照或预览变形等问题。因为我这边要求的是拍照之后显示图片在下一个页面进行显示,所以拍照之后不会只是暂停然后继续进行预览,另外要注意的是,我这边使用的是前置摄像头,所以在doOpenCamera()方法中要判断手机摄像头的个数,让它选择开启前置摄像头。具体代码如下:
/** * 打开Camera * * @param callback */ public void doOpenCamera(CamOpenOverCallback callback) { Log.i(TAG, "Camera open...."); mCamera = null; try { Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); mCameraCount = Camera.getNumberOfCameras(); // 获得摄像头的个数 for (int camIdx = 0; camIdx < mCameraCount; camIdx++) { Camera.getCameraInfo(camIdx, cameraInfo); // get camerainfo if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { // 代表摄像头的方位,目前有定义值两个分别为CAMERA_FACING_FRONT前置和CAMERA_FACING_BACK后置 mCamera = Camera.open(camIdx); } } } catch (Exception e) { Log.e(TAG, "摄像头未正常打开"); } Log.i(TAG, "Camera open over...."); callback.cameraHasOpened(); }(2)接下来就说预览的这个方法doStartPreview(),方法里面的参数用的是设备的宽高。为了适配多个不同分辨率的机型,我这里对获取预览尺寸getPropPreviewSize()方法和获取拍照后的图片尺寸getPropPictureSize()方法进行了修改,传的是一个经过了处理的值,而不是某个固定值,至于这个值是怎么处理的,后面会细说。先上doStartPreview()方法的代码:
public void doStartPreview(SurfaceHolder holder, int width, int height) { Log.i(TAG, "doStartPreview..."); if (mCamera != null) { mParams = mCamera.getParameters(); mParams.setPictureFormat(PixelFormat.JPEG);//设置拍照后存储的图片格式 //设置PreviewSize和PictureSize Camera.Size previewSize = CamParaUtil.getInstance().getPropPreviewSize( mParams.getSupportedPreviewSizes(), width, height); mParams.setPreviewSize(previewSize.width, previewSize.height); Camera.Size pictureSize = CamParaUtil.getInstance().getPropPictureSize( mParams.getSupportedPictureSizes(), previewSize.width, previewSize.height); mParams.setPictureSize(pictureSize.width, pictureSize.height); mCamera.setDisplayOrientation(90); ...... }首先,从上面这段代码中,我们可以看到在getPropPreviewSize()方法中,我不是传递固定的值,那么传的到底是什么呢,其实是我们手机屏幕的宽高。有人就会疑惑了,那个可以我们可以直接拿到啊,直接把它丢进去就好了呀,干嘛还费这么大的劲儿。这里就要说到每个手机所能提供的预览Size列表了。我这里用的是小米4,它提供的预览Size如下:
我们知道小米4的分辨率是1920*1080,而我们预览的又是全屏,所以应该拿到全屏的Size,这里正好有对应的Size,所以就通过我优化的方法拿到了。但是如果没有和手机屏幕分辨率的Size呢?那就拿最大的,很多人就有疑问了,拿最大的不会有问题吗?这里就要说到我在开发过程中遇到的一个问题了。
我在三星S6上面看了它提供的预览Size是没有三星S6的分辨率2560*1440的,同时其他的Size在原来的方法上也没找到匹配的,所以如果不选最大的,选择最小的那个,也就是176*144的话,就会出现预览特别模糊的情况。
当然这个方法在其他多部手机上也进行了试验,是可以完美解决某些手机上模糊或变形等问题的。getPropPreviewSize()方法的代码如下:
public Size getPropPreviewSize(List<Size> list, int minWidth, int minHeight) { Collections.sort(list, sizeComparator); Log.i(TAG, "PreviewSize : minWidth = " + minWidth); int i = 0; for (Size s : list) { Log.i(TAG, "PreviewSize : width = " + s.width + "height = " + s.height); if ((s.height == minWidth) && s.width >= minHeight) { Log.i(TAG, "PreviewSize : w = " + s.width + "h = " + s.height); break; } i++; } if (i == list.size()) { i = list.size() - 1;//如果没找到,就选最大的size } return list.get(i); }
看了上面的代码又有人问了,你不是要找和分辨率相同的宽高吗?为什么是s.width >= minHeight而不是s.width == minHeight?
这是因为我在匹配一台低分辨率的HTC手机时,发现它不是标准的800*480,它的高度其实只有768。所以我在判断的时候写的是s.width >= minHeight,而不是等于,其实就是用就近原则拿到我们预览的最合适的高度。当然大家会看到在getPropPreviewSize()方法的第二行有一句
Collections.sort(list, sizeComparator);也就是让我们这个Size的列表进行了一个排序,这样最近的尺寸就是差异最小的。至于为什么那个HTC手机的高度是768呢?我发现HTC手机的底部的屏幕上有几个按键,或许是这几个按键占据了剩下那32个像素,当然这个没有实际根据,暂且搁在一边不管。
(3)说完了预览的Size,接下来说说getPropPictureSize()方法,这里我传的参数是预览拿到的Size的宽高,也就是下面这句代码中的previewSize.width和previewSize.height。
Camera.Size pictureSize = CamParaUtil.getInstance().getPropPictureSize(
mParams.getSupportedPictureSizes(), previewSize.width, previewSize.height);
为什么要传这两个参数呢?前面获得预览Size的方法已经完美适配了多台手机,而我们的pictureSize肯定也希望拿到的是和预览看到的至少是相同比例的图片,这样我们拿到的图片才是没有变形的。所以我们只需要拿这两个参数和手机提供的pictureSize列表中的宽高进行比较就好,如果拿不到同样也拿最大的那个图片,原因我就不说了,前面其实说的很清楚了。我们的getPropPictureSize()方法的代码如下:
public Size getPropPictureSize(List<Size> list, int minWidth, int minHeight) {
Collections.sort(list, sizeComparator);
int i = 0;
for (Size s : list) {
Log.i(TAG, "PreviewSize : width = " + s.width + "height = " + s.height);
if (s.height == minHeight && s.width == minWidth) {
Log.i(TAG, "PreviewSize : w = " + s.width + "h = " + s.height);
break;
}
i++;
}
if (i == list.size()) {
i = list.size() - 1;//如果没找到,就选最大的size
}
return list.get(i);
}
(4)开启了预览,自然也有在退出相应的Activity时关闭预览的方法,这里我写了两个关闭预览的方法doStopCamera()和doDestroyedCamera(),代码如下:
/** * 停止预览,不需要释放Camera */ public void doStopCamera() { try { if (null != mCamera) { mCamera.setPreviewCallback(null); mCamera.stopPreview(); } } catch (Exception e) { Log.e(TAG, e + "stopCamera"); } } /** * 停止预览,释放Camera */ public void doDestroyedCamera() { try { if (null != mCamera) { mCamera.setPreviewCallback(null); mCamera.stopPreview(); mCamera.release(); mCamera = null; } } catch (Exception e) { Log.e(TAG, e + "destoryCamera"); } }为什么要用两个关闭预览的方法呢?这两个方法看起来差不多,唯一不同的是doStopCamera()方法没有释放了Camera,而doDestroyedCamera()方法释放了。这里就要说到另一个问题,在部分手机上测试的时候,我发现如果我们点击home键直接从拍照的这个页面退出或者点击back键之后,再重新进入拍照页面就会直接出现黑屏的问题。最终也是去查找了一下原因,原来是我们做这个操作时,那些手机里面的Camera被释放掉了,所以才会出现黑屏。因此我们要合理地释放Camera,在我们的Activity中,我是这样调用这两个方法的。
@Override protected void onPause() { super.onPause(); SurfaceHolder holder = vSurfaceView.getSurfaceHolder(); vSurfaceView.surfaceDestroyed(holder); } @Override protected void onDestroy() { super.onDestroy(); CameraInterface.getInstance().doDestroyedCamera(); }而在上面代码的surfaceDestroyed()方法中我调用的是doStopCamera()方法,这样我们的Camera就不会被释放,可以顺利唤醒了。不过这里还要提到一点,就是在onResume()方法中加上一个判断,当我们的Surface还是无效的时候,让一个线程休眠50毫秒。因为我们的SurfaceView不可见的时候,Surface就会被销毁。onResume()方法中的处理如下:
@Override protected void onResume() { super.onResume(); new Thread(new Runnable() { @Override public void run() { // 解决三星S6等部分机型的黑屏问题 try { SurfaceHolder holder = vSurfaceView.getSurfaceHolder(); while (!holder.getSurface().isValid()) { Thread.sleep(50); } CameraInterface.getInstance().doStartPreview(holder, DeviceInfoUtil.getScreenWidth(TakePhoto.this), DeviceInfoUtil.getScreenHeight(TakePhoto.this)); } catch (Exception e) { e.printStackTrace(); } } }).start(); }
(4)我看到有人曾经说,想要做成微信扫一扫那样,下面有一部分不显示出来,但是发现预览的时候变形了。其实变形的原因除了是获取的预览Size有问题,还有一点就是我们的这个SurfaceView的宽高设置的不正确。请一定一定设置成全屏,另外说到微信扫一扫,其实也是全屏预览的,只是底部的控件把预览框盖住了一部分而已。想象摄像头拍的是正常的预览的样子,你非要把预览的样子改成一个和它比例不一样的,宽度相同,高度不同,不变形才怪。如果真的想制造一种我只用了不到一个屏幕的高度在拍照,可以使用相对布局,直接用下方的控件盖住一定的高度就好。至于大家会说拍出来的照片会把下面显示出来,那把拍出来的照片进行裁剪就好了呀。此外要注意的一点就是,在拍照页面对应的Activity中一定要设置好SurfaceView的宽高为全屏的宽高,也就是我在这个里面写的setSurfaceView()方法。对应的Activity和布局代码如下:
import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.SurfaceHolder; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.Toast; import com.sherry.face.CameraInterface; import com.sherry.face.MySurfaceView; import com.sherry.ui.R; import com.sherry.util.DeviceInfoUtil; import com.sherry.util.FileUtil; import com.sherry.util.ImageUtil; import java.io.File; /** * 拍照页面 * * Created by xueli on 2016/11/15. */ public class TakePhoto extends Activity implements CameraInterface.CamOpenOverCallback, View.OnClickListener { private static final String TAG = "TakePhoto"; public static File mTempFile; private final int WHAT_TAKE_PHOTO_SUCCEED = 0; private String TEMP_PHONE_FILENAME = ""; private MySurfaceView vSurfaceView; private Handler mHandler = new Handler() { @Override public void dispatchMessage(Message msg) { super.dispatchMessage(msg); switch (msg.what) { case WHAT_TAKE_PHOTO_SUCCEED: if (msg.obj != null) { Uri uri = (Uri) msg.obj; Intent intent = new Intent(TakePhoto.this, Preview.class); intent.setDataAndType(uri, "image/*"); startActivity(intent); finish(); } break; default: break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.face_take_photo); setSurfaceView(); Thread openThread = new Thread(){ @Override public void run() { CameraInterface.getInstance().doOpenCamera(TakePhoto.this); } }; openThread.start(); findView(); } private void setSurfaceView() { vSurfaceView = (MySurfaceView) findViewById(R.id.sv_take_photo); ViewGroup.LayoutParams params = vSurfaceView.getLayoutParams(); params.width = DeviceInfoUtil.getScreenWidth(this); params.height = DeviceInfoUtil.getScreenHeight(this); vSurfaceView.setLayoutParams(params); } private void findView() { RelativeLayout rl = (RelativeLayout) findViewById(R.id.rl_face_take_photo); RelativeLayout.LayoutParams rlPs = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); rlPs.width = DeviceInfoUtil.getScreenWidth(this); rlPs.height = DeviceInfoUtil.getScreenHeight(this); rl.setLayoutParams(rlPs); LinearLayout llBottom = (LinearLayout) findViewById(R.id.ll_face_take_photo); RelativeLayout.LayoutParams llPs = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); llPs.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); llPs.width = DeviceInfoUtil.getScreenWidth(this); llPs.height = DeviceInfoUtil.getScreenHeight(this) * 8 / 25; llBottom.setLayoutParams(llPs); Button btnTakePhoto = (Button) findViewById(R.id.btn_take_photo); btnTakePhoto.setOnClickListener(this); } @Override public void cameraHasOpened() { SurfaceHolder holder = vSurfaceView.getSurfaceHolder(); CameraInterface.getInstance().doStartPreview(holder, DeviceInfoUtil.getScreenWidth(TakePhoto.this), DeviceInfoUtil.getScreenHeight(TakePhoto.this)); } @Override public void onClick(View view) { switch (view.getId()) { case R.id.btn_take_photo: if (ImageUtil.hasSdcard()) { FileUtil.prepareFile(Environment.getExternalStorageDirectory() + "/DCIM/"); // 准备文件夹 String fileName = ImageUtil.getPhotoFileName(); mTempFile = new File(Environment.getExternalStorageDirectory() + "/DCIM/", fileName); TEMP_PHONE_FILENAME = fileName; try { CameraInterface.getInstance().doTakePicture(); Uri uri = Uri.fromFile(new File(Environment.getExternalStorageDirectory() + "/DCIM/", TEMP_PHONE_FILENAME)); Log.d(TAG, "Uri===" + String.valueOf(uri)); Message msgObj = mHandler.obtainMessage(WHAT_TAKE_PHOTO_SUCCEED, uri); mHandler.sendMessageDelayed(msgObj, 2000); } catch (Exception e) { Toast.makeText(this, "您的设备未正常打开,请重试", Toast.LENGTH_LONG).show(); Log.e(TAG, e + "您的设备未正常打开,请重试"); } } else { Toast.makeText(this, "没找到SD卡", Toast.LENGTH_LONG).show(); } break; default: break; } } @Override protected void onResume() { super.onResume(); new Thread(new Runnable() { @Override public void run() { // 解决三星S6等部分机型的黑屏问题 try { SurfaceHolder holder = vSurfaceView.getSurfaceHolder(); while (!holder.getSurface().isValid()) { Thread.sleep(50); } CameraInterface.getInstance().doStartPreview(holder, DeviceInfoUtil.getScreenWidth(TakePhoto.this), DeviceInfoUtil.getScreenHeight(TakePhoto.this)); } catch (Exception e) { e.printStackTrace(); } } }).start(); } @Override protected void onPause() { super.onPause(); SurfaceHolder holder = vSurfaceView.getSurfaceHolder(); vSurfaceView.surfaceDestroyed(holder); } @Override protected void onDestroy() { super.onDestroy(); CameraInterface.getInstance().doDestroyedCamera(); } }
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <RelativeLayout android:id="@+id/rl_face_take_photo" android:layout_width="match_parent" android:layout_height="wrap_content"> <com.sherry.face.MySurfaceView android:id="@+id/sv_take_photo" android:layout_width="match_parent" android:layout_height="wrap_content" /> <com.sherry.face.TakePhotoView android:id="@+id/tpv_take_photo" android:layout_width="match_parent" android:layout_height="wrap_content" /> </RelativeLayout> <LinearLayout android:id="@+id/ll_face_take_photo" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:background="@color/white"> <Button android:id="@+id/btn_take_photo" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:padding="15dp" android:text="@string/take_photo" android:textColor="@color/white" android:textSize="@dimen/text_size_16" /> </LinearLayout> </RelativeLayout>
好的,关于照相机的预览、拍照、以及机型出现的各种问题解决方式就介绍到这里,最后别忘了在清单文件中加上照相机和SD卡读写权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" />
转载请注明http://write.blog.csdn.net/postedit/53189034。