Android 记录CameraX拍照录屏以及翻转相机黑屏处理

1634899868549.gif

先说明一个问题,本记录没有解决CameraX前置拍摄视频,视频画面镜像问题,如果和我一样被这个困惑了的,就不要看了,我没解决,谁要是知道怎么解决麻烦评论去告诉我,谢了。(猜想:是不是要设置一个ImageAnalysis解析数据流,录屏时动态镜像啊。)

把想要的东西都封装在一个自定义view里面,使用时只要添加在activity的布局里面,外部无需关心调用。
值得一提的是,他切换相机会黑屏,原因是CameraX的切换摄像头逻辑是先解绑在绑定。我猜在解绑的时候,PreviewView的数据断流了,自己也没有保持最后一帧画面,就是黑屏的。那么他没保持最后一帧,那么我们自己保持就行了,关键PreviewView是提供了获取画面bitmap方法的。具体逻辑看下面的mPreviewView.getBitmap()相关逻辑代码。
还有就是他前置拍摄照片也是镜像的,需要我们设置一下

ImageCapture.Metadata metadata = new ImageCapture.Metadata();
            metadata.setReversedHorizontal(lensFacing == CameraSelector.LENS_FACING_FRONT);

我就想不通了,你拍照有设置,为啥录视频没有,la ji。

最新的库

//相机录制
    implementation "androidx.camera:camera-core:1.0.2"
    implementation "androidx.camera:camera-camera2:1.0.2"
    // If you want to additionally use the CameraX Lifecycle library
    implementation "androidx.camera:camera-lifecycle:1.0.2"
    // If you want to additionally use the CameraX View class
    implementation "androidx.camera:camera-view:1.0.0-alpha29"
    // If you want to additionally use the CameraX Extensions library
    implementation "androidx.camera:camera-extensions:1.0.0-alpha29"
    //promission
    implementation 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.5@aar'

权限

<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"/>

RecodingView

public class RecodingView extends RelativeLayout implements View.OnClickListener {

    private static final double RATIO_4_3_VALUE = 4.0 / 3.0;
    private static final double RATIO_16_9_VALUE = 16.0 / 9.0;
    private boolean mIsBack = true;

    private int lensFacing = CameraSelector.LENS_FACING_BACK;

    private Context mContext;
    private PreviewView mPreviewView;
    private ProcessCameraProvider mCameraProvider;
    private ImageView mBtnFlip;
    private Camera camera;
    private Preview mPreview;
    private boolean first = true;
    private ImageView mFlipIv;
    private Bitmap mBitmapFlip;
    private Handler mMainHandle = new Handler(Looper.getMainLooper());
    private PreviewView.StreamState lastStreamState = PreviewView.StreamState.IDLE;
    private RecordButton mRecordButton;
    private ImageCapture mImageCapture;
    private ExecutorService mCameraExecutor;
    private VideoCapture mVideoCapture;
    private int mCurrentState = RecordButton.NORMAL;
    private ImageView mIvDelete;
    private ImageView mIvOk;
    private String mCurrentPicturePath;
    private String mCurrentVideoPath;
    private RelativeLayout mRlVideo;
    private VideoView mVideoView;

    public RecodingView(Context context) {
        this(context, null);
    }

