注: 参考文献,本文基于此文章进行了更为详细的说明,同时修复了部分bug。文章分为两个部分:设计思路和代码,需要代码的直接到最后就行
1.使用Android CaremaX实现预览、拍照和录像
1.1功能说明:通过一个按钮同时实现拍照和录像
- 单击为拍照
- 长按为录像
- 长按时底部的按钮会有进度条进行显示
- 完成拍照和录像后会提示保存路径
1.1.1 实现拍照摄影按钮逻辑(LongClickView.java)
1.1.1.1 文件结构:
1.1.1.2 方法说明:
MyClickListener-----内部接口,定义了三个方法用于,拍照、录像开始、录像结束。
MyAsyncTask--------在触摸事件发生的情况下,开启的异步处理,用于判断当前触摸事件为单击还是长按。
LongClickView1-----构造函数
getAttrs、onMeasure、onDraw-----这三个方法是用于录像过程中进度条的变化。
initView----------------用于初始化视图界面,其中包括了一个长按监听
onTouchEvent-------触摸事件监听,设置内部接口的执行逻辑。
1.1.1.3 处理流程图:
1.1.2 实现预览、拍照、录像功能
1.1.2.1 基础步骤
- 编写视图文件(activity_main.xml)
- 首先需要请求相机权限(需要改动AndroidManifext.xml和MainActivity.java)
- 初始化相机:同时需要在此处展示预览界面
- 获取拍照、录像按钮,并实现其内部接口
1.1.2.2 文件结构&方法说明
1.1.2.2.1 文件结构
1.1.2.2.2 方法说明
initCarema-----初始化相机
takephoto------实现拍照功能
startVideo------该方法弃用了
takeVideo------录像开始时的操作
stopVideo------录像结束时的操作
bindPreview-----实现预览
allPermissionGranted------获取权限
1.1.2.3 完整代码
1.1.2.3.1 MainActivity.java
package com.example.camero_demo_zhh_2;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.Preview;
import androidx.camera.core.VideoCapture;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.media.MediaRecorder;
import android.net.Uri;
import android.os.Bundle;
import android.os.Looper;
import android.provider.SyncStateContract;
import android.util.Log;
import android.widget.Toast;
import com.example.camero_demo_zhh_2.views.LongClickView;
import com.example.camero_demo_zhh_2.views.LongClickView1;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
public class MainActivity extends AppCompatActivity {
private PreviewView mPreviewView;
private Preview mPreview;
private Camera mCamera;
private ImageCapture mImageCapture;
private File outputDirectory; //视频文件储存地址
private VideoCapture mVideoCapture;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//请求相机权限
if (allPermissionGranted()){
initCamera();
} else {
ActivityCompat.requestPermissions(this, Configuration.REQUIRED_PERMISSIONS,
Configuration.REQUEST_CODE_PERMISSIONS);
}
LongClickView1 myLonClickView = findViewById(R.id.iv_camera);
myLonClickView.setMyClickListener(new LongClickView1.MyClickListener(){
@Override
public void longClickStart() {
Log.d(Configuration.TAG, " 启动录像");
takeVideo();
}
@Override
public void singleClick() {
Log.d(Configuration.TAG, " 启动拍照");
takePhoto();
}
@Override
public void longClickStop() {
Log.d(Configuration.TAG, " 结束录像");
stopVideo();
}
});
//设置照片等保存的位置
outputDirectory = getOutputDirectory();
}
//获取照片保存位置
private File getOutputDirectory(){
File mediaDir = new File(getExternalMediaDirs()[0], getString(R.string.app_name) );
boolean isExist = mediaDir.exists() || mediaDir.mkdir();
return isExist ? mediaDir : null;
}
// 启动相机
private void initCamera() {
ListenableFuture<ProcessCameraProvider> mProcessCameraProviderListenableFuture = ProcessCameraProvider.getInstance(this);
mProcessCameraProviderListenableFuture.addListener(() -> {
try {
ProcessCameraProvider mProcessCameraProvider = mProcessCameraProviderListenableFuture.get();
bindPreview(mProcessCameraProvider);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, ContextCompat.getMainExecutor(this));
}
// 图片拍摄
private void takePhoto() {
//确保imageCapture已经被实例化,否则程序可能崩溃
if(mImageCapture != null){
//创建带时间戳的输出文件以保存图片,带时间戳为了保证文件名称唯一
File photoFile = new File(outputDirectory,
new SimpleDateFormat(Configuration.FILENAME_FORMAT,
Locale.SIMPLIFIED_CHINESE).format
(System.currentTimeMillis())
+ ".jpg");
//创建output option对象,用以指定照片的输出方式
ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions
.Builder(photoFile).build();
//执行takePicture方法
mImageCapture.takePicture(outputFileOptions,
ContextCompat.getMainExecutor(this),
new ImageCapture.OnImageSavedCallback() {//保存照片时的回调
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
Uri saveUri = Uri.fromFile(photoFile);
String msg = "照片捕获成功" + saveUri;
Toast.makeText(getBaseContext(), msg, Toast.LENGTH_LONG).show();
Log.d(Configuration.TAG, msg);
}
@Override
public void onError(@NonNull ImageCaptureException exception) {
Log.e(Configuration.TAG, "Photo Capture failed: " + exception.getMessage());
}
});
}
}
@SuppressLint("RestrictedApi")
private void startVideo() {
String path = "/sdcard/a.mp4";
@SuppressLint("RestrictedApi") VideoCapture.OutputFileOptions build = new VideoCapture.OutputFileOptions.Builder(new File(path)).build();
mVideoCapture.startRecording(build, CameraXExecutors.mainThreadExecutor(), new VideoCapture.OnVideoSavedCallback() {
@Override
public void onVideoSaved(@NonNull VideoCapture.OutputFileResults outputFileResults) {
if (false) {
new File(path).delete();
} else {
Toast.makeText(MainActivity.this, "视频已保存" + outputFileResults.getSavedUri().getPath(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) {
Log.e("TAG", "onError: " + message);
new File(path).delete();//视频不足一秒会走到这里来,但是视频依然生成了,所以得删掉
}
});
}
// 图片拍摄
@SuppressLint("RestrictedApi")
private void takeVideo() {
//确保imageCapture已经被实例化,否则程序可能崩溃
if(mVideoCapture != null){
//创建带时间戳的输出文件以保存图片,带时间戳为了保证文件名称唯一
File videoFile = new File(outputDirectory,
new SimpleDateFormat(Configuration.FILENAME_FORMAT,
Locale.SIMPLIFIED_CHINESE).format
(System.currentTimeMillis())
+ ".mp4");
//创建output option对象,用以指定照片的输出方式
// ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions
// .Builder(photoFile).build();
@SuppressLint("RestrictedApi")
VideoCapture.OutputFileOptions outputFileOptions = new VideoCapture.OutputFileOptions
.Builder(videoFile).build();
//执行takePicture方法
// mImageCapture.takePicture(outputFileOptions,
// ContextCompat.getMainExecutor(this),
// new ImageCapture.OnImageSavedCallback() {//保存照片时的回调
// @Override
// public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
// Uri saveUri = Uri.fromFile(photoFile);
// String msg = "照片捕获成功" + saveUri;
// Toast.makeText(getBaseContext(), msg, Toast.LENGTH_LONG).show();
// Log.d(Configuration.TAG, msg);
// }
//
// @Override
// public void onError(@NonNull ImageCaptureException exception) {
// Log.e(Configuration.TAG, "Photo Capture failed: " + exception.getMessage());
// }
// });
// mVideoCapture.setupEncoder CameraXExecutors.mainThreadExecutor(),
mVideoCapture.startRecording(outputFileOptions,
ContextCompat.getMainExecutor(this),
new VideoCapture.OnVideoSavedCallback() {
@Override
public void onVideoSaved(@NonNull VideoCapture.OutputFileResults outputFileResults) {
// try {
// Toast.makeText(MainActivity.this, "视频已保存" + outputFileResults.getSavedUri().getPath(), Toast.LENGTH_SHORT).show();
//
// }catch (Exception e){
// System.out.println("弹窗失败" + e);
// }
Uri saveUri = Uri.fromFile(videoFile);
String msg = "视频捕获成功" + saveUri;
Toast.makeText(getBaseContext(), msg, Toast.LENGTH_LONG).show();
Log.d(Configuration.TAG, msg);
}
@Override
public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) {
Log.e(Configuration.TAG, "Video Capture failed: " );
}
}
);
}
}
@SuppressLint("RestrictedApi")
private void stopVideo(){
mVideoCapture.stopRecording();
}
// 绑定页面
@SuppressLint("RestrictedApi")
private void bindPreview(ProcessCameraProvider processCameraProvider) {
mPreviewView = (PreviewView)findViewById(R.id.preview);
//创建preview
mPreview = new Preview.Builder().build();
//指定所需的相机选项,设置摄像头镜头切换
boolean isFront = false;
CameraSelector mCameraSelector = new CameraSelector.Builder().requireLensFacing(isFront ? CameraSelector.LENS_FACING_FRONT :
CameraSelector.LENS_FACING_BACK).build();
//将 Preview 连接到 PreviewView。
mPreview.setSurfaceProvider(mPreviewView.getSurfaceProvider());
//将所选相机和任意用例绑定到生命周期。
mImageCapture = new ImageCapture.Builder()
.setTargetRotation(mPreviewView.getDisplay().getRotation())
.build();
mVideoCapture = new VideoCapture.Builder()
.setTargetRotation(mPreviewView.getDisplay().getRotation())//设置旋转角度
.setVideoFrameRate(60)//每秒的帧数
.setBitRate(3 * 1024 * 1024)//设置每秒的比特率
.setAudioRecordSource(MediaRecorder.AudioSource.MIC)
.build();
processCameraProvider.unbindAll();
mCamera = processCameraProvider.bindToLifecycle(this, mCameraSelector,
mImageCapture, mVideoCapture, mPreview);
}
//判断相机权限是否申请成功
private boolean allPermissionGranted() {
for (String permission : Configuration.REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
static class Configuration{
public static final String TAG = "CameraxDemo";
public static final String FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS";
public static final int REQUEST_CODE_PERMISSIONS = 10;
public static final String[] REQUIRED_PERMISSIONS = new String[]{
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
};
}
}
1.1.2.3.2 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/preview"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.camera.view.PreviewView>
<ImageView
android:id="@+id/iv_reverse"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="20dp"
android:src="@mipmap/icon_reverse"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.example.camero_demo_zhh_2.views.LongClickView1
android:id="@+id/iv_camera"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginBottom="20dp"
app:annulusColor="@color/color_2196F3"
app:annulusWidth="20"
app:delayMilliseconds="40"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:maxSeconds="10" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
1.1.2.3.3 AndroidManifext.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.camero_demo_zhh_2">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Camero_demo_zhh_2"
android:requestLegacyExternalStorage="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
1.1.2.3.4 LongClickView.java
package com.example.camero_demo_zhh_2.views;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
import com.example.camero_demo_zhh_2.R;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;
public class LongClickView1 extends View {
public int DEFAULT_MAX_SECONDS = 15;
public int DEFAULT_ANNULUS_WIDTH = 5;
public int DEFAULT_ANNULUS_COLOR;
public int DEFAULT_RATE = 50;
private Paint mSmallCirclePaint;
private Paint mMiddenCirclePaint;
private Paint mBigCirclePaint;
private Paint mAngleCirclePaint;
private int mWidthSize;
private Timer mTimer;//计时器
private AtomicInteger mCount = new AtomicInteger(0);
private LongClickView1.MyClickListener mMyClickListener;
private boolean mIsFinish = true;
private int mMaxSeconds;
private int mDelayMilliseconds;
private int mAnnulusColor;
private float mAnnulusWidth;
private boolean isUp = false;//touchEvent
private boolean isLongTouch = false;
public LongClickView1(Context context) {
super(context);
}
public LongClickView1(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public LongClickView1(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
getAttrs(context, attrs);
initView();
}
public interface MyClickListener {
void longClickStart();//长按开始
void longClickStop();//长按结束
void singleClick();//单击结束
}
public void setMyClickListener(LongClickView1.MyClickListener myClickListener) {
mMyClickListener = myClickListener;
}
private void getAttrs(Context context, @Nullable AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LongClickView);
//maxSeconds 最大的秒数
mMaxSeconds = typedArray.getInt(R.styleable.LongClickView_maxSeconds, DEFAULT_MAX_SECONDS);
//annulusWidth 圆环的宽度
mAnnulusWidth = typedArray.getInt(R.styleable.LongClickView_annulusWidth, DEFAULT_ANNULUS_WIDTH);
//annulusColor 圆环的颜色
DEFAULT_ANNULUS_COLOR = context.getResources().getColor(R.color.color_grey);
mAnnulusColor = typedArray.getColor(R.styleable.LongClickView_annulusColor, DEFAULT_ANNULUS_COLOR);
//delayMilliseconds 进度条隔多少时间走一次,值越小走的越快,显得更流畅
mDelayMilliseconds = typedArray.getInt(R.styleable.LongClickView_delayMilliseconds, DEFAULT_RATE);
}
private static final String TAG = "CameraxDemo";
//初始化视图
private void initView() {
mBigCirclePaint = new Paint();
mSmallCirclePaint = new Paint();
mMiddenCirclePaint = new Paint();
mAngleCirclePaint = new Paint();
mBigCirclePaint.setStyle(Paint.Style.FILL);
mBigCirclePaint.setColor(Color.LTGRAY);
mBigCirclePaint.setAntiAlias(true);
mBigCirclePaint.setStrokeWidth(5);
mSmallCirclePaint.setStrokeWidth(5);
mSmallCirclePaint.setAntiAlias(true);
mSmallCirclePaint.setColor(Color.WHITE);
mSmallCirclePaint.setStyle(Paint.Style.FILL);
mMiddenCirclePaint.setStrokeWidth(5);
mMiddenCirclePaint.setAntiAlias(true);
mMiddenCirclePaint.setColor(Color.LTGRAY);
mMiddenCirclePaint.setStyle(Paint.Style.FILL);
mAngleCirclePaint.setStrokeWidth(5);
mAngleCirclePaint.setAntiAlias(true);
mAngleCirclePaint.setColor(mAnnulusColor);
mAngleCirclePaint.setStyle(Paint.Style.FILL);
//监听长按监听
this.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Log.d(TAG,"长按监听:启动录像");
mMyClickListener.longClickStart();
mIsFinish = false;
mCount.set(0);
mTimer = new Timer();
mTimer.schedule(new TimerTask() {
@Override
public void run() {
mCount.addAndGet(1);
invalidate();//刷新页面
//若长按时间大于最长时间,结束监听
if (mCount.get() * mDelayMilliseconds >= mMaxSeconds * 1000) {
mCount.set(0);
this.cancel();
Log.d(TAG, "时间超时");
mMyClickListener.longClickStop();
//
invalidate();
}
}
}, 0, mDelayMilliseconds);
return true;
}
});
}
//当出现触摸时间时,进行的处理
@Override
public boolean onTouchEvent(MotionEvent event) {
//当该触摸的状态是拿起时,设置结束时间
if (event.getAction() == MotionEvent.ACTION_UP) {
isUp = true;
//新建异步处理
if (isLongTouch) {
mMyClickListener.longClickStop();
mTimer.cancel();
mCount.set(0);
invalidate();//刷新页面
} else {
mMyClickListener.singleClick();
}
}//当为按下时设置开始时间
else if (event.getAction() == MotionEvent.ACTION_DOWN) {
isUp = false;
new MyAsyncTask().execute();
}
return super.onTouchEvent(event);
}
//主要作用:判断此次单击是否为长按
public class MyAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... voids) {
if (mTimer != null) {
mTimer.cancel();
}
return null;
}
@Override
protected void onPreExecute() {
Log.d(TAG, "异步处理中:isUp_:" + isUp + " isLongTouch_:" + isLongTouch);
Timer myTimer = new Timer();
myTimer.schedule(new TimerTask() {
@Override
public void run() {
Log.d(TAG, "__run_isUp_:" + isUp);
//判断在延迟内是否UP,若up则为单击,否则为长按
if (isUp) {
isLongTouch = false;
} else {
isLongTouch = true;
myTimer.cancel();
}
}
}, 400);
//将状态恢复,否则每次录像过后,再次进行单击也会被认为是长按
isLongTouch = false;
super.onPreExecute();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidthSize = MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(mWidthSize, mWidthSize);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(mWidthSize / 2, mWidthSize / 2, mWidthSize / 2, mBigCirclePaint);//最外层的填充圆
RectF rectF = new RectF(0, 0, mWidthSize, mWidthSize);//进度扇形
if (mCount.get() > 0) {
//求出每一次定时器执行所绘制的扇形度数
float perAngle = 360f / mMaxSeconds / (1000f / mDelayMilliseconds);
canvas.drawArc(rectF, 0, perAngle * mCount.get(), true, mAngleCirclePaint);
}
canvas.drawCircle(mWidthSize / 2, mWidthSize / 2, mWidthSize / 2 - mAnnulusWidth, mMiddenCirclePaint);//中间一层灰色的圆
//最后绘制中心圆
if (mIsFinish) {
canvas.drawCircle(mWidthSize / 2, mWidthSize / 2, mWidthSize / 2 - mAnnulusWidth, mSmallCirclePaint);
} else {
canvas.drawCircle(mWidthSize / 2, mWidthSize / 2, mWidthSize / 8, mSmallCirclePaint);
}
super.onDraw(canvas);
}
}