前段时间在做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;
}
}
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 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 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();
}
}
好的,关于照相机的预览、拍照、以及机型出现的各种问题解决方式就介绍到这里,最后别忘了在清单文件中加上照相机和SD卡读写权限: