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

概述

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

启动相机的两种方式

1.直接启动系统相机

  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.自定义启动相机

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

自定义相机的一般步骤

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

一般步骤的代码演示

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();//自动对焦。
        mCamera.setDisplayOrientation(90);// 设置PreviewDisplay的方向,效果就是将捕获的画面旋转多少度显示
        mCamera.setParameters(parameters);

    }

    /**
     * 从列表中选取合适的分辨率
     * 默认w:h = 4:3
     * <p>注意:这里的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方法有三个参数,分别是ShutterCallback、PictureCallback和PictureCallback。这里我们只用了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;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;

/**
 * Created by dyk 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类中都没有对方的对象或者引用。没错,通过共同持有RectOnCamera和CameraSurfaceView的Activity可以实现此功能。下面是具体的实现方法。

动起来

首先,想要随着手指的滑动而改变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界面并没有做的很精致,只是提供了一种思路。按照此思路能做出比较华丽的效果,授人以鱼不如授人以渔。


源码下载:http://download.csdn.net/detail/qq_17250009/9484160

时间: 2024-08-05 06:49:27

Android 手把手带你玩转自定义相机的相关文章

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

本文已授权微信公众号<鸿洋>原创首发,转载请务必注明出处. 概述 相机差点儿是每一个APP都要用到的功能,万一老板让你定制相机方不方?反正我是有点方. 关于相机的两天奋斗总结免费送给你. Intent intent = new Intent(); intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); startActivity(intent); 或者指定返回图片的名称mCurrentPhotoFile. Intent intent = new I

wing带你玩转自定义view系列(2) 简单模仿qq未读消息去除效果

上一篇介绍了贝塞尔曲线的简单应用 仿360内存清理效果 这一篇带来一个  两条贝塞尔曲线的应用 : 仿qq未读消息去除效果. 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50503630 老规矩,先上效果图: qq的未读消息去除很炫酷,其实就是用了两条贝塞尔曲线,我们按思路来,先来画两个圆,及两条贝塞尔曲线,辅助点为圆心y坐标的一半.我们把下面移动的圆,叫做mMoveCircle. 这样一画,就很简单明了了对不对.只要在拖动的时候

wing带你玩转自定义view系列(1) 仿360内存清理效果

本篇是接自 手把手带你做自定义view系列 宗旨都是一样,带大家一起来研究自定义view的实现,与其不同的是本系列省去了简单的坐标之类的讲解,重点在实现思路,用简洁明了的文章,来与大家一同一步步学习. 转载请注明出处:http://blog.csdn.net/wingichoy/article/details/50500479 上一篇介绍了:神奇的贝塞尔曲线,这篇就来研究其应用. 我自己的学习方法是:学习了贝塞尔曲线之后,去研究他的规律,然后开始联想有没有见过类似的效果,最后自己去研究实现,在没

【Android】自定义相机的实现(支持连续拍照、前后摄像头切换、连续对焦)

~转载请注明http://blog.csdn.net/u013015161/article/details/46921257 介绍 这几天,写了一个自定义照相机的demo,支持连续拍照和摄像头切换.由于自己以前没接触过相关的编程,也算是一个学习的过程,在这里做一下记录,同时也分享出来,并附上源码和工程. 效果如图: 左上角switch切换摄像头,右边snap按钮进行拍照. 一般流程 Android进行拍照,需要调用摄像头类android.hardware.Camera.而要进行预览,则需要用an

手把手教你玩转CSS3 3D技术

手把手教你玩转 CSS3 3D 技术 要玩转css3的3d,就必须了解几个词汇,便是透视(perspective).旋转(rotate)和移动(translate).透视即是以现实的视角来看屏幕上的2D事物,从而展现3D的效果.旋转则不再是2D平面上的旋转,而是三维坐标系的旋转,就包括X轴,Y轴,Z轴旋转.平移同理. 当然用理论来说明,估计你还不明白.下面是3个gif: 沿着X轴旋转 沿着Y轴旋转 沿着Z轴旋转 旋转应该没问题了,那理解平移起来就比较容易了,就是在在X轴.Y轴.z轴移动. 你可能

大牛带你玩转 CSS3 3D 技术

css3的3d起步 要玩转css3的3d,就必须了解几个词汇,便是透视(perspective).旋转(rotate)和移动(translate).透视即是以现实的视角来看屏幕上的2D事物,从而展现3D的效果.旋转则不再是2D平面上的旋转,而是三维坐标系的旋转,就包括X轴,Y轴,Z轴旋转.平移同理. 当然用理论来说明,估计你还不明白.下面是3个gif: 沿着X轴旋转 沿着Y轴旋转 沿着Z轴旋转 旋转应该没问题了,那理解平移起来就比较容易了,就是在在X轴.Y轴.z轴移动. 你可能会说透视比较不好理

手把手带你做一个超炫酷loading成功动画view Android自定义view

写在前面: 本篇可能是手把手自定义view系列最后一篇了,实际上我也是一周前才开始真正接触自定义view,通过这一周的练习,基本上已经熟练自定义view,能够应对一般的view需要,那么就以本篇来结尾告一段落,搞完毕设的开题报告后去学习新的内容. 有人对我说类似的效果网上已经有了呀,直接拿来就可以用,为什么还要写.我个人的观点是:第三方控件多数不能完全满足UI的要求,如果需要修改,那么必须理解他的实现,所以很有必要自己去写一款出来,成为程序的创造者,而不单单是使用者.所以,写一写已经实现的效果,

Android调用系统相机、自定义相机、处理大图片

Android调用系统相机和自定义相机实例 本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显示出来,该例子也会涉及到Android加载大图片时候的处理(避免OOM),还有简要提一下有些人SurfaceView出现黑屏的原因. Android应用拍照的两种方式,下面为两种形式的Demo展示出来的效果.    知识点: 一.调用系统自带的相机应用 二.自定义我们自己的拍照界面 三.关于计算机解析图片原理(如何正确加载图片到Android应用中) 所需

android开发——自定义相机开发总结

最近这段时间我一直在开发自定义相机,谷歌了些网上的demo,发现有很多各种各样的问题.最终还是从API的camera类开始学习,进行改进.下面对之前的实现进行一些总结. 官方camera API: http://developer.android.com/guide/topics/media/camera.html 中文翻译: http://www.cnblogs.com/over140/archive/2011/11/16/2251344.html 自定义相机大致实现流程: 预览Camera这