Android Camera了解一下

作者: juexingzhe | 来源:发表于2018-11-23 11:39 被阅读18次

    今天我们来了解下Android Camera的一些基本知识,包括下面一些内容

    1. 调用设备的相机app拍摄照片

    2. 调用设备的相机app拍摄视频

    3. 通过相机api拍摄照片和视频

    1.调用设备的相机app拍摄照片

    先看下效果图,拍摄可以返回缩略图和原图,这里看下返回原图的效果,点击CAPTURE按钮会调用设备的camera app,拍摄后会返回Bitmap:

    take pic.jpg
    1.1 获取相机特征权限

    这个和平常的相机权限不一样,声明改特征是如果应用的主要特征是跟相机摄像头有关,那么应用商店Google Play会根据设备是否有相机来决定是否下载该应用:

    <manifest ... >
        <uses-feature android:name="android.hardware.camera"
                      android:required="true" />
        ...
    </manifest>
    

    如果android:required设置为true,那么如果设备没有相机就不会下载该应用;设置为false,会允许下载,这个时候程序员就需要在代码中增加是否有相机的特征判断:

    hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
    
    1.2 通过设备camera拍摄照片

    做Android的小伙伴们都知道要委托其他app完成某些工作需要通过系统的Intent来做。所以,我们通过设备camera app也是需要Intent,包含三个步骤:

    1. 构造Intent
    2. 启动相机app的activity
    3. 处理返回的数据

    看下前面两个步骤通过startActivityForResult的实现:

    static final int REQUEST_IMAGE_CAPTURE = 1;
    
    private void dispatchTakePictureIntent() {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
        }
    }
    

    这里有一点需要注意的,startActivityForResult之前需要增加一个判断resolveActivity,它会返回第一个可以处理这个intent的activity,如果找不到可以处理这个intent的app,那么我们的app就会crash,所以注意增加这个判断。

    第三点,处理返回的数据分成两部分,一个是返回缩略图,像素值小,另外一个就是返回原图片,分别来看下这两种情形。

    1.3 返回缩略图

    Camera app会在返回intent的extras中的“data”这个可以下带回缩小版的Bitmap,看下代码:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
            Bundle extras = data.getExtras();
            Bitmap imageBitmap = (Bitmap) extras.get("data");
            mImageView.setImageBitmap(imageBitmap);
        }
    }
    
    1.4 保存原图片

    可以给Camera app一个路径用于保存原图。如果不是敏感的图片,Android系统推荐通过

    getExternalStoragePublicDirectory(DIRECTORY_PICTURES)
    

    放在这个路径下的图片可以被所有的app访问到。当前前提是要声明写权限:

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

    如果希望只有本app可以访问到,可以通过下面路径访问到:

    getExternalFilesDir(Environment.DIRECTORY_PICTURES)
    

    所以可以抽离出一个函数返回保存图片的路径:

    String mCurrentPhotoPath;
    
    private File createImageFile() throws IOException {
        // Create an image file name
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        String imageFileName = "JPEG_" + timeStamp + "_";
        File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        File image = File.createTempFile(
            imageFileName,  /* prefix */
            ".jpg",         /* suffix */
            storageDir      /* directory */
        );
    
        // Save a file: path for use with ACTION_VIEW intents
        mCurrentPhotoPath = image.getAbsolutePath();
        return image;
    }
    

    接着返回构造一个FileProvider,这样Camera app才能访问到:

        private void dispatchTakeFullSizePicIntent() {
            Intent takePicIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            if (takePicIntent.resolveActivity(getPackageManager()) != null) {
                // Create the File where the photo should go
                File photoFile = null;
                try {
                    photoFile = FileUtils.createImageFile(this);
                } catch (IOException e) {
                    // Error occurred while creating the File
                    e.printStackTrace();
                }
    
                if (photoFile != null) {
                    currentPhotoPath = photoFile.getAbsolutePath();
                    Uri photoURI = FileProvider.getUriForFile(
                            this,
                            getResources().getString(R.string.fileprovider_authority),
                            photoFile
                    );
                    takePicIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
                    startActivityForResult(takePicIntent, REQUEST_TAKE_PHOTO);
                }
            }
        }
    

    FileProvider需要在清单文件中注册,

    <provider
                android:authorities="@string/fileprovider_authority"
                android:name="android.support.v4.content.FileProvider"
                android:exported="false"
                android:grantUriPermissions="true">
                <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/file_paths"/>
            </provider>
    

    需要注意的是authorities需要和getUriForFile的第二个参数一样,在meta-data标签中通过在xml目录下的file_paths路径的xml文件指定图片的存储路径:

    <?xml version="1.0" encoding="utf-8"?>
    <paths xmlns:android="http://schemas.android.com/apk/res/android">
        <external-path name="my_images" path="Android/data/com.example.package.name/files/Pictures" />
    </paths>
    

    这个路径其实就是上面的photoFile,也就是通过getExternalFilesDir(Environment.DIRECTORY_PICTURES)得到的。

    如果要把上面保存的图片放到相册可以通过下面的方式:

    private void galleryAddPic() {
        Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        File f = new File(mCurrentPhotoPath);
        Uri contentUri = Uri.fromFile(f);
        mediaScanIntent.setData(contentUri);
        this.sendBroadcast(mediaScanIntent);
    }
    

    2. 调用设备的相机app拍摄视频

    跟上面调用设备的相机app拍摄照片其实差不多,首先也是获取相机特征权限,这个和上面是完全一样的,不再重复说了。调用设备的相机app拍摄视频也是需要三个步骤,

    1. 构造Intent
    2. 启动相机app的activity
    3. 处理返回的数据

    首先看下构造intent和启动:

    static final int REQUEST_VIDEO_CAPTURE = 1;
    
    private void dispatchTakeVideoIntent() {
        Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
        if (takeVideoIntent.resolveActivity(getPackageManager()) != null) {
            startActivityForResult(takeVideoIntent, REQUEST_VIDEO_CAPTURE);
        }
    }
    

    和拍摄照片唯一不一样的就是action,视频的是MediaStore.ACTION_VIDEO_CAPTURE.

    第三步就是处理返回的数据了:

        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            if (requestCode == REQUEST_VIDEO_CAPTURE && resultCode == RESULT_OK) {
                Uri videoUri = data.getData();
                videoView.setVideoURI(videoUri);
                if (videoView.getVisibility() == View.GONE) {
                    videoView.setVisibility(View.VISIBLE);
                }
    
                videoView.start();
            }
        }
    

    通过intent的getData可以拿到视频的uri,通过VideoView进行播放。

    3.控制相机

    一般创建自定义相机界面有下面几个步骤:

    1. 确认设备有相机和申请权限
    2. 创建一个预览界面,可以用TextureView或者SurfaceView
    3. 创建一个布局,包含上面的预览界面和控制接口UI,比如按钮
    4. 建立UI和预览界面之间的联系,比如点击拍摄,控制相机拍摄然后预览界面显示
    5. 保存拍摄的文件,照片或者视频
    6. 释放相机
    3.1 确认设备是否有相机

    如前面第一部分说的,如果没有在清单文件中申明app需要相机,那么就需要在代码中增加判断设备是否有相机,可以通过PackageManager.hasSystemFeature()方法判断:

    /** Check if this device has a camera */
    private boolean checkCameraHardware(Context context) {
        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){
            // this device has a camera
            return true;
        } else {
            // no camera on this device
            return false;
        }
    }
    

    在Android 2.3版本及以上,如果设备有多个相机,可以通过Camera.getNumberOfCameras()确认

    3.2 申请权限

    在Android 6.0以上需要运行时申请权限,首先需要确认是否已授权:

    ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED)
    

    如果未授权,需要申请:

    //ask for authorisation
    ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_PERMISSION_CODE);
    

    最后一个参数是常量请求码,在权限反馈结果返回中需要用到,在onRequestPermissionsResult返回结果:

        @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
            switch (requestCode) {
                case REQUEST_PERMISSION_CODE:
                    // If request is cancelled, the result arrays are empty.
                    if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                        // permission was granted
                        mPreview.startPreview();
                    } else {
                        // permission denied, boo! Disable the
                        // functionality that depends on this permission.
                        finish();
                    }
                    break;
                    default:
                        break;
            }
        }
    
    3.3 获取相机实例

    确认过设备有相机和app有相机权限后需要获取相机实例,其中id可以是前置相机(Camera.CameraInfo.CAMERA_FACING_FRONT)或者后置相机(Camera.CameraInfo.CAMERA_FACING_BACK):

        private Camera safeCameraOpen(final int id) {
            Camera camera;
    
            try {
                releaseCameraAndPreview();
                camera = Camera.open(id);
            } catch (Exception e) {
                e.printStackTrace();
                camera = null;
            }
    
            latch.countDown();
    
            return camera;
        }
    
        private void releaseCameraAndPreview() {
            if (mCamera != null) {
                mCamera.release();
                mCamera = null;
            }
        }
    

    open操作需要catch 异常,有可能其他app在使用相机或者相机设备不存在,另外open操作是耗时操作,建议放在线程中。

    3.4 获取更多相机特征

    成功获取相机实例后,可以通过Camera.getParameters()获取更多相机的参数信息,通过Camera.getCameraInfo()获取相机是前置还是后置,和图片的旋转方向。

    3.5 创建预览界面

    这里通过TextureView创建预览界面,TextureView需要实现接口TextureView.SurfaceTextureListener:

        private void initTextureView() {
            if (null == textureView) {
                textureView = new TextureView(context);
                textureView.setKeepScreenOn(true);
                textureView.setSurfaceTextureListener(this);
            }
            LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER);
            addView(textureView, layoutParams);
        }
    

    在系统创建好TextureView后会回调onSurfaceTextureAvailable,在这里就可以创建相机实例了:

        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
            initImg();
    
            coverImg.setImageResource(R.drawable.second);
            final Camera[] camera = new Camera[1];
    
            WorkerManager.getInstance().postTask(new Runnable() {
                @Override
                public void run() {
                    camera[0] = safeCameraOpen(Camera.CameraInfo.CAMERA_FACING_BACK);
                }
            });
            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            setCamera(surface, camera[0]);
        }
    

    其中coverImg是ImageView,在相机创建之前显示一张封面图用的。在异步线程中打开相机,然后把SurfaceTexture传递给相机就可以预览到界面了。

    在接口onSurfaceTextureDestroyed记得释放相机:

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
            stopPreviewAndFreeCamera();
            return true;
    }
    
        /**
         * When this function returns, mCamera will be null.
         */
    private void stopPreviewAndFreeCamera() {
        if (mCamera != null) {
            mCamera.stopPreview();
    
            mCamera.release();
    
            mCamera = null;
    }
    
    3.6 创建一个布局

    布局很简单,添加一个容器用来放置上面的预览界面CameraPreview:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical">
    
        <Button
            android:id="@+id/control"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/btn_bg"
            android:text="@string/capture_image" />
    
        <com.example.juexingzhe.jueapp.view.CameraPreview
            android:id="@+id/cameraShower"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center_horizontal"
            android:scaleType="fitXY" />
    
    </LinearLayout>
    
    3.7 建立UI和预览界面的关系

    接下来就是建立UI,就是上面的按钮,和预览界面CameraPreview之间的关系,这里就是点击按钮拍摄一张照片:

        private void initView() {
            mPreview = findViewById(R.id.cameraShower);
            controlBtn = findViewById(R.id.control);
            controlBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mPreview != null) {
                        mPreview.takePicture();
                    }
                }
            });
        }
    
    3.8 拍摄照片

    上面点击按钮调用CameraPreview的takePicture拍摄照片,这里需要实现Camera.PictureCallback接口,在接口会回调回来二进制数据:

        /**
         * 拍照片
         */
        public void takePicture() {
            if (mCamera != null) {
                mCamera.takePicture(null, null, new Camera.PictureCallback() {
                    @Override
                    public void onPictureTaken(byte[] data, Camera camera) {
    
                        if (coverImg == null) {
                            return;
                        }
    
                        if (data == null || data.length == 0) {
                            return;
                        }
    
                        Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
                        if (bitmap != null) {
                            coverImg.setVisibility(VISIBLE);
                            coverImg.setImageBitmap(BitmapUtils.rotateBitmap(bitmap, (180 - rotationDigree)));
                            stopPreviewAndFreeCamera();
                        }
                    }
                });
            }
        }
    

    这里直接decode二进制数据生成Bitmap,然后给ImageView显示。

    4.总结

    上面总结了Camera的一些用法,包括通过调用系统的相机app拍摄照片和视频,自定义控制相机拍摄照片,其实还有一个自定义控制相机拍摄视频,官方文档推荐结合用MediaRecorder录制,但是现在比较常用的方式就是通过MediaCodec进行编码,然后通过MediaMuxer混合成视频格式,这个后面专门写篇博客分享。

    相关文章

      网友评论

        本文标题:Android Camera了解一下

        本文链接:https://www.haomeiwen.com/subject/ypoqqqtx.html