    public RecodingView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RecodingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setVisibility(GONE);
        mContext = context;
        mCameraExecutor = Executors.newSingleThreadExecutor();
        inflate(context, R.layout.recoding_layout, this);
        mBtnFlip = findViewById(R.id.mBtnFlip);
        mBtnFlip.setOnClickListener(this);
        mPreviewView = findViewById(R.id.previewView);
        mFlipIv = findViewById(R.id.mFlipIv);
        mIvDelete = findViewById(R.id.mIvDelete);
        mIvDelete.setOnClickListener(this);
        mIvOk = findViewById(R.id.mIvOk);
        mIvOk.setOnClickListener(this);
        mRlVideo = findViewById(R.id.mRlVideo);
        findViewById(R.id.mIvBack).setOnClickListener(this);
        mPreviewView.post(new Runnable() {
            @Override
            public void run() {
                RxPermissions rxPermissions = new RxPermissions((Activity) mContext);
                rxPermissions.request(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE,
                        Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO).subscribe(new Consumer<Boolean>() {
                    @SuppressLint("CheckResult")
                    @Override
                    public void accept(Boolean aBoolean) throws Exception {
                        if (aBoolean == true) {
                            setVisibility(VISIBLE);
                            startCameraPreview();
                        }else{
                            rxPermissions.shouldShowRequestPermissionRationale((Activity) mContext,Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE,
                                    Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO).subscribe(new Consumer<Boolean>() {
                                @Override
                                public void accept(Boolean aBoolean) throws Exception {
                                    if(aBoolean == false){
                                        Toast.makeText(mContext, "您已永久禁止请去设置打开", Toast.LENGTH_SHORT).show();
                                    }else{
                                        Toast.makeText(mContext, "请同意所有权限", Toast.LENGTH_SHORT).show();
                                    }
                                }
                            });
                        }
                    }
                });

            }
        });
        mRecordButton = findViewById(R.id.mRecordButton);
        mRecordButton.setRecordButtonListener(new RecordButton.RecordButtonListener() {
            @Override
            public void onClick() {
                takePicture();
            }

            @Override
            public void onLongClick() {
                takeVideo();
            }

            @SuppressLint("RestrictedApi")
            @Override
            public void onLongClickFinish(int result) {
                switch (result) {
                    case RecordButton.NORMAL:
                        mCurrentState = RecordButton.NORMAL;
                        mVideoCapture.stopRecording();//停止录制
                        break;
                    case RecordButton.RECORD_SHORT:
                        mCurrentState = RecordButton.RECORD_SHORT;
                        mVideoCapture.stopRecording();//停止录制
                        break;
                    default:
                }
            }
        });
    }

    /**
     * 录制屏幕
     */
    @SuppressLint("RestrictedApi")
    private void takeVideo() {
        if (mVideoCapture != null) {
            File videoFile = createFile(Environment.getExternalStorageDirectory().getPath() + "/CameraX", "yyyy-MM-dd-HH-mm-ss-SSS", ".mp4");
            VideoCapture.OutputFileOptions outputOptions = new VideoCapture.OutputFileOptions.Builder(videoFile)
                    .build();
            if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
                return;
            }
            mVideoCapture.startRecording(outputOptions, mCameraExecutor, new VideoCapture.OnVideoSavedCallback() {
                @Override
                public void onVideoSaved(@NonNull VideoCapture.OutputFileResults outputFileResults) {
                    mMainHandle.post(new Runnable() {
                        @Override
                        public void run() {
                            if (mCurrentState == RecordButton.NORMAL) {
                                //录制成功
                                setRecordView(false);
                                mCurrentVideoPath = outputFileResults.getSavedUri().getPath();
                                //展示视频
                                showVideoView(outputFileResults.getSavedUri());
                            } else {
                                //录制过短
                                Toast.makeText(mContext, "录制过短,重新录制", Toast.LENGTH_SHORT).show();
                                FileUtil.deleteFile(outputFileResults.getSavedUri().getPath());
                            }
                        }
                    });
                }

                @Override
                public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) {

                }
            });
        }
    }

    /**
     * 展示视频
     * @param savedUri
     */
    private void showVideoView(Uri savedUri) {
        mVideoView = new VideoView(mContext);
        mVideoView.setVideoURI(savedUri);
        mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mediaPlayer) {
                mediaPlayer.start();
            }
        });
        mRlVideo.addView(mVideoView,new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        mVideoView.start();
    }

    private void hideVideoView(){
        if(mVideoView != null){
            mVideoView.pause();
            mVideoView = null;
        }
        mRlVideo.removeAllViews();
    }


    private void takePicture() {
        if(mImageCapture != null){
            File photoFile = createFile(Environment.getExternalStorageDirectory().getPath()+"/CameraX", "yyyy-MM-dd-HH-mm-ss-SSS", ".jpg");
            ImageCapture.Metadata metadata = new ImageCapture.Metadata();
            metadata.setReversedHorizontal(lensFacing == CameraSelector.LENS_FACING_FRONT);
            ImageCapture.OutputFileOptions outputOptions = new ImageCapture.OutputFileOptions.Builder(photoFile)
                    .setMetadata(metadata)
                    .build();
            mImageCapture.takePicture(outputOptions, mCameraExecutor, new ImageCapture.OnImageSavedCallback() {
                @Override
                public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
                    mMainHandle.post(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                setRecordView(false);
                                mCurrentPicturePath = outputFileResults.getSavedUri().getPath();
                                Glide.with(mContext).load(mCurrentPicturePath).into(mFlipIv);
                            }catch (Exception e){
                                e.printStackTrace();
                            }
                        }
                    });

                }

                @Override
                public void onError(@NonNull ImageCaptureException exception) {

                }
            });
        }
    }

    private File createFile(String baseFolder, String format, String extension) {
        if(!new File(baseFolder).exists()){
            new File(baseFolder).mkdirs();
        }
        return new File(baseFolder, new SimpleDateFormat(format, Locale.US)
                .format(System.currentTimeMillis()) + extension);
    }

    private void startCameraPreview() {
        ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(mContext);
        cameraProviderFuture.addListener(new Runnable() {
            @Override
            public void run() {
                try {
                    mCameraProvider = cameraProviderFuture.get();
                    if(hasBackCamera()){
                        lensFacing = CameraSelector.LENS_FACING_BACK;
                    }else if(hasFrontCamera()){
                        lensFacing = CameraSelector.LENS_FACING_FRONT;
                    }else{
                        throw new IllegalArgumentException("Back and front camera are unavailable");
                    }
                    updateCameraSwitchButton();
                    bindCameraUseCases();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },ContextCompat.getMainExecutor(mContext));
    }

    @SuppressLint("RestrictedApi")
    private void bindCameraUseCases() {
        CameraSelector cameraSelector = new CameraSelector.Builder().requireLensFacing(lensFacing).build();
        if(mPreview == null){
            mPreview = new Preview.Builder()
                    .setTargetAspectRatio(aspectRatio())
                    // Set initial target rotation
                    .setTargetRotation(Surface.ROTATION_0)
                    .build();
        }
        mImageCapture = new ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
                .setCameraSelector(cameraSelector)
                // We request aspect ratio but no resolution to match preview config, but letting
                // CameraX optimize for whatever specific resolution best fits our use cases
                .setTargetAspectRatio(aspectRatio())
                .setTargetRotation(mPreviewView.getDisplay().getRotation())
                .build();
        mVideoCapture = new VideoCapture.Builder()
                .setTargetAspectRatio(aspectRatio())
                .setTargetRotation(mPreviewView.getDisplay().getRotation())
                .setCameraSelector(cameraSelector)
                .build();
        mCameraProvider.unbindAll();
        try {
            // A variable number of use-cases can be passed here -
            // camera provides access to CameraControl & CameraInfo
            camera = mCameraProvider.bindToLifecycle((LifecycleOwner) mContext, cameraSelector, mPreview,mImageCapture,mVideoCapture);
            // Attach the viewfinder's surface provider to preview use case
            if(first){
                first = false;
                mPreview.setSurfaceProvider(mPreviewView.getSurfaceProvider());
            }
            mPreviewView.getPreviewStreamState().observe((LifecycleOwner) mContext, new Observer<PreviewView.StreamState>() {
                @Override
                public void onChanged(PreviewView.StreamState streamState) {
                    //有可能会多次调用,那怎么办呢?结合规律,他会多次调用PreviewView.StreamState.IDLE或者PreviewView.StreamState.STREAMING
                    //所以很简单,我们记录上一次的状态如果是IDLE,而当前这一次回调是STREAMING,那么就是成功切换后的第一次调用。就只会执行一次
                    if(lastStreamState == PreviewView.StreamState.IDLE && streamState == PreviewView.StreamState.STREAMING){
                        flipImageViewRecycler();
                    }
                    lastStreamState = streamState;
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 预览数据开始后,去掉假设的画面帧
     */
    private void flipImageViewRecycler() {
        mMainHandle.postDelayed(new Runnable() {
            @Override
            public void run() {
                mBtnFlip.setEnabled(true);
                mFlipIv.setImageBitmap(null);
                if(mBitmapFlip != null){
                    mBitmapFlip.recycle();
                    mBitmapFlip = null;
                }
            }
        },200);
    }

    private int aspectRatio() {
        int width = getWidth();
        int height = getHeight();
        double previewRatio = (double)Math.max(width, height) / (double)Math.min(width, height);
        if (Math.abs(previewRatio - RATIO_4_3_VALUE) <= Math.abs(previewRatio - RATIO_16_9_VALUE)) {
            return AspectRatio.RATIO_4_3;
        }
        return AspectRatio.RATIO_16_9;
    }

    private void updateCameraSwitchButton() {
        mBtnFlip.setEnabled(hasBackCamera() && hasFrontCamera());
    }

    private boolean hasFrontCamera() {
        try {
            return mCameraProvider.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ? true: false;
        } catch (CameraInfoUnavailableException e) {
            e.printStackTrace();
            return false;
        }
    }

    private boolean hasBackCamera() {
        try {
            return mCameraProvider.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ? true: false;
        } catch (CameraInfoUnavailableException e) {
            e.printStackTrace();
            return false;
        }
    }

    @Override
    public void onClick(View view) {
        int id = view.getId();
        if(id == R.id.mBtnFlip){
            mBtnFlip.setEnabled(false);
            if(lensFacing == CameraSelector.LENS_FACING_BACK){
                lensFacing = CameraSelector.LENS_FACING_FRONT;
            }else{
                lensFacing = CameraSelector.LENS_FACING_BACK;
            }
            mBitmapFlip = mPreviewView.getBitmap();
            mFlipIv.setImageBitmap(mBitmapFlip);
            bindCameraUseCases();
        }else if(id == R.id.mIvDelete){
            setRecordView(true);
            mFlipIv.setImageBitmap(null);
            hideVideoView();
            FileUtil.deleteFile(mCurrentPicturePath);
            mCurrentPicturePath = null;
            mCurrentVideoPath = null;
        }else if(id == R.id.mIvOk){
            successOption();
        }else if(id == R.id.mIvBack){
            ((Activity)mContext).finish();
        }
    }

    /**
     * 拍摄成功后
     */
    private void successOption() {
        ((Activity)mContext).finish();
        //可在此做回调
        Log.d("yanjin","path = "+ (TextUtils.isEmpty(mCurrentPicturePath)?mCurrentVideoPath:mCurrentPicturePath));
    }

    private void setRecordView(boolean flag){
        if(flag){
            mRecordButton.setVisibility(VISIBLE);
            mIvDelete.setVisibility(GONE);
            mIvOk.setVisibility(GONE);
        }else{
            mRecordButton.setVisibility(GONE);
            mIvDelete.setVisibility(VISIBLE);
            mIvOk.setVisibility(VISIBLE);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if(mCameraExecutor != null){
            mCameraExecutor.shutdownNow();
        }
    }
}

布局recoding_layout.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:background="@color/black">
    <androidx.camera.view.PreviewView
        android:id="@+id/previewView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <ImageView
        android:id="@+id/mFlipIv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <RelativeLayout
        android:id="@+id/mRlVideo"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <ImageView
        android:id="@+id/mIvBack"
        android:layout_width="35dp"
        android:layout_height="35dp"
        android:src="@mipmap/iv_back"
        android:layout_marginTop="20dp"
        android:layout_marginLeft="15dp" />
    <ImageView
        android:id="@+id/mBtnFlip"
        android:layout_width="38dp"
        android:layout_height="38dp"
        android:layout_alignParentRight="true"
        android:layout_marginTop="16dp"
        android:layout_marginRight="15dp"
        android:src="@mipmap/iv_flip_button"/>
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="50dp">
        <com.example.demo.recoding.view.RecordButton
            android:id="@+id/mRecordButton"
            android:layout_width="120dp"
            android:layout_height="120dp"
            android:layout_centerHorizontal="true" />
        <View
            android:id="@+id/mCenterFlagView"
            android:layout_width="120dp"
            android:layout_height="120dp"
            android:layout_centerHorizontal="true"/>
        <ImageView
            android:id="@+id/mIvDelete"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:src="@mipmap/iv_delete"
            android:layout_toLeftOf="@id/mCenterFlagView"
            android:layout_centerVertical="true"
            android:layout_marginRight="30dp"
            android:visibility="gone"/>
        <ImageView
            android:id="@+id/mIvOk"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:src="@mipmap/iv_ok"
            android:layout_toRightOf="@id/mCenterFlagView"
            android:layout_centerVertical="true"
            android:layout_marginLeft="30dp"
            android:visibility="gone"/>
    </RelativeLayout>

</RelativeLayout>

布局里面的图标,有兴趣可以去阿里图标库下载对应的替换上去。
RecordButton

public class RecordButton extends View implements View.OnTouchListener {

    /**
     * 返回参数:正常
     */
    public static final int NORMAL = 0;

    /**
     * 返回参数:录制时间太短
     */
    public static final int RECORD_SHORT = 1;

    /**
     * RecordButton的监听器
     */
    public interface RecordButtonListener {

        /**
         * 点击事件监听
         */
        void onClick();

        /**
         * 长按事件监听
         */
        void onLongClick();

        /**
         * 长按事件结束
         * @param result 返回值状态码 0:正常录制   1:录制时间过短
         */
        void onLongClickFinish(int result);

    }

    /**
     * 触摸延迟为300毫秒
     */
    private static final long TOUCH_DELAY_DEFAULT = 300;
    private long mTouchDelay = TOUCH_DELAY_DEFAULT;

    /**
     * 动画持续时间
     */
    private static final long DURING_TIME = 200;

    /**
     * 录制时间15s
     */
    private static final long RECORD_TIME_DEFAULT = 15000;
    private long mRecordTime = RECORD_TIME_DEFAULT;

    /**
     * 录制过短时间
     */
    private static final long MIN_RECORD_TIME_DEFAULT = 3000;
    private long mMinRecordTime = MIN_RECORD_TIME_DEFAULT;

    /**
     * 缩放系数
     * 取值于微信,暂不提供外部修改接口
     * 可直接修改源码
     */
    private static final float SCALE_NUM = 0.67f;
    /**
     * 初始状态下内外圆的比例
     * 系数取值于微信,暂不提供外部修改接口
     * 可直接修改源码
     */
    private static final float INNER_EXTERNAL = 0.75f;

    /**
     * 触摸时间
     */
    private long mTouchDown;
    /**
     * 触摸动作完成开关
     */
    private volatile boolean isDone = false;
    /**
     * view的宽度
     */
    private int mViewLength = 0;
    /**
     * 内圆,外圆,进度条画笔
     */
    private Paint mInnerPaint, mExternalPaint, mProgressPaint;
    /**
     * progress的rectf
     */
    private RectF mRectF;
    /**
     * 外圆的半径
     */
    private int mExternalCircleRadius;
    /**
     * 内圆的半径
     */
    private int mInnerCircleRadius;
    /**
     * 同心圆的中心点
     */
    private int mCircleCenterX, mCircleCenterY;
    /**
     * progress的宽度
     */
    private float mStrokeWidth = Utils.dp2px(5f);
    /**
     * 进度条的角度
     */
    private float mSweepAngle = 0;
    /**
     * 触摸事件的监听器
     */
    private RecordButtonListener mRecordButtonListener;
    /**
     * 延时任务
     */
    private LongClickRunnable mLongClickRunnable;
    private Handler mHandler = new Handler();

    public RecordButton(Context context) {
        this(context, null);
    }

    public RecordButton(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RecordButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    /**
     * 初始化画笔及其他参数
     *
     * @param context 上下文
     * @param attrs   属性
     */
    private void init(Context context, AttributeSet attrs) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecordButton);
        mRecordTime = (long) a.getFloat(R.styleable.RecordButton_recordTime, RECORD_TIME_DEFAULT);
        mTouchDelay = (long) a.getFloat(R.styleable.RecordButton_touchDelay, TOUCH_DELAY_DEFAULT);
        mMinRecordTime = (long) a.getFloat(R.styleable.RecordButton_minRecordTime, MIN_RECORD_TIME_DEFAULT);
        a.recycle();

        //初始化内部圆的画笔
        mInnerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mInnerPaint.setColor(getResources().getColor(R.color.innercircle));

        //初始化外圈圆的画笔
        mExternalPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mExternalPaint.setColor(getResources().getColor(R.color.externalcircle));
        //初始化弧形画笔
        mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mProgressPaint.setStyle(Paint.Style.STROKE);
        mProgressPaint.setStrokeWidth(mStrokeWidth);
        mProgressPaint.setColor(getResources().getColor(R.color.progress));

        setBackgroundResource(R.color.background);
        setOnTouchListener(this);

    }

    /**
     * 重置参数
     */
    private void reSetParameters() {
        mExternalCircleRadius = (int) ((mViewLength / 2) * SCALE_NUM);
        mInnerCircleRadius = (int) (mExternalCircleRadius * INNER_EXTERNAL);
        mSweepAngle = 0;
        mRectF = new RectF(mStrokeWidth / 2,
                mStrokeWidth / 2,
                mViewLength - (mStrokeWidth / 2),
                mViewLength - (mStrokeWidth / 2));
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int viewWidth = MeasureSpec.getSize(widthMeasureSpec);
        int viewHeigth = MeasureSpec.getSize(heightMeasureSpec);

        mViewLength = Math.min(viewWidth, viewHeigth);
        mCircleCenterX = mViewLength / 2;
        mCircleCenterY = mViewLength / 2;

        reSetParameters();

        setMeasuredDimension(mViewLength, mViewLength);
    }

    @Override
    protected void onDraw(Canvas canvas) {

        //画外圈圆
        canvas.drawCircle(mCircleCenterX, mCircleCenterY, mExternalCircleRadius, mExternalPaint);
        //画内圈圆
        canvas.drawCircle(mCircleCenterX, mCircleCenterY, mInnerCircleRadius, mInnerPaint);
        //画弧形
        canvas.drawArc(mRectF, -90, mSweepAngle, false, mProgressPaint);

    }


    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        if (null == mRecordButtonListener) {
            return false;
        }
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                onActionDown();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                onActionUp();
                break;
            default:
        }
        return true;
    }

    /**
     * 按下时执行操作
     */
    private void onActionDown() {
        if (null == mRecordButtonListener) {
            return;
        }
        //清楚上一个正在执行的任务
        if (null != mLongClickRunnable) {
            mHandler.removeCallbacks(mLongClickRunnable);
        }
        //记录按下时间
        mTouchDown = System.currentTimeMillis();
        //完成置为false
        isDone = false;
        //提交延时任务
        mLongClickRunnable = new LongClickRunnable();
        mHandler.postDelayed(mLongClickRunnable, mTouchDelay);
    }

    /**
     * 抬起时执行操作
     * 手势抬起的时间用于点击判断
     */
    private void onActionUp() {
        //完成开关置为true
        isDone = true;
        //清除动画效果
        clearAnimation();

        long mTouchTime = System.currentTimeMillis() - mTouchDown;

        //如果时间小于touch_delay就是点击事件
        if (mTouchTime < mTouchDelay) {
            mRecordButtonListener.onClick();
        }
    }

    /**
     * 结束动作后执行的逻辑
     * 动画结束后的时间用于长按判断
     */
    private void onActionEndAction() {
        long mTouchTime = System.currentTimeMillis() - mTouchDown;

        //完成开关置为true
        isDone = true;
        //执行结束动画效果
        startEndCircleAnimation();
        //如果触摸事件小于RECORD_SHORT_TIME就是录制过短
        if (mTouchTime < mMinRecordTime) {
            mRecordButtonListener.onLongClickFinish(RecordButton.RECORD_SHORT);
        } else {
            mRecordButtonListener.onLongClickFinish(RecordButton.NORMAL);
        }
    }

    private class LongClickRunnable implements Runnable {

        @Override
        public void run() {
            //如果延迟过后,触摸动作还没有结束
            if (!isDone) {
                //开启开始动画
                startBeginCircleAnimation();
            }
        }
    }

    /**
     * 开启开始动画
     */
    private void startBeginCircleAnimation() {
        //每次缩放动画之前重置参数,防止出现ui错误
        reSetParameters();
        //这里是内圈圆半径获取和赋值
        ValueAnimator animator = ValueAnimator.ofInt(mInnerCircleRadius, (int) (mInnerCircleRadius * SCALE_NUM));
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mInnerCircleRadius = (int) valueAnimator.getAnimatedValue();
                if (isDone) {
                    //如果在开始动画执行的过程中停止触摸动作,及时取消动画
                    valueAnimator.cancel();
                }
                //更新ui
                postInvalidate();
            }
        });
        //这里是外圈圆半径获取和赋值
        ValueAnimator animator1 = ValueAnimator.ofInt(mExternalCircleRadius, (int) (mExternalCircleRadius / SCALE_NUM));
        animator1.setInterpolator(new LinearInterpolator());
        animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mExternalCircleRadius = (int) valueAnimator.getAnimatedValue();
                if (isDone) {
                    //如果在开始动画执行的过程中停止触摸动作,及时取消动画
                    valueAnimator.cancel();
                }
            }
        });
        AnimatorSet set = new AnimatorSet();
        set.playTogether(animator, animator1);
        set.setDuration(DURING_TIME);
        set.setInterpolator(new LinearInterpolator());
        set.start();
        set.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                if (isDone) {
                    return;
                }
                //开始进度条动画
                startProgressAnimation();
                //同时调用长按点击事件
                if (mRecordButtonListener != null) {
                    mRecordButtonListener.onLongClick();
                }
            }
        });

    }

    /**
     * 开始进度条动画
     */
    private void startProgressAnimation() {
        //这里是进度条进度获取和赋值
        ValueAnimator animator = ValueAnimator.ofFloat(0, 360);
        animator.setDuration(mRecordTime);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mSweepAngle = isDone ? 0 : (float) valueAnimator.getAnimatedValue();
                //更新ui
                postInvalidate();
                //如果动作结束了,结束动画
                if (isDone) {
                    valueAnimator.cancel();
                }
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                onActionEndAction();
            }
        });
        animator.start();
    }


    /**
     * 开启结束动画
     */
    private void startEndCircleAnimation() {
        //每次缩放动画之前重置参数,防止出现ui错误
        reSetParameters();
        //这里是内圈圆半径获取和赋值
        ValueAnimator animator = ValueAnimator.ofInt((int) (mInnerCircleRadius * SCALE_NUM), mInnerCircleRadius);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mInnerCircleRadius = (int) valueAnimator.getAnimatedValue();
                if (!isDone) {
                    //如果在结束动画播放过程中再次点击,及时停止动画
                    valueAnimator.cancel();
                }
                //更新ui
                postInvalidate();
            }
        });
        //这里是外圈圆半径获取和赋值
        ValueAnimator animator1 = ValueAnimator.ofInt((int) (mExternalCircleRadius / SCALE_NUM), mExternalCircleRadius);
        animator1.setInterpolator(new LinearInterpolator());
        animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mExternalCircleRadius = (int) valueAnimator.getAnimatedValue();
                if (!isDone) {
                    //如果在结束动画播放过程中再次点击,及时停止动画
                    valueAnimator.cancel();
                }
            }
        });
        AnimatorSet set = new AnimatorSet();
        set.playTogether(animator, animator1);
        set.setDuration(DURING_TIME);
        set.setInterpolator(new LinearInterpolator());
        set.start();
        set.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                postInvalidate();
            }
        });
    }

    /**
     * 设置触摸事件的监听器
     *
     * @param recordButtonListener 触摸监听器
     */
    public void setRecordButtonListener(RecordButtonListener recordButtonListener) {
        this.mRecordButtonListener = recordButtonListener;
    }

    /**
     * 设置触摸延迟时间,区分点击还是长按
     *
     * @param touchDelay 触摸延迟时间(毫秒)
     */
    public void setTouchDelay(long touchDelay) {
        this.mTouchDelay = touchDelay;
    }

    /**
     * 设置最长录制时间
     *
     * @param recordTime 最长录制时间(毫秒)
     */
    public void setRecordTime(long recordTime) {
        this.mRecordTime = recordTime;
    }

    /**
     * 设置多长时间之内算是录制过短
     *
     * @param minRecordTime 录制过短时间(毫秒)
     */
    public void setMinRecordTime(long minRecordTime) {
        this.mMinRecordTime = minRecordTime+mTouchDelay+DURING_TIME;
    }
}

FileUtil

public class FileUtil {
    public static void deleteFile(String path){
        try {
            File file = new File(path);
            if(file.exists()){
                boolean delete = file.delete();
                Log.d("yanjin","文件删除delete="+delete);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

最后activity里面调用就简单了

class RecodingActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(
            WindowManager.LayoutParams.FLAG_FULLSCREEN,
            WindowManager.LayoutParams.FLAG_FULLSCREEN);
        setContentView(R.layout.activity_recoding)
    }
}

activity布局activity_recoding.xml

<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"
    android:background="@color/black"
    tools:context=".recoding.RecodingActivity">
    <com.example.demo.recoding.view.RecodingView
        android:id="@+id/mRecodingView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>

尽量在自定义view里面实现逻辑代码,当然这里缺一个点击ok后将数据返回的回调。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容