照相机预览、拍照以及适配问题的完美解决

         前段时间在做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如下:

照相机预览、拍照以及适配问题的完美解决_第1张图片

我们知道小米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卡读写权限:

    
    
    

    
    

转载请注明http://write.blog.csdn.net/postedit/53189034。



你可能感兴趣的:(Android)