Android 手把手带你玩转自定义相机

来源:互联网 发布:windows正版购买 编辑:程序博客网 时间:2024/04/30 01:43

本文已授权微信公众号《鸿洋》原创首发,转载请务必注明出处。

概述

相机几乎是每个APP都要用到的功能,万一老板让你定制相机方不方?反正我是有点方。关于相机的两天奋斗总结免费送给你。

  Intent intent = new Intent();    intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);    startActivity(intent);

或者指定返回图片的名称mCurrentPhotoFile

  Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);  intent.putExtra(MediaStore.EXTRA_OUTPUT,Uri.fromFile(mCurrentPhotoFile));  startActivityForResult(intent, CAMERA_WITH_DATA);

2.自定义启动相机。

今天以第二种为例。效果图如下
demo

自定义相机的一般步骤

  1. 创建显示相机画面的布局,Android已经为我们选定好SurfaceView
  2. 通过SurfaceView#getHolder()获得链接CameraSurfaceViewSurfaceHolder
  3. Camame.open()打开相机
  4. 通过SurfaceHolder链接CameraurfaceView

一般步骤的代码演示

public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Camera.AutoFocusCallback {    private static final String TAG = "CameraSurfaceView";    private Context mContext;    private SurfaceHolder holder;    private Camera mCamera;    private int mScreenWidth;    private int mScreenHeight;    public CameraSurfaceView(Context context) {        this(context, null);    }    public CameraSurfaceView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public CameraSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        mContext = context;        getScreenMetrix(context);        initView();    }    private void getScreenMetrix(Context context) {        WindowManager WM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);        DisplayMetrics outMetrics = new DisplayMetrics();        WM.getDefaultDisplay().getMetrics(outMetrics);        mScreenWidth = outMetrics.widthPixels;        mScreenHeight = outMetrics.heightPixels;    }    private void initView() {        holder = getHolder();//获得surfaceHolder引用        holder.addCallback(this);        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);//设置类型    }    @Override    public void surfaceCreated(SurfaceHolder holder) {        Log.i(TAG, "surfaceCreated");        if (mCamera == null) {            mCamera = Camera.open();//开启相机            try {                mCamera.setPreviewDisplay(holder);//摄像头画面显示在Surface上            } catch (IOException e) {                e.printStackTrace();            }        }    }    @Override    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {        Log.i(TAG, "surfaceChanged");        mCamera.startPreview();    }    @Override    public void surfaceDestroyed(SurfaceHolder holder) {        Log.i(TAG, "surfaceDestroyed");        mCamera.stopPreview();//停止预览        mCamera.release();//释放相机资源        mCamera = null;        holder = null;    }    @Override    public void onAutoFocus(boolean success, Camera Camera) {        if (success) {            Log.i(TAG, "onAutoFocus success="+success);        }    }}

添加相机和自动聚焦限权

<uses-permission android:name="android.permission.CAMERA" /><uses-feature android:name="android.hardware.camera.autofocus" />

CameraSurfaceView放在布局文件中,这里建议最外层为FrameLayout,后面会用到。如此,我们便有了一个没有照相功能的相机。初次之外,仔细观察相机显示画面,图片是不是变形严重?那是因为我们还没有为相机设置各种参数。在预览前要设置摄像头的分辨率、预览分辨率和图片分辨率的宽高比保持一致。这样图片才不会变形。这是个比较难以理解的部分,想深刻理解还需读者自己动手去实践。

   private void setCameraParams(Camera camera, int width, int height) {        Log.i(TAG,"setCameraParams  width="+width+"  height="+height);        Camera.Parameters parameters = mCamera.getParameters();        // 获取摄像头支持的PictureSize列表        List<Camera.Size> pictureSizeList = parameters.getSupportedPictureSizes();        for (Camera.Size size : pictureSizeList) {            Log.i(TAG, "pictureSizeList size.width=" + size.width + "  size.height=" + size.height);        }        /**从列表中选取合适的分辨率*/        Camera.Size picSize = getProperSize(pictureSizeList, ((float) height / width));        if (null == picSize) {            Log.i(TAG, "null == picSize");            picSize = parameters.getPictureSize();        }        Log.i(TAG, "picSize.width=" + picSize.width + "  picSize.height=" + picSize.height);         // 根据选出的PictureSize重新设置SurfaceView大小        float w = picSize.width;        float h = picSize.height;        parameters.setPictureSize(picSize.width,picSize.height);        this.setLayoutParams(new FrameLayout.LayoutParams((int) (height*(h/w)), height));        // 获取摄像头支持的PreviewSize列表        List<Camera.Size> previewSizeList = parameters.getSupportedPreviewSizes();        for (Camera.Size size : previewSizeList) {            Log.i(TAG, "previewSizeList size.width=" + size.width + "  size.height=" + size.height);        }        Camera.Size preSize = getProperSize(previewSizeList, ((float) height) / width);        if (null != preSize) {            Log.i(TAG, "preSize.width=" + preSize.width + "  preSize.height=" + preSize.height);            parameters.setPreviewSize(preSize.width, preSize.height);        }        parameters.setJpegQuality(100); // 设置照片质量        if (parameters.getSupportedFocusModes().contains(android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {            parameters.setFocusMode(android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);// 连续对焦模式        }        mCamera.cancelAutoFocus();//自动对焦。        // 设置PreviewDisplay的方向,效果就是将捕获的画面旋转多少度显示        // TODO 这里直接设置90°不严谨,具体见https://developer.android.com/reference/android/hardware/Camera.html#setPreviewDisplay%28android.view.SurfaceHolder%29        mCamera.setDisplayOrientation(90);        mCamera.setParameters(parameters);    }    /**     * 从列表中选取合适的分辨率     * 默认w:h = 4:3     * <p>tip:这里的w对应屏幕的height     *            h对应屏幕的width<p/>     */    private Camera.Size getProperSize(List<Camera.Size> pictureSizeList, float screenRatio) {        Log.i(TAG, "screenRatio=" + screenRatio);        Camera.Size result = null;        for (Camera.Size size : pictureSizeList) {            float currentRatio = ((float) size.width) / size.height;            if (currentRatio - screenRatio == 0) {                result = size;                break;            }        }        if (null == result) {            for (Camera.Size size : pictureSizeList) {                float curRatio = ((float) size.width) / size.height;                if (curRatio == 4f / 3) {// 默认w:h = 4:3                    result = size;                    break;                }            }        }        return result;    }

进去的是屏幕宽高,出来的是调整好了的参数。在surfaceChanged方法中执行mCamera.startPreview(); 前调用setCameraParams(mCamera, mScreenWidth, mScreenHeight); 就可以了。最后要在AndroidManifest.xml里设置activity的方向android:screenOrientation="portrait"代码里有很多注释,其中也有我自己调试时候的Log,大家可以自己调试下,看看不同参数的效果。昨天调参数搞到一点多,都在折腾这个函数。唉,一把辛酸泪。
身为一个相机,居然不能照相?真是太丢脸了!下面给我们的相机添加上照相的功能。照相核心代码就一句:mCamera.takePicture(null, null, jpeg);
可以看到takePicture方法有三个参数,分别是ShutterCallbackPictureCallbackPictureCallback。这里我们只用了PictureCallback

    // 拍照瞬间调用    private Camera.ShutterCallback shutter = new Camera.ShutterCallback() {        @Override        public void onShutter() {            Log.i(TAG,"shutter");        }    };    // 获得没有压缩过的图片数据    private Camera.PictureCallback raw = new Camera.PictureCallback() {        @Override        public void onPictureTaken(byte[] data, Camera Camera) {            Log.i(TAG, "raw");        }    };    //创建jpeg图片回调数据对象    private Camera.PictureCallback jpeg = new Camera.PictureCallback() {        @Override        public void onPictureTaken(byte[] data, Camera Camera) {            BufferedOutputStream bos = null;            Bitmap bm = null;            try {                // 获得图片                bm = BitmapFactory.decodeByteArray(data, 0, data.length);                if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {                    Log.i(TAG, "Environment.getExternalStorageDirectory()="+Environment.getExternalStorageDirectory());                    String filePath = "/sdcard/dyk"+System.currentTimeMillis()+".jpg";//照片保存路径                    File file = new File(filePath);                    if (!file.exists()){                        file.createNewFile();                    }                    bos = new BufferedOutputStream(new FileOutputStream(file));                    bm.compress(Bitmap.CompressFormat.JPEG, 100, bos);//将图片压缩到流中                }else{                    Toast.makeText(mContext,"没有检测到内存卡", Toast.LENGTH_SHORT).show();                }            } catch (Exception e) {                e.printStackTrace();            } finally {                try {                    bos.flush();//输出                    bos.close();//关闭                    bm.recycle();// 回收bitmap空间                    mCamera.stopPreview();// 关闭预览                    mCamera.startPreview();// 开启预览                } catch (IOException e) {                    e.printStackTrace();                }            }        }    };

jpeg#onPictureTaken()里。我们将存储照片信息的byte[] data解析成bitmap,然后转换成JPG格式的图片保存在SD卡中。注意finally中最后两句mCamera.stopPreview();// 关闭预览 mCamera.startPreview();// 开启预览 上文也提到:当调用camera.takePiture()方法后,camera关闭了预览,这时需要调用startPreview()来重新开启预览。如果不再次开启预览,则会一直停留在拍摄照片画面。为了方便外部调用拍照。这里我暴露了一个方法供外部拍照。

    public void takePicture(){        //设置参数,并拍照        setCameraParams(mCamera, mScreenWidth, mScreenHeight);        // 当调用camera.takePiture方法后,camera关闭了预览,这时需要调用startPreview()来重新开启预览        mCamera.takePicture(null, null, jpeg);    }

在布局文件中添加一个Button,点击Button执行takePicture()方法。不要忘了添加写SD卡限权

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

至此,一个具有照相并保存拍摄图片功能的相机就做出来了。But,我们就此满足了吗?要是为了这些简单的功能我也不会写这篇博客。这只是个开始

真正的开始

别人APP在照相的时候,屏幕上居然可以显示像效果图那样的框框啦、辅助点啦、图片bulabulabula~。在网上搜索一番实现方式,再加上一些自己的理解,构成了这篇博客。
上文布局文件一直没有贴,现在贴出来大家先扫一眼,有些控件会在接下来展示

<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <com.dyk.cameratest.view.CameraSurfaceView        android:id="@+id/cameraSurfaceView"        android:layout_width="match_parent"        android:layout_height="match_parent" />    <com.dyk.cameratest.view.RectOnCamera        android:layout_width="match_parent"        android:layout_height="match_parent" />    <RelativeLayout        android:layout_width="match_parent"        android:layout_height="match_parent">        <Button            android:layout_alignParentBottom="true"            android:layout_centerHorizontal="true"            android:layout_marginBottom="20dp"            android:id="@+id/takePic"            android:layout_width="80dp"            android:layout_height="50dp"            android:background="#88427ac7"            android:text="拍照"            android:textColor="#aaa" />    </RelativeLayout></FrameLayout>

布局文件的最外层是个FrameLayout,我们知道FrameLayout是自带覆盖效果的。由来这个思路接下来就很简单了。编程重要的是思想,思想有了,其余的就剩具体的实现细节。

自定义边边框框

为了和CameraSurfaceView区分开,再自定义一个RectOnCamera专门用来画边边框框这些东西。这样做还一个好处是方便维护,不至于将所有东西都放在一个View中。

RectOnCamera

package com.dyk.cameratest.view;.../** * Created by 一口仨馍 on 2016/4/7. */public class RectOnCamera extends View {    private static final String TAG = "CameraSurfaceView";    private int mScreenWidth;    private int mScreenHeight;    private Paint mPaint;    private RectF mRectF;    // 圆    private Point centerPoint;    private int radio;    public RectOnCamera(Context context) {        this(context, null);    }    public RectOnCamera(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public RectOnCamera(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        getScreenMetrix(context);        initView(context);    }    private void getScreenMetrix(Context context) {        WindowManager WM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);        DisplayMetrics outMetrics = new DisplayMetrics();        WM.getDefaultDisplay().getMetrics(outMetrics);        mScreenWidth = outMetrics.widthPixels;        mScreenHeight = outMetrics.heightPixels;    }    private void initView(Context context) {        mPaint = new Paint();        mPaint.setAntiAlias(true);// 抗锯齿        mPaint.setDither(true);// 防抖动        mPaint.setColor(Color.RED);        mPaint.setStrokeWidth(5);        mPaint.setStyle(Paint.Style.STROKE);// 空心        int marginLeft = (int) (mScreenWidth*0.15);        int marginTop = (int) (mScreenHeight * 0.25);        mRectF = new RectF(marginLeft, marginTop, mScreenWidth - marginLeft, mScreenHeight - marginTop);        centerPoint = new Point(mScreenWidth/2, mScreenHeight/2);        radio = (int) (mScreenWidth*0.1);    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        mPaint.setColor(Color.RED);        canvas.drawRect(mRectF, mPaint);        mPaint.setColor(Color.WHITE);        Log.i(TAG, "onDraw");        canvas.drawCircle(centerPoint.x,centerPoint.y, radio,mPaint);// 外圆        canvas.drawCircle(centerPoint.x,centerPoint.y, radio - 20,mPaint); // 内圆    }}

这里简单的画了一个类似二维码扫描的框框,还有一个类似聚焦的内外圆。那么问题来了,聚焦的内外圆要随着手指滑而改变位置,而且要有聚焦的效果。可又和具有聚焦功能的CameraSurfaceView不是同一个类,不仅如此聚焦内外圆还完全覆盖了CameraSurfaceView。要处理这种问题,需要接口回调。这就是思想下面的细节。现在虽然确定接口回调,但还有一个问题,CameraSurfaceView类和RectOnCamera类中都没有对方的对象或者引用。没错,通过共同持有RectOnCameraCameraSurfaceViewActivity可以实现此功能。下面是具体的实现方法

动起来

首先,想要随着手指的滑动而改变RectOnCamera的位置肯定是要复写onTouchEvent()方法

    @Override    public boolean onTouchEvent(MotionEvent event) {        switch (event.getAction()){            case MotionEvent.ACTION_DOWN:            case MotionEvent.ACTION_MOVE:            case MotionEvent.ACTION_UP:                int x = (int) event.getX();                int y = (int) event.getY();                centerPoint = new Point(x, y);                invalidate();                return true;        }        return true;    }

其次,定义回调接口

 private IAutoFocus mIAutoFocus;    /** 聚焦的回调接口 */    public interface  IAutoFocus{        void autoFocus();    }    public void setIAutoFocus(IAutoFocus mIAutoFocus) {        this.mIAutoFocus = mIAutoFocus;    }

onTouchEvent()return前加入

  if (mIAutoFocus != null){      mIAutoFocus.autoFocus();  }

至此我们的回调接口已经定义好了,此时还需要CameraSurfaceView暴露一个聚焦方法,以便Activity调用

    public void setAutoFocus(){        mCamera.autoFocus(this);    }

准备工作已经全部完成,下面请看Activity的具体实现:

public class MainActivity extends Activity implements View.OnClickListener,RectOnCamera.IAutoFocus{    private CameraSurfaceView mCameraSurfaceView;    private RectOnCamera mRectOnCamera;    private Button takePicBtn;    private boolean isClicked;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        requestWindowFeature(Window.FEATURE_NO_TITLE);        // 全屏显示             getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,                            WindowManager.LayoutParams.FLAG_FULLSCREEN);        setContentView(R.layout.activity_main);        mCameraSurfaceView = (CameraSurfaceView) findViewById(R.id.cameraSurfaceView);        mRectOnCamera = (RectOnCamera) findViewById(R.id.rectOnCamera);        takePicBtn= (Button) findViewById(R.id.takePic);        mRectOnCamera.setIAutoFocus(this);        takePicBtn.setOnClickListener(this);    }    @Override    public void onClick(View v) {        switch (v.getId()){            case R.id.takePic:                mCameraSurfaceView.takePicture();                break;            default:                break;        }    }    @Override    public void autoFocus() {        mCameraSurfaceView.setAutoFocus();    }}

可以看到,MainActivity实现了IAutoFocus接口,并且在复写的IAutoFocus#autoFocus()方法中,调用了CameraSurfaceView暴露出来的方法setAutoFocus()。至此,在RectOnCamera每次的滑动过程中都会改变聚焦内外圆的位置,还会增加聚焦功能。一心二用甚至一心多用岂不是更好。

结束语

在经历两次断电没保存和一次CSDN服务器错误内容丢失之后终究还是完成了这篇博客,实属不易。十分感谢能听我啰嗦到结尾~

PS:Demo界面并没有做的很精致,只是提供了一种思路。按照此思路能做出比较华丽的效果,授人以鱼不如授人以渔。

2016.10.12 在经历了上述种种磨难之后,终于发表了这篇博文,然而发表没几天,被我自己覆盖了。这下博文是真的丢了。心塞ing。今天没事百度下自己CSDN昵称“一口仨馍”,发现其他网站爬过这篇博文,随后我复制了自己原创的博文,再次发表。感谢那些爬我博文还不署名的网站。谢谢你全家。

源码下载

http://download.csdn.net/detail/qq_17250009/9484160

10 1
原创粉丝点击