分享

android.hardware.camera2详解(实时更新,未完待续...)

 lifei_szdz 2019-02-22

看到有些读者对博客提出的疑问,在此真诚说一句实在抱歉,本人是一名在校非科班出身大学生,之前 由于是期末季,所以非常忙,没有空更新,一月二十几号才放假,非常抱歉,评论所诉内容应该都已解决,只是我没有更新,近期将会更新!再次致歉!

前言:原有的android.hardware.camera已经被google废除,改用功能更强大的camera2,camera2的复杂度远远超过camera,所以写这篇博客之前也是经历了各种心态爆炸。非常费解,谷歌官网竟然没有给出一个好的demo,导致我只能去网上各处搜博客,其间大多数博客真的让我恼火,不是写的有问题,就是介绍不全,还有就是完全不解释,我很讨厌这种不负责的博客,对我来说,简直就是扯淡!(实在忍不住,所以爆了点粗口,望谅解)。出于以上原因,我会尽我最大努力,写出一篇逻辑清晰,易于理解,代码完整的博客。

一,让我们先来了解一下其大致原理和使用流程
先看两张图:

可以看到图中有个pipeline,这是camera2引入的概念,在我看来,这个单词给定的很恰当。通过在Android Device(代码中的一个对象CameraDevice)和Camera Device(照相的硬件设备)搭建一个通道,来进行两者信息的交流,Android Device向Camera Device发出请求——CaptureRequest,这个CaptureRequest可以设置各种我们想要设置的相机属性——自动聚焦,闪光灯等等,而Camera Device拍摄对应影像,收集图像信息,通过CameraMatadata传递给Android Device,从而使Android程序端可以获取到照片数据,进行对应的处理。
另外,我们还注意到的上面有个箭头指向Surface,Android端将Camera Device传递过来的图像信息投射到Surface上,实现预览的功能。

这张图展示各个类之间的关系,让我们来分别介绍一下:

CameraManager:管理Camera的类,通过这个类我们可以打开Camera——对应方法为CameraManager.openCamera;获取各种Camera参数——对应方法为CameraManager.getCameraCharacteristics。这里我们先大概了解就行,稍后都会介绍。
其中打开一个Camera时,会有对应的回调接口——CameraDevice.StateCallback,处理相机打开失败,成功后者错误的情况。

CameraCharacteristics:含有Camera的各种参数,如闪光灯,自动对焦,自动曝光等等。

CameraDevice:java代码中代表Camera的对象,可以关闭相机,向相机硬件端发出请求等等。

CameraCaptureSession:session直译为”会议“,这是一个很形象的词,其实就是上文提到的pipeline,程序中通过创造一个CameraCaptureSession,在安卓端和相机硬件端建立管道,从而可以获取拍摄的图片信息。
在创造一个会议时,会回调两个接口——
StateCallback:处理session建立成功和失败的情况,通常在这里会进行预览的一些初始化设置。
CaptureCallback:捕获图像成功、失败、进行时等情况的处理。

CameraRequest:安卓端相机参数的设定请求,会在创建session时被当作参数。

CameraMetadata:控制相机和带有相机参数的基础类,它的子类是:
CameraCharacteristics,CaptureRequest,CaptureResult。

CaptureResult:从图像传感器捕获单个图像的结果的子集。包含捕获硬件(传感器,镜头,闪存),处理流水线,控制算法和输出缓冲区的最终配置的一个子集。概念有点难懂,可以去官网仔细了解下。


这张图很重要,大家要仔细看看,稍后的代码的逻辑结构就是依照这个而来的,不一定要全部看懂,可以结合下面的代码流程去理解。
有了这些基础知识储备,让我们来打造一个最简单的android.hardware.camera2 demo吧!
二,最简单的camera2 demo

(一)添加权限,以及部分依赖
在app/gradle中添加依赖:

 compile 'de.hdodenhof:circleimageview:2.1.0'

这是一个具有将任何形状图片变为圆形功能的开源View,图片中左下角的圆形图片就是调用了这个控件。
在AndroidManifest.xml添加相关权限:

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

