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


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

    概述

    相机差点儿是每一个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是自带覆盖效果的。由来这个思路接下来就非常easy了。

    编程重要的是思想,思想有了,其余的就剩详细的实现细节。

    自己定义边边框框

    为了和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每次的滑动过程中都会改变聚焦内外圆的位置,还会添加聚焦功能。

    一心二用甚至一心多用岂不是更好。

    结束语

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

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

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

    谢谢你全家。

    源代码下载

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

  • 相关阅读:
    高通平台msm8953 Linux DTS(Device Tree Source)设备树详解之二(DTS设备树匹配过程)
    Linux DTS(Device Tree Source)设备树详解之一(背景基础知识篇)
    ACPI Table 与 Device Tree
    Electron 的 安装
    adb连接安卓设备失败failed to start daemon
    Linux greybus
    Android安卓版本、API级别和Linux内核对应关系
    「转」Android编译选项中的eng、user、user-debug
    高通SOC启动流程
    Android for MSM Project
  • 原文地址:https://www.cnblogs.com/yfceshi/p/7382534.html
Copyright © 2020-2023  润新知