在今年的Google I/O大会上,Google新推出了CameraX支持包,按照官方的说法, 这个包的作用是:
help you make camera app development easier
安卓中使用相机从来就不是一件容易的事。
Camera1要自己管理Camera相机实例,要处理SufraceView相关的一堆东西,还有预览尺寸跟画面尺寸的选择,页面生命周期切换等等问题,后来推出了Camera2,从官方demo 就上千行代码来看,Camera2并不解决用起来复杂的问题,它提供了更多的调用接口,可定制性更好,结果就是对普通开发者来说更难用了。。。
终于Google也意识到这个问题,推出了最终版CameraX. CameraX实际上还是用的Camera2,但它对调用API进行了很好的封装,使用起来非常方便。官方教程也很详细,如下:
https://codelabs.developers.google.com/codelabs/camerax-getting-started/#0
注意:CameraX跟Camera2一样最低支持API21,也就是5.0及以上。
开发环境用Android Studio3.3及以上,依赖库都用androidx的
创建app
创建app,声明相机权限
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
添加相关依赖
// CameraX core library def camerax_version = "1.0.0-alpha05" // CameraX view library def camerax_view_version = "1.0.0-alpha02" // CameraX extensions library def camerax_ext_version = "1.0.0-alpha02" implementation "androidx.camera:camera-core:$camerax_version" // If you want to use Camera2 extensions implementation "androidx.camera:camera-camera2:$camerax_version" // If you to use the Camera View class implementation "androidx.camera:camera-view:$camerax_view_version" // If you to use Camera Extensions implementation "androidx.camera:camera-extensions:$camerax_ext_version"
布局文件:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.camera.view.CameraView android:id="@+id/view_camera" android:layout_width="match_parent" android:layout_height="match_parent" /> <Button android:id="@+id/btn_take_pic" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_marginLeft="16dp" android:layout_marginBottom="16dp" android:text="拍照" /> <Button android:id="@+id/btn_start_recording" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_marginLeft="16dp" android:layout_marginBottom="16dp" android:layout_toRightOf="@+id/btn_take_pic" android:text="@string/start_recording" /> <Button android:id="@+id/btn_switch_camera" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_marginLeft="16dp" android:layout_marginBottom="16dp" android:layout_toRightOf="@+id/btn_start_recording" android:text="切换摄像头" /> <ImageView android:id="@+id/iv_show_pic" android:layout_width="100dp" android:layout_height="150dp" android:layout_alignParentRight="true" android:layout_marginTop="16dp" android:layout_marginRight="16dp" /> <VideoView android:id="@+id/vv_show_recording" android:layout_width="100dp" android:layout_height="150dp" android:visibility="gone" /> </RelativeLayout>
切记在启动该Activity的时候要动态获取到相机权限 读写权限,麦克风权限
我demo 中引入的是:
implementation 'com.blankj:utilcodex:1.26.0' 动态获取取权限:
public void getPermission() { PermissionUtils.permission(PermissionConstants.STORAGE, PermissionConstants.CAMERA, PermissionConstants.MICROPHONE) .rationale(new PermissionUtils.OnRationaleListener() { @Override public void rationale(final ShouldRequest shouldRequest) { shouldRequest.again(true); } }) .callback(new PermissionUtils.FullCallback() { @Override public void onGranted(List<String> permissionsGranted) { startActivity(new Intent(WelcomeActivity.this, MainActivity.class)); } @Override public void onDenied(List<String> permissionsDeniedForever, List<String> permissionsDenied) { if (!permissionsDeniedForever.isEmpty()) { PermissionUtils.launchAppDetailsSettings(); } } }).request(); }
onGranted回调是获取到了所有权限,获取到所有权限后就可以进入到相机界面,这时候进入相机界面相机后置摄像头就默认打开了,也就是进入了Preview状态
相机界面首先: CameraView 的 bindToLifeCycle 方法将这个 View 与当前组件的生命周期绑定:
mCameraView.bindToLifecycle(this);
拍照:
if (mCameraView == null) { return; } mCameraView.setCaptureMode(CameraView.CaptureMode.IMAGE); mCameraView.takePicture(initTakePicPath(), new ImageCapture.OnImageSavedListener() { @Override public void onImageSaved(@NonNull File file) { LogUtils.d("MainActivity takePicture onImageSaved file : " + file); if (mShowPicView == null) { return; } try { Glide.with(MainActivity.this) .load(file) .into(mShowPicView); } catch (Exception e) { } } @Override public void onError(@NonNull ImageCapture.ImageCaptureError imageCaptureError, @NonNull String message, @Nullable Throwable cause) { LogUtils.d("MainActivity takePicture onError imageCaptureError : " + imageCaptureError + " message : " + message + " Throwable : " + cause); } });
录像:
if (mCameraView == null) { return; } if (mCameraView.isRecording()) { mCameraView.stopRecording(); mStartRecordingView.setText(ResourceUtil.getString(R.string.start_recording)); } else { mStartRecordingView.setText(ResourceUtil.getString(R.string.stop_recording)); mCameraView.setCaptureMode(CameraView.CaptureMode.VIDEO); mCameraView.startRecording(initStartRecordingPath(), new VideoCapture.OnVideoSavedListener() { @Override public void onVideoSaved(@NonNull File file) { LogUtils.d("MainActivity startRecording onVideoSaved file : " + file); if (mShowVideoView == null) { return; } mShowVideoView.post(new Runnable() { @Override public void run() { mShowVideoView.setVisibility(View.VISIBLE); mShowVideoView.setVideoPath(file.getAbsolutePath()); mShowVideoView.start(); mShowVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mediaPlayer) { mShowVideoView.start(); } }); } }); } @Override public void onError(@NonNull VideoCapture.VideoCaptureError videoCaptureError, @NonNull String message, @Nullable Throwable cause) { LogUtils.d("MainActivity startRecording onError imageCaptureError : " + videoCaptureError + " message : " + message + " Throwable : " + cause); } }); }
注意:1:录像回来是子线程 如果想使用VideoView播放该视频 需要切换到UI线程 2.录像钱要切换:CaptureMode 否则会报错:
throw new IllegalStateException("Can not record video under IMAGE capture mode.");
看下源码就知道:
public void startRecording(File file, final OnVideoSavedListener listener) { if (mVideoCapture == null) { return; } if (getCaptureMode() == CaptureMode.IMAGE) { throw new IllegalStateException("Can not record video under IMAGE capture mode."); } if (listener == null) { throw new IllegalArgumentException("OnVideoSavedListener should not be empty"); } mVideoIsRecording.set(true); mVideoCapture.startRecording( file, new VideoCapture.OnVideoSavedListener() { @Override public void onVideoSaved(@NonNull File savedFile) { mVideoIsRecording.set(false); listener.onVideoSaved(savedFile); } @Override public void onError( @NonNull VideoCapture.VideoCaptureError videoCaptureError, @NonNull String message, @Nullable Throwable cause) { mVideoIsRecording.set(false); Log.e(TAG, message, cause); listener.onError(videoCaptureError, message, cause); } }); }
切换摄像头:
if (mCameraView == null) { return; } CameraX.LensFacing lensFacing = mCameraView.getCameraLensFacing(); if (lensFacing == null) { return; } if (lensFacing == CameraX.LensFacing.FRONT) { mCameraView.setCameraLensFacing(CameraX.LensFacing.BACK); } else { mCameraView.setCameraLensFacing(CameraX.LensFacing.FRONT); }
demo已经上传到Github 上了:https://github.com/HuaDanJson/CameraX