这里的读取和读入功能以后扩充的代码要用到。
(二)为你的相机创建个性布局
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas./apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
>
    <SurfaceView
        android:id="@+id/mFirstSurfaceView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        />
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#0E0E0E">
        <!--提供预览功能,以及从相册选取图片进行识别功能-->
        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/img_show"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:layout_alignParentStart="true"
            android:layout_alignParentBottom="true"
            android:src="@mipmap/ic_launcher"
            android:layout_marginStart="10dp"/>
        <Button
            android:id="@+id/take_picture"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:layout_alignParentBottom="true"
            android:text="拍照"/>
        <!--点击观看识别结果-->
        <Button
            android:id="@+id/recognition_result"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:layout_alignParentBottom="true"
            android:text="结果"
            android:layout_marginEnd="10dp"/>
    </RelativeLayout>
</LinearLayout>

其中SurfaceView是用来预览拍摄图片的。CircleImageView是我们第一步提到的开源库。
(三)在MainActivity中实现相关功能
老规矩,先贴整体代码:
MainActivity.class

package com.example.wordrecognition;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Camera;
import android.graphics.ImageFormat;
import android.hardware.camera2.*;
import android.media.Image;
import android.media.ImageReader;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;
import android.Manifest.permission.*;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.jar.Manifest;

public class MainActivity extends AppCompatActivity {
    private CameraManager mCameraManager;
    private SurfaceView mSurfaceView;
    private SurfaceHolder mSurfaceViewHolder;
    private Handler mHandler;
    private String mCameraId;
    private ImageReader mImageReader;
    private CameraDevice mCameraDevice;
    private CaptureRequest.Builder mPreviewBuilder;
    private CameraCaptureSession mSession;
    private ImageView img_show;
    private Button take_picture_bt;
    private Handler mainHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ActionBar actionBar=getSupportActionBar();
        if(actionBar!=null){
            actionBar.hide();
        }
        img_show= (ImageView) findViewById(R.id.img_show);
        take_picture_bt=(Button)findViewById(R.id.take_picture);
        initSurfaceView();//初始化SurfaceView
        /**
         * 拍照按钮监听
         */
        take_picture_bt.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                takePicture();
            }
        });

    }
