安卓相机应用CameraX开发教程(附所有Java代码)

这个教程讲解如何用安卓 CameraX 开发一个相机应用,包括拍照,拍录像,以及保存文件到系统照片目录。我们会用到 CameraX 的新部件 CameraView。所有程序都是兼容安卓 10。

应用运行示意图:

Android CameraX CameraView

本文主要参考了这篇文章:JetPack开发中使用CameraX完成拍照和拍视频功能,先予以感谢。

我们都知道安卓 Camera2 非常繁琐难懂,令人望而生畏。好消息是现在谷歌出了 CameraX,虽然还在 beta 阶段,但足以开发相机应用了。

有了 CameraX 和它的部件 CameraView,相比 Camera2,我们只用写少很多的代码。

1. 添加 Dependencies 和 CompileOptions

先打开 module/app 级的 build.gradle 文件,在里面添加 dependenciescompileOptions 如下:

(build.gradle 文件)

apply plugin: 'com.android.application'

android {
compileSdkVersion 29
buildToolsVersion "29.0.3"

defaultConfig {
applicationId "com.haoc.cameraxfullcodedemo"
minSdkVersion 23
targetSdkVersion 29
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
//TODO: 添加这几行
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
// TODO: 添加这些 dependencies
// CameraX core library using the camera2 implementation
def camerax_version = "1.0.0-beta08"
// The following line is optional, as the core library is included indirectly by camera-camera2
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
// If you want to additionally use the CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
// If you want to additionally use the CameraX View class
implementation "androidx.camera:camera-view:1.0.0-alpha15"
// If you want to additionally use the CameraX Extensions library
implementation "androidx.camera:camera-extensions:1.0.0-alpha15"

}

最好去谷歌官方网页 Android CameraX 查一下最新的版本号。

点右上角 "Sync Now" 获取更新。

2. 添加相机许可 

现在向 AndroidManifest.xml 文件添加相机许可,就在 application 标签的前面:

(AndroidManifest.xml 文件)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.haoc.cameraxfullcodedemo">

<!--TODO: 这里添加3个许可-->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>


3. 布局中添加 CameraView

在布局文件 activity_main.xml 中添加 CameraView。这里 CameraView 就是相机预览。布局中有另外5个按钮:

 

Android Camera App CameraX

activity_main.xml 文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<androidx.camera.view.CameraView
android:id="@+id/view_finder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_above="@id/btnLens"/>

<Button
android:id="@+id/btnClose"
android:layout_height="wrap_content"
android:layout_width="96dp"
android:text="CLOSE"
android:layout_alignParentRight="true"
android:layout_above="@id/btnPhoto"/>

<Button
android:id="@+id/btnLens"
android:layout_height="wrap_content"
android:layout_width="96dp"
android:text="Lens"
android:layout_above="@id/btnVideo"
android:layout_centerHorizontal="true"/>

<Button
android:id="@+id/btnVideo"
android:layout_height="wrap_content"
android:layout_width="96dp"
android:text="VIDEO"
android:layout_toLeftOf="@id/btnStop"
android:layout_alignParentBottom="true"/>

<Button
android:id="@+id/btnStop"
android:layout_height="wrap_content"
android:layout_width="96dp"
android:text="STOP"
android:layout_toLeftOf="@id/btnPhoto"
android:layout_alignParentBottom="true"/>

<Button
android:id="@+id/btnPhoto"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:text="PHOTO"
android:layout_height="wrap_content"
android:layout_width="96dp"/>

</RelativeLayout>

4. 请求用户授权相机许可

本应用需要用户授权以下许可:使用相机,录音和读写文件。

private final String[] REQUIRED_PERMISSIONS = new String[]{"android.permission.CAMERA",
"android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.RECORD_AUDIO"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (allPermissionsGranted()) {
startCamera();
} else {
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS,
REQUEST_CODE_PERMISSIONS);
}
}

public boolean allPermissionsGranted(){
for(String permission : REQUIRED_PERMISSIONS){
if(ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED){
return false;
}
}
return true;
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

if(requestCode == REQUEST_CODE_PERMISSIONS){
if(allPermissionsGranted()){
startCamera();
} else{
Toast.makeText(this, "Permissions not granted by the user.", Toast.LENGTH_SHORT).show();
this.finish();
}
}
}

5. 准备 CameraView

private Executor executor = Executors.newSingleThreadExecutor();
CameraSelector cameraSelector;
CameraView mCameraView;
mCameraView = findViewById(R.id.view_finder);
mCameraView.setFlash(ImageCapture.FLASH_MODE_AUTO);
//设置闪光模式,选项有自动,一直闪,不闪...
//可选额外功能
ImageCapture.Builder builder = new ImageCapture.Builder();
//Vendor-Extensions (The CameraX extensions dependency in build.gradle)
HdrImageCaptureExtender hdrImageCaptureExtender = HdrImageCaptureExtender.create(builder);
// 如果相机有 hdr 功能
if (hdrImageCaptureExtender.isExtensionAvailable(cameraSelector)) {
// 就打开 hdr.
hdrImageCaptureExtender.enableExtension(cameraSelector);
mCameraView.bindToLifecycle((LifecycleOwner) MainActivity.this); 
//就这么简单,相机准备好了
//CameraView 会自动选择前后照相机,并预设为最好的配置
//绑定 MainActivity 后, 就不用担心关闭或释放相机了。


6. 拍照和拍录像

//拍照
mCameraView.setCaptureMode(CameraView.CaptureMode.IMAGE);
ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(file1).build();
mCameraView.takePicture(outputFileOptions, executor, new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) { //这里保存文件
}
}

//录像
mCameraView.setCaptureMode(CameraView.CaptureMode.VIDEO);
mCameraView.startRecording(file, executor, new VideoCapture.OnVideoSavedCallback() {

@Override
public void onVideoSaved(@NonNull OutputFileResults outputFileResults) {
//这里保存文件
} }

7. 保存文件并导入 MediaStore,所有图片应用都能打开它

public String getBatchDirectoryName() {
String app_folder_path;
if (android.os.Build.VERSION.SDK_INT >= 29) {//如果是安卓 10,先存入这个私有目录
app_folder_path = getExternalFilesDir(Environment.DIRECTORY_PICTURES).toString();
} else { //如果低于安卓 10,还是用这个弃用的目录
app_folder_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString() + "/Camera";
}

File dir = new File(app_folder_path);
if (!dir.exists() && !dir.mkdirs()) {
}
return app_folder_path;
}
//Broadcast to MediaStore
...       
if (android.os.Build.VERSION.SDK_INT >= 29) { //如果是安卓 10
values.put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/Camera");
values.put(MediaStore.MediaColumns.DATE_TAKEN, System.currentTimeMillis());
values.put(MediaStore.MediaColumns.IS_PENDING, true);

Uri uri = getContentResolver().insert(externalContentUri, values);
if (uri != null) {
try {
if (WriteFileToStream(originalFile, getContentResolver().openOutputStream(uri))) {
values.put(MediaStore.MediaColumns.IS_PENDING, false);
getContentResolver().update(uri, values, null, null);
}
} catch (Exception e) {
getContentResolver().delete(uri, null, null);
}
}
originalFile.delete();
} else { //如果低于安卓 10, 照用这个弃用的方法
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
mediaScanIntent.setData(Uri.fromFile(originalFile));
sendBroadcast(mediaScanIntent);
}


MainActivity.java 完整代码

package com.haoc.cameraxfullcodedemo;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.VideoCapture;
import androidx.camera.extensions.HdrImageCaptureExtender;
import androidx.camera.view.CameraView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import android.Manifest;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.provider.MediaStore;
import android.view.View;
import android.webkit.MimeTypeMap;
import android.widget.Button;
import android.widget.Toast;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import static androidx.camera.core.VideoCapture.*;


public class MainActivity extends AppCompatActivity {

static Button btnClose, btnLens, btnVideo, btnStop, btnPhoto;

private Executor executor = Executors.newSingleThreadExecutor();
CameraSelector cameraSelector;
CameraView mCameraView;

private int REQUEST_CODE_PERMISSIONS = 1001;
private final String[] REQUIRED_PERMISSIONS = new String[]{"android.permission.CAMERA",
"android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.RECORD_AUDIO"};


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (allPermissionsGranted()) {
startCamera();
} else {
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS,
REQUEST_CODE_PERMISSIONS);
}
}


private void startCamera() {
btnPhoto = findViewById(R.id.btnPhoto);
btnVideo = findViewById(R.id.btnVideo);
btnStop = findViewById(R.id.btnStop);
btnLens = findViewById(R.id.btnLens);
btnClose = findViewById(R.id.btnClose);
mCameraView = findViewById(R.id.view_finder);
mCameraView.setFlash(ImageCapture.FLASH_MODE_AUTO);
//can set flash mode to auto,on,off...
ImageCapture.Builder builder = new ImageCapture.Builder();
//Vendor-Extensions (The CameraX extensions dependency in build.gradle)
HdrImageCaptureExtender hdrImageCaptureExtender = HdrImageCaptureExtender.create(builder);
// if has hdr (optional).
if (hdrImageCaptureExtender.isExtensionAvailable(cameraSelector)) {
// Enable hdr.
hdrImageCaptureExtender.enableExtension(cameraSelector);
}

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
mCameraView.bindToLifecycle((LifecycleOwner) MainActivity.this);

// set click listener to all buttons

btnPhoto.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(mCameraView.isRecording()){return;}

SimpleDateFormat mDateFormat = new SimpleDateFormat("yyyyMMddHHmmss", Locale.US);
final File file1 = new File(getBatchDirectoryName(), mDateFormat.format(new Date()) + ".jpg");

mCameraView.setCaptureMode(CameraView.CaptureMode.IMAGE);
ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(file1).build();
mCameraView.takePicture(outputFileOptions, executor, new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
galleryAddPic(file1, 0);
}
});
}