public void initSurfaceView(){
    mSurfaceView = (SurfaceView) findViewById(R.id.mFirstSurfaceView);
    mSurfaceViewHolder = mSurfaceView.getHolder();//通过SurfaceViewHolder可以对SurfaceView进行管理
    mSurfaceViewHolder.addCallback(new SurfaceHolder.Callback() {
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            initCameraAndPreview();
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            //释放camera
            if (mCameraDevice != null) {
                mCameraDevice.close();
                mCameraDevice = null;
            }
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

        }
    });
}
    @TargetApi(19)
    public void initCameraAndPreview() {
        HandlerThread handlerThread = new HandlerThread("My First Camera2");
        handlerThread.start();
        mHandler = new Handler(handlerThread.getLooper());
        mainHandler = new Handler(getMainLooper());//用来处理ui线程的handler,即ui线程
        try {
            mCameraId = "" + CameraCharacteristics.LENS_FACING_FRONT;
            mImageReader = ImageReader.newInstance(mSurfaceView.getWidth(), mSurfaceView.getHeight(), ImageFormat.JPEG,/*maxImages*/7);
            mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mainHandler);//这里必须传入mainHandler,因为涉及到了Ui操作
            mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE);
            if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                return;//按理说这里应该有一个申请权限的过程,但为了使程序尽可能最简化,所以先不添加
            }
            mCameraManager.openCamera(mCameraId, deviceStateCallback, mHandler);
        } catch (CameraAccessException e) {
            Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show();
        }
    }

    private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            //进行相片存储
            mCameraDevice.close();
            Image image = reader.acquireNextImage();
            ByteBuffer buffer = image.getPlanes()[0].getBuffer();
            byte[] bytes = new byte[buffer.remaining()];
            buffer.get(bytes);//将image对象转化为byte,再转化为bitmap
            final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
            if (bitmap != null) {
               img_show.setImageBitmap(bitmap);
            }
        }
    };
    private CameraDevice.StateCallback deviceStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice camera) {
            mCameraDevice = camera;
            try {
                takePreview();
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            if (mCameraDevice != null) {
                mCameraDevice.close();
                mCameraDevice = null;
            }
        }

        @Override
        public void onError(CameraDevice camera, int error) {
            Toast.makeText(MainActivity.this, "打开摄像头失败", Toast.LENGTH_SHORT).show();
        }
    };

    public void takePreview() throws CameraAccessException {
        mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        mPreviewBuilder.addTarget(mSurfaceViewHolder.getSurface());
        mCameraDevice.createCaptureSession(Arrays.asList(mSurfaceViewHolder.getSurface(), mImageReader.getSurface()), mSessionPreviewStateCallback, mHandler);
    }

    private CameraCaptureSession.StateCallback mSessionPreviewStateCallback = new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(@NonNull CameraCaptureSession session) {
            mSession = session;
            //配置完毕开始预览
            try {
                /**
                 * 设置你需要配置的参数
                 */
                //自动对焦
                mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                //打开闪光灯
                mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                //无限次的重复获取图像
                mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
            Toast.makeText(MainActivity.this, "配置失败", Toast.LENGTH_SHORT).show();
        }
    };
    private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() {
        @Override
        public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
            mSession = session;
        }

        @Override
        public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
            mSession = session;
        }

         @Override
        public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
            super.onCaptureFailed(session, request, failure);
        }
    };


    public void takePicture() {
        try {
            CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);//用来设置拍照请求的request
            captureRequestBuilder.addTarget(mImageReader.getSurface());
            // 自动对焦
            captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            // 自动曝光
            captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
            int rotation = getWindowManager().getDefaultDisplay().getRotation();
            CameraCharacteristics cameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId);
            captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation(cameraCharacteristics, rotation));//使图片做顺时针旋转
            CaptureRequest mCaptureRequest = captureRequestBuilder.build();
            mSession.capture(mCaptureRequest, null, mHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
//获取图片应该旋转的角度,使图片竖直
    public int getOrientation(int rotation) {
        switch (rotation) {
            case Surface.ROTATION_0:
                return 90;
            case Surface.ROTATION_90:
                return 0;
            case Surface.ROTATION_180:
                return 270;
            case Surface.ROTATION_270:
                return 180;
            default:
                return 0;
        }
    }
    //获取图片应该旋转的角度,使图片竖直
    private int getJpegOrientation(CameraCharacteristics c, int deviceOrientation) {
        if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN)
            return 0;
        int sensorOrientation = c.get(CameraCharacteristics.SENSOR_ORIENTATION);

        // Round device orientation to a multiple of 90
        deviceOrientation = (deviceOrientation + 45) / 90 * 90;

        // LENS_FACING相对于设备屏幕的方向,LENS_FACING_FRONT相机设备面向与设备屏幕相同的方向
        boolean facingFront = c.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT;
        if (facingFront) deviceOrientation = -deviceOrientation;

        // Calculate desired JPEG orientation relative to camera orientation to make
        // the image upright relative to the device orientation
        int jpegOrientation = (sensorOrientation + deviceOrientation + 360) % 360;

        return jpegOrientation;
    }

}

代码有点长,下面我们来分解它的逻辑
1,隐藏ActionBar等以及基本部件初始化操作

ActionBar actionBar=getSupportActionBar();
        if(actionBar!=null){
            actionBar.hide();
        }
        img_show= (ImageView) findViewById(R.id.img_show);
        take_picture_bt=(Button)findViewById(R.id.take_picture);

2,初始化surfaceView,即照相机预览界面

     mSurfaceView = (SurfaceView) findViewById(R.id.mFirstSurfaceView);
    mSurfaceViewHolder = mSurfaceView.getHolder();//通过SurfaceViewHolder可以对SurfaceView进行管理
    mSurfaceViewHolder.addCallback(new SurfaceHolder.Callback() {
    //SurfaceView被成功创建
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            initCameraAndPreview();
        }
//SurfaceView被销毁
        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            //释放camera
            if (mCameraDevice != null) {
                mCameraDevice.close();
                mCameraDevice = null;
            }
        }
//SurfaceView内容发生改变
        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

        }
    });

首先我们获取到了SurfaceView实例,然后通过SurfaceView获取SurfaceViewHolder,SurfaceViewHolder是用来管理SurfaceView的类,别的类通过SurfaceViewHolder可以对SurfaceView进行编辑。
然后我们通过SurfaceViewHolder为SurfaceView添加回调接口,三个回调接口方法的功能注释已经解释了。在第二个方法中,释放了Camera资源。
而在第一个方法中,SurfaceView被成功创建后,我们紧接着调用initCameraAndPreView,初始化Camera和将相机拍摄界面投射到SurfaceView上。
3,初始化相机和实现相机预览功能

@TargetApi(19)
    public void initCameraAndPreview() {
        HandlerThread handlerThread = new HandlerThread("My First Camera2");
        handlerThread.start();
        mHandler = new Handler(handlerThread.getLooper());
        mainHandler = new Handler(getMainLooper());//用来处理ui线程的handler,即ui线程
        try {
            mCameraId = "" + CameraCharacteristics.LENS_FACING_FRONT;
            mImageReader = ImageReader.newInstance(mSurfaceView.getWidth(), mSurfaceView.getHeight(), ImageFormat.JPEG,/*maxImages*/7);                                mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mainHandler);//这里必须传入mainHandler,因为涉及到了Ui操作
            mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE);
            if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                return;//按理说这里应该有一个申请权限的过程,但为了使程序尽可能最简化,所以先不添加
            }
            mCameraManager.openCamera(mCameraId, deviceStateCallback, mHandler);
        } catch (CameraAccessException e) {
            Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show();
        }
    }
     private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            //进行相片存储
            mCameraDevice.close();
            //mSurfaceView.setVisibility(View.GONE);//存疑
            Image image = reader.acquireNextImage();
            ByteBuffer buffer = image.getPlanes()[0].getBuffer();
            byte[] bytes = new byte[buffer.remaining()];
            buffer.get(bytes);//将image对象转化为byte,再转化为bitmap
            final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
            if (bitmap != null) {
               img_show.setImageBitmap(bitmap);
            }
        }
    };

在这步中首先我们初始化了两个线程:

        HandlerThread handlerThread = new HandlerThread("My First Camera2");
        handlerThread.start();
        mHandler = new Handler(handlerThread.getLooper());
        mainHandler = new Handler(getMainLooper());//用来处理ui线程的handler,即ui线程

关于HandlerThread不熟悉的读者可以看下鸿洋大神的博客——HandlerThread.
mHandler用来处理普通线程,mainHandler用来处理主线程——ui线程。

然后我们设置了CameraId:
前置摄像头:LENS_FACING_BACK,名字不要弄反了。
后置摄像头:LENS_FACING_FRONT。

设置ImageReader,用来读取拍摄图像的类,ImageReader.newInstance方法的原型:

ImageReader.newInstance(int width,int height,int format,int maxImages);
其它参数好理解,maxImages代表用户想读取的最大Image对象数量。

然后我们为ImageReader设置了监听接口:

mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mainHandler);//这里必须传入mainHandler,因为涉及到了Ui操作

mOnImageAvailiableListener对象如下:

 private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            //进行相片存储和展示
            mCameraDevice.close();
            Image image = reader.acquireNextImage();
            ByteBuffer buffer = image.getPlanes()[0].getBuffer();
            byte[] bytes = new byte[buffer.remaining()];
            buffer.get(bytes);//将image对象转化为byte,再转化为bitmap
            final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
            if (bitmap != null) {
               img_show.setImageBitmap(bitmap);
            }
        }
    };

这个接口在图片拍摄完成后就会回调,这个方法中我们关闭了相机,并进行了图片的读取,其中buffer.remaining返回buffer的元素数量,image.getPlanes()代表获取图片像素信息组成的数组。
同时还要注意到,我们设置监听接口时还传入了mainHandler,这代表我们可以在回调方法中进行ui操作。

紧接着获取CameraManager,并打开了Camera:

mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE);
            if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                return;//按理说这里应该有一个申请权限的过程,但为了使程序尽可能最简化,所以先不添加
            }
            mCameraManager.openCamera(mCameraId, deviceStateCallback, mHandler);
        } catch (CameraAccessException e) {
            Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show();
        }

注意打开Camera之前一定要动态申请权限,这里暂时还没写,运行时我是直接在手机打开相关权限的。否则无法发运行!!!
在打开Camera时传入了CameraDevice.StateCallback,用来反馈相机工作状态:

private CameraDevice.StateCallback deviceStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice camera) {
            mCameraDevice = camera;
            try {
                takePreview();
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            if (mCameraDevice != null) {
                mCameraDevice.close();
                mCameraDevice = null;
            }
        }

        @Override
        public void onError(CameraDevice camera, int error) {
            Toast.makeText(MainActivity.this, "打开摄像头失败", Toast.LENGTH_SHORT).show();
        }
    };

相信聪明的读者从字面意思就可以理解代码的意义了,就不细说了。
4,显示预览界面,进行正式预览
在成功打开相机后,回调onOpened方法,调用takePreview()方法,进行正式的预览,经过这步,我们就可以实时看到拍摄的画面。

 public void takePreview() throws CameraAccessException {
        mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        mPreviewBuilder.addTarget(mSurfaceViewHolder.getSurface());
        mCameraDevice.createCaptureSession(Arrays.asList(mSurfaceViewHolder.getSurface(), mImageReader.getSurface()), mSessionPreviewStateCallback, mHandler);
    }
 private CameraCaptureSession.StateCallback mSessionPreviewStateCallback = new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(@NonNull CameraCaptureSession session) {
            mSession = session;
            //配置完毕开始预览
            try {
                /**
                 * 设置你需要配置的参数
                 */
                //自动对焦
                mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                //打开闪光灯
                mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                //无限次的重复获取图像
                mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
            Toast.makeText(MainActivity.this, "配置失败", Toast.LENGTH_SHORT).show();
        }
    };
    private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() {
        @Override
        public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
            mSession = session;
        }

        @Override
        public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
            mSession = session;
        }
         @Override
        public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
            super.onCaptureFailed(session, request, failure);
        }
    };
 mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

mPreviewBuilder是前文讲过的CaptureRequest的Builder(Builder设计模式),用来对CaptureRequest进行编辑。这里有个参数——CameraDevice.TEMPLATE_PREVIEW:看名字可以猜到,这是表明这个request是针对于相机预览界面的。

 mPreviewBuilder.addTarget(mSurfaceViewHolder.getSurface());

将request设置的参数数据应用于SurfaceView对应的surface,request设置的参数形成的图像数据都会保存在SurfaceView的surface中。
来简单介绍下Surface:

应用产生的图像数据保存的地方,SurfaceView和ImageReader都有一个对应的surface。
mCameraDevice.createCaptureSession(Arrays.asList(mSurfaceViewHolder.getSurface(), mImageReader.getSurface()), mSessionPreviewStateCallback, mHandler);

这个在前文也提到过,在安卓端和相机硬件端建立通道,进行信息的交换。
我们来看看这个方法的原型:

createCaptureSession(List<Surface> outputs, CameraCaptureSession.StateCallback callback, Handler handler)

只有第一个参数有点难理解,它是在声明相机拍摄图像数据的存储点——Surface对象。
再回到我们程序中,第一个参数是:

Arrays.asList(mSurfaceViewHolder.getSurface(), mImageReader.getSurface()

即是SurfaceView和ImageReader的Surface组成的一个数组,表明图像数据将会输出到这两个地方。

再看mSessionPreviewStateCallback:

 private CameraCaptureSession.StateCallback mSessionPreviewStateCallback = new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(@NonNull CameraCaptureSession session) {
            mSession = session;
            //配置完毕开始预览
            try {
                /**
                 * 设置你需要配置的参数
                 */
                //自动对焦
                mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                //打开闪光灯
                mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                //无限次的重复获取图像
                mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }


        @Override
        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
            Toast.makeText(MainActivity.this, "配置失败", Toast.LENGTH_SHORT).show();
        }
    };

这是创建session的一个回调,前文也提到过。
方法中分别对session建立成功和失败的情况进行了处理,成功后,设置了一系列参数,最后调用:

            //无限次的重复获取图像
           mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);

这个很好理解,预览的过程其实就是不断重复请求图像数据的过程,所以叫”repeating”。
其原型为:

setRepeatingRequest(CaptureRequest request, CameraCaptureSession.CaptureCallback listener, Handler handler)

注意第二个参数,虽然我们目前的代码设置为null没有用到,但是还是给出了这个回调接口的代码:

 private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() {
 //拍摄完全完成并且成功,拍摄图像数据可用时回调
        @Override
        public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
            mSession = session;
        }