@Override
public void onError(@NonNull ImageCaptureException error) {
error.printStackTrace();
}
}); //image saved callback end

} //onclick end
}); //btnPhoto click listener end


btnVideo.setOnClickListener(v -> {
if(mCameraView.isRecording()){return;}

SimpleDateFormat mDateFormat = new SimpleDateFormat("yyyyMMddHHmmss", Locale.US);
File file = new File(getBatchDirectoryName(), mDateFormat.format(new Date()) + ".mp4");

mCameraView.setCaptureMode(CameraView.CaptureMode.VIDEO);
mCameraView.startRecording(file, executor, new VideoCapture.OnVideoSavedCallback() {

@Override
public void onVideoSaved(@NonNull OutputFileResults outputFileResults) {
galleryAddPic(file, 1);
}

@Override
public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) {
//Log.i("TAG",message);
mCameraView.stopRecording();
}

}); //image saved callback end
}); //video listener end


btnStop.setOnClickListener(v -> {
if (mCameraView.isRecording()) {
mCameraView.stopRecording();
}
});


//close app
btnClose.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
if (mCameraView.isRecording()) {
mCameraView.stopRecording();
}
finish();
}
});// on click listener end


btnLens.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mCameraView.isRecording()) {
return;
}

if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
if (mCameraView.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT)) {
mCameraView.toggleCamera();
} else {
return;
}
}//onclick end
}); // btnLens listener end


} //start camera end


@Override
public void onDestroy() {
super.onDestroy();
if (mCameraView.isRecording()) {
mCameraView.stopRecording();
}
finish();
}


public boolean allPermissionsGranted(){
for(String permission : REQUIRED_PERMISSIONS){
if(ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED){
return false;
}
}
return true;
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

if(requestCode == REQUEST_CODE_PERMISSIONS){
if(allPermissionsGranted()){
startCamera();
} else{
Toast.makeText(this, "Permissions not granted by the user.", Toast.LENGTH_SHORT).show();
this.finish();
}
}
}


public String getBatchDirectoryName() {
String app_folder_path;
if (android.os.Build.VERSION.SDK_INT >= 29) {
app_folder_path = getExternalFilesDir(Environment.DIRECTORY_PICTURES).toString();
} else {
app_folder_path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString() + "/Camera";
}

File dir = new File(app_folder_path);
if (!dir.exists() && !dir.mkdirs()) {
}
return app_folder_path;
}


private void galleryAddPic(File originalFile, int mediaType) {
if (!originalFile.exists()) {
return;
}

int pathSeparator = String.valueOf(originalFile).lastIndexOf('/');
int extensionSeparator = String.valueOf(originalFile).lastIndexOf('.');
String filename = pathSeparator >= 0 ? String.valueOf(originalFile).substring(pathSeparator + 1) : String.valueOf(originalFile);
String extension = extensionSeparator >= 0 ? String.valueOf(originalFile).substring(extensionSeparator + 1) : "";

// Credit: https://stackoverflow.com/a/31691791/2373034
String mimeType = extension.length() > 0 ? MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase(Locale.ENGLISH)) : null;

ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.TITLE, filename);
values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename);
values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);

if (mimeType != null && mimeType.length() > 0)
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);

Uri externalContentUri;
if (mediaType == 0)
externalContentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
else if (mediaType == 1)
externalContentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
else
externalContentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;

// Android 10 restricts our access to the raw filesystem, use MediaStore to save media in that case
if (android.os.Build.VERSION.SDK_INT >= 29) {
values.put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/Camera");
values.put(MediaStore.MediaColumns.DATE_TAKEN, System.currentTimeMillis());
values.put(MediaStore.MediaColumns.IS_PENDING, true);

Uri uri = getContentResolver().insert(externalContentUri, values);
if (uri != null) {
try {
if (WriteFileToStream(originalFile, getContentResolver().openOutputStream(uri))) {
values.put(MediaStore.MediaColumns.IS_PENDING, false);
getContentResolver().update(uri, values, null, null);
}
} catch (Exception e) {
getContentResolver().delete(uri, null, null);
}
}
originalFile.delete();
} else {
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
mediaScanIntent.setData(Uri.fromFile(originalFile));
sendBroadcast(mediaScanIntent);
}

} //gallery add end


private static boolean WriteFileToStream(File file, OutputStream out){
try
{
InputStream in = new FileInputStream( file );
try
{
byte[] buf = new byte[1024];
int len;
while( ( len = in.read( buf ) ) > 0 )
out.write( buf, 0, len );
}
finally
{
try
{
in.close();
}
catch( Exception e )
{
//Log.e( "Unity", "Exception:", e );
}
}
}
catch( Exception e )
{
//Log.e( "Unity", "Exception:", e );
return false;
}
finally
{
try
{
out.close();
}
catch( Exception e )
{
//Log.e( "Unity", "Exception:", e );
}
}
return true;
} //write end


}//Main activity end


评论

此博客中的热门博文

Flutter 应用添加 AdMob 广告教程 2020

Flutter/Dart Shared Preferences和设置菜单实例