//拍摄进行中,但拍摄图像数据部分可用时回调
        @Override
        public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
            mSession = session;
        }
    };
    //拍摄失败时回调
     @Override
        public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
            super.onCaptureFailed(session, request, failure);
        }

我们可以在这个回调做图片的保存工作,但当然还是出于最简化原则,暂时不写。
5,一番辛苦,我们终于可以拍照片了!!!

 public void takePicture() {
        try {
            CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);//用来设置拍照请求的request
            captureRequestBuilder.addTarget(mImageReader.getSurface());
            // 自动对焦
            captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            // 自动曝光
            captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
            int rotation = getWindowManager().getDefaultDisplay().getRotation();
            CameraCharacteristics cameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId);
            captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation(cameraCharacteristics, rotation));//使图片做顺时针旋转
            //captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,getOrientaion(rotation))两种方法都可以。
            CaptureRequest mCaptureRequest = captureRequestBuilder.build();
            mSession.capture(mCaptureRequest, null, mHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);//用来设置拍照请求的request

这个之前讲过,只是参数改变了——CameraDevice.TEMPLATE_STILL_CAPTURE,这次是针对拍摄图片的request。

接下来挑关键的讲:

int rotation = getWindowManager().getDefaultDisplay().getRotation();

获取屏幕的方向,你旋转屏幕这个值会发生相应变化,因为对这个理解也不深,不透彻,不敢乱讲,大家可以google一下,很多这个的讲解,如果我以后理解深了,就会写进博客。

captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation(cameraCharacteristics, rotation));//使图片做顺时针旋转
            //captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,getOrientaion(rotation))两种方法都可以。

这个是设置图片的方向使图片竖直放置,注意第二个参数,代表应该图片应该旋转的角度,因为这个比较复杂(真的复杂,,,),大家想了解可以看看这篇博客——Android相机开发那些坑
代码中我提供了两种第二个参数:
网友提出的:

//获取图片应该旋转的角度,使图片竖直
    public int getOrientation(int rotation) {
        switch (rotation) {
            case Surface.ROTATION_0:
                return 90;
            case Surface.ROTATION_90:
                return 0;
            case Surface.ROTATION_180:
                return 270;
            case Surface.ROTATION_270:
                return 180;
            default:
                return 0;
        }
    }

Google官网上的:

 //获取图片应该旋转的角度,使图片竖直
    private int getJpegOrientation(CameraCharacteristics c, int deviceOrientation) {
        if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN)
            return 0;
        int sensorOrientation = c.get(CameraCharacteristics.SENSOR_ORIENTATION);

        // Round device orientation to a multiple of 90
        deviceOrientation = (deviceOrientation + 45) / 90 * 90;

        // LENS_FACING相对于设备屏幕的方向,LENS_FACING_FRONT相机设备面向与设备屏幕相同的方向
        boolean facingFront = c.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT;
        if (facingFront) deviceOrientation = -deviceOrientation;

        // Calculate desired JPEG orientation relative to camera orientation to make
        // the image upright relative to the device orientation
        int jpegOrientation = (sensorOrientation + deviceOrientation + 360) % 360;

        return jpegOrientation;
    }

目前发现两种都可以,所以两种都放上来,但个人也不是全部理解,所以很抱歉,暂时还不能给大家做解释,不过相信聪明的你们可以通过阅读上面的博客理解它!

mSession.capture(mCaptureRequest, null, mHandler);

这是最关键的一步,通过这步,拍照动作才真正完成。
其原型是:

capture(CaptureRequest request, CameraCaptureSession.CaptureCallback listener, Handler handler)

相对简单,就不解释了。

经过上述有点复杂的步骤,相信大家都可以做出一个最简单的相机应用

三,进阶应用之图片的保存
我将它集成在一个方法中,方便需要时插入到代码中:

//将照片存储在相机照片存储位置,这里采用bitmap方式保存
    public String savePicture(byte[] imgBytes) {
        pictureId++;
        String imgPath = Environment.getExternalStorageDirectory() + "/DCIM/Camera/WordRecognition_picture" + pictureId + ".jpg";
        Bitmap bitmap = BitmapFactory.decodeByteArray(imgBytes, 0, imgBytes.length);//图像数据被转化为bitmap
        File outputImage = new File(imgPath);
        FileOutputStream outputStream = null;
        try {
            if (outputImage.exists()) {
                outputImage.delete();//存在就删除
            }
            outputImage.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            outputStream = new FileOutputStream(outputImage);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);//第二个参数为压缩质量
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        try {
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
//及时更新到系统相册
        MediaScannerConnection.scanFile(this, new String[]{Environment.getExternalStorageDirectory() + "//DCIM//Camera//WordRecogniton_picture" + pictureId + ".jpg"}, null, null);//"//"可以用File.separator代替
        return imgPath;
    }

同样,我们接下来分开讲解:

public String savePicture(byte[] imgBytes)

参数部分我传入了图片的byte数据格式,那么它是如何获得的呢——

 private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            byte[] imgBytes;
            imgBytes = getImagBytes(reader);//获取img的bytes数据格式
            filePath = savePicture(imgBytes);
            }
    };

这个就是从之前代码改过来的,一般都在这个回调方法中保存照片

 pictureId++;
        String imgPath = Environment.getExternalStorageDirectory() + "/DCIM/Camera/WordRecognition_picture" + pictureId + ".jpg";

pictureId就是一个为命名专门而设的一个int变量,初衷是为了不使图片命名重复,但现在看来其实使用系统时间应该更明智。
imgPath是你的图片要存放的目标文件地址,其中
Environment.getExternalStorageDirectory() :内部存储文件夹,其下是所有内部存储文件,你打开文件管理,应该可以看到这个内部存储文件夹,点进去是手机里各种文件夹。
DCIM/Camera:所有的安卓手机拍摄的照片都保存在这个文件夹中,大家可以去看看。

 File outputImage = new File(imgPath);
        FileOutputStream outputStream = null;
        try {
            if (outputImage.exists()) {
                outputImage.delete();//存在就删除
            }
            outputImage.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }

创造对应文件及输出流,对应文件不存在就创造,存在就删除。

 bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);//第二个参数为压缩质量

Bitmap自带的一个图像压缩方法,第一个参数为压缩的格式,第二个参数为压缩质量(100表示无损),第三个参数表明压缩数据的输出对象。

//及时更新到系统相册
        MediaScannerConnection.scanFile(this, new String[]{Environment.getExternalStorageDirectory() + "//DCIM//Camera//WordRecogniton_picture" + pictureId + ".jpg"}, null, null);//"//"可以用File.separator代替

更新图片到相册的一个方法。

四,进阶应用之调用系统相册
我将其写成一个方法:

 public void openAlbum() {
        Intent intent = new Intent("android.intent.action.GET_CONTENT");//选择照片后毁掉onActivityResult方法
        intent.setType("image/*");
        startActivityForResult(intent, CHOOSE_PHOTO);
    }

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case CHOOSE_PHOTO:
                String imgPath=null;
                if (resultCode == RESULT_OK) {
                    if (Build.VERSION.SDK_INT > 19) {
                        imgPath = handlerImgOnNewVersion(data);
                    } else {
                        imgPath = handlerImgOnOldVersion(data);
                    }
                    //以上获取了选择的图片的路径,在这里可以应用这个路径,做一些想要做的东西
                }
                break;
            default:
        }
    }
private String handlerImgOnOldVersion(Intent data) {
        Uri uri = data.getData();
        String imgPath = getImagePath(uri, null);
        return imgPath;
    }
    private String handlerImgOnNewVersion(Intent data) {
        String imgPath = null;//选择的图片的路径
        Uri uri = data.getData();//选择图片的结果,即图片地址的封装,接下来对其进行解析
        if (DocumentsContract.isDocumentUri(this, uri)) {//判断是否是document类型
            String docId = DocumentsContract.getDocumentId(uri);
            switch (uri.getAuthority())//就是获取uri的最开头部分
            {
                case "com.android.providers.media.documents":
                    String id = docId.split(":")[1];//解析出数字格式的id
                    String selection = MediaStore.Images.Media._ID + "=" + id;
                    imgPath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
                    break;
                case "com.android.providers.downloads.documents":
                    Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
                    imgPath = getImagePath(contentUri, null);
                    break;
                default:
            }
        } else if ("content".equalsIgnoreCase(uri.getScheme())) {
            imgPath = getImagePath(uri, null);
        } else if ("file".equalsIgnoreCase(uri.getScheme())) {
            imgPath = uri.getPath();
        }
        return imgPath;
    }

private String getImagePath(Uri uri, String selection) {
        String path = null;
        Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
        if (cursor != null) {
            if (cursor.moveToFirst()) {
                path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
            }
            cursor.close();
        }
        return path;
    }

现在分开讲解:

public void openAlbum() {
        Intent intent = new Intent("android.intent.action.GET_CONTENT");//选择照片后毁掉onActivityResult方法
        intent.setType("image/*");
        startActivityForResult(intent, CHOOSE_PHOTO);
    }

此处intent启动显然是隐式启动,传入系统内部定义的action:”android.intent.action.GET_CONTENT”:代表你想要获取特定种类的数据,setType方法确定你要获取的类型。

再看回调:
你选择一张图片时就会回调onActivityResult方法

                if (resultCode == RESULT_OK) {
                    if (Build.VERSION.SDK_INT > 19) {
                        imgPath = handlerImgOnNewVersion(data);
                    } else {
                        imgPath = handlerImgOnOldVersion(data);
                    }

这里做了个系统适配:
4.4系统以上调用handlerImgOnNewVersion方法:

  private String handlerImgOnNewVersion(Intent data) {
        String imgPath = null;//选择的图片的路径
        Uri uri = data.getData();//选择图片的结果,即图片地址的封装,接下来对其进行解析
        if (DocumentsContract.isDocumentUri(this, uri)) {//判断是否是document类型,对应DocumentProvider
            String docId = DocumentsContract.getDocumentId(uri);//代表文件的唯一id
            switch (uri.getAuthority())//就是获取uri的最开头部分
            {
                case "com.android.providers.media.documents"://多媒体文件类型
                    String id = docId.split(":")[1];//解析出数字格式的id
                    String selection = MediaStore.Images.Media._ID + "=" + id;//筛选条件
                    imgPath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
                    break;
                case "com.android.providers.downloads.documents"://下载内容
                    Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));//将给定的id加到路径的末尾,相当于构造了这类文件的唯一id
                    imgPath = getImagePath(contentUri, null);
                    break;
                default:
            }
        } else if ("content".equalsIgnoreCase(uri.getScheme())) {//对应ContentProvider
            imgPath = getImagePath(uri, null);
        } else if ("file".equalsIgnoreCase(uri.getScheme())) {//对应FileProvider
            imgPath = uri.getPath();
        }
        return imgPath;
    }

你所选择的图片的信息封装在data中,通过getData方法你获取到图片对应的Uri对象,4.4系统以后Uri就被封装了,不能直接提取图片地址,必须解析。
一个Uri的数据格式分为下面三种:

[scheme:]scheme-specific-part[#fragment]  
[scheme:][//authority][path][?query][#fragment]  
[scheme:][//host:port][path][?query][#fragment]  

显然这里是第二种。
其实理解各类型uri是怎么划分,哪种文件的uri属于哪种类型是没有什么意义的,我们只需要知道你获取任意形式内容时,Uri有三个大的类型来源——
DocumentProvider,ContentProvider,FileProvider(三种内容提供器),即按Scheme可以分为这三大类。
然后每种可能按authority分小类,处理方法不同。其它在注释中大都都解释了,selection代表筛选的条件,系统通过这个条件和uri识别出对应文件。

private String getImagePath(Uri uri, String selection) {
        String path = null;
        Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
        if (cursor != null) {
            if (cursor.moveToFirst()) {
                path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
            }
            cursor.close();
        }
        return path;
    }

query原型:

 query(Uri,String[]projection,String selection,String[]selectionArgs,String sortOrder)

projection:所要搜寻的列的名字(columns),null表示所有列
selection:筛选行的条件,null表示所有行
selectionArgs:为selection中的占位符“?”提供具体的值
sorOrder:返回结果的排序方式,null表示默认顺序
query返回Cursor用于遍历数据。

cursor.getColumnIndex(MediaStore.Images.Media.DATA)

返回指定列的名称对应的Cursor索引,用于搜寻对应数据。
MediaStore.Images.Media.DATA存储了对应图片的地址信息。
以上种种过程其实是查表的过程,你可以想象系统用了一张很大的表来存储各种信息。
介绍到这,相信大家差不多理解了如何打开相册。
马上我会传一下自己写的一个小的文字识别的app.

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多