Android 記錄CameraX拍照錄屏以及翻轉(zhuǎn)相機黑屏處理

1634899868549.gif

先說明一個問題墓毒,本記錄沒有解決CameraX前置拍攝視頻,視頻畫面鏡像問題亲怠,如果和我一樣被這個困惑了的所计,就不要看了,我沒解決团秽,誰要是知道怎么解決麻煩評論去告訴我主胧,謝了叭首。(猜想:是不是要設(shè)置一個ImageAnalysis解析數(shù)據(jù)流,錄屏時動態(tài)鏡像啊踪栋。)

把想要的東西都封裝在一個自定義view里面焙格,使用時只要添加在activity的布局里面,外部無需關(guān)心調(diào)用夷都。
值得一提的是眷唉,他切換相機會黑屏,原因是CameraX的切換攝像頭邏輯是先解綁在綁定囤官。我猜在解綁的時候冬阳,PreviewView的數(shù)據(jù)斷流了,自己也沒有保持最后一幀畫面治拿,就是黑屏的摩泪。那么他沒保持最后一幀,那么我們自己保持就行了劫谅,關(guān)鍵PreviewView是提供了獲取畫面bitmap方法的见坑。具體邏輯看下面的mPreviewView.getBitmap()相關(guān)邏輯代碼。
還有就是他前置拍攝照片也是鏡像的捏检,需要我們設(shè)置一下

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

我就想不通了荞驴,你拍照有設(shè)置,為啥錄視頻沒有贯城,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'

權(quán)限

<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, "您已永久禁止請去設(shè)置打開", Toast.LENGTH_SHORT).show();
                                    }else{
                                        Toast.makeText(mContext, "請同意所有權(quán)限", 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) {
                    //有可能會多次調(diào)用能犯,那怎么辦呢鲫骗?結(jié)合規(guī)律,他會多次調(diào)用PreviewView.StreamState.IDLE或者PreviewView.StreamState.STREAMING
                    //所以很簡單踩晶,我們記錄上一次的狀態(tài)如果是IDLE执泰,而當前這一次回調(diào)是STREAMING,那么就是成功切換后的第一次調(diào)用渡蜻。就只會執(zhí)行一次
                    if(lastStreamState == PreviewView.StreamState.IDLE && streamState == PreviewView.StreamState.STREAMING){
                        flipImageViewRecycler();
                    }
                    lastStreamState = streamState;
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 預覽數(shù)據(jù)開始后术吝,去掉假設(shè)的畫面幀
     */
    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();
        //可在此做回調(diào)
        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>

布局里面的圖標,有興趣可以去阿里圖標庫下載對應(yīng)的替換上去茸苇。
RecordButton

public class RecordButton extends View implements View.OnTouchListener {

    /**
     * 返回參數(shù):正常
     */
    public static final int NORMAL = 0;

    /**
     * 返回參數(shù):錄制時間太短
     */
    public static final int RECORD_SHORT = 1;

    /**
     * RecordButton的監(jiān)聽器
     */
    public interface RecordButtonListener {

        /**
         * 點擊事件監(jiān)聽
         */
        void onClick();

        /**
         * 長按事件監(jiān)聽
         */
        void onLongClick();

        /**
         * 長按事件結(jié)束
         * @param result 返回值狀態(tài)碼 0:正常錄制   1:錄制時間過短
         */
        void onLongClickFinish(int result);

    }

    /**
     * 觸摸延遲為300毫秒
     */
    private static final long TOUCH_DELAY_DEFAULT = 300;
    private long mTouchDelay = TOUCH_DELAY_DEFAULT;

    /**
     * 動畫持續(xù)時間
     */
    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;

    /**
     * 縮放系數(shù)
     * 取值于微信排苍,暫不提供外部修改接口
     * 可直接修改源碼
     */
    private static final float SCALE_NUM = 0.67f;
    /**
     * 初始狀態(tài)下內(nèi)外圓的比例
     * 系數(shù)取值于微信,暫不提供外部修改接口
     * 可直接修改源碼
     */
    private static final float INNER_EXTERNAL = 0.75f;

    /**
     * 觸摸時間
     */
    private long mTouchDown;
    /**
     * 觸摸動作完成開關(guān)
     */
    private volatile boolean isDone = false;
    /**
     * view的寬度
     */
    private int mViewLength = 0;
    /**
     * 內(nèi)圓学密,外圓淘衙,進度條畫筆
     */
    private Paint mInnerPaint, mExternalPaint, mProgressPaint;
    /**
     * progress的rectf
     */
    private RectF mRectF;
    /**
     * 外圓的半徑
     */
    private int mExternalCircleRadius;
    /**
     * 內(nèi)圓的半徑
     */
    private int mInnerCircleRadius;
    /**
     * 同心圓的中心點
     */
    private int mCircleCenterX, mCircleCenterY;
    /**
     * progress的寬度
     */
    private float mStrokeWidth = Utils.dp2px(5f);
    /**
     * 進度條的角度
     */
    private float mSweepAngle = 0;
    /**
     * 觸摸事件的監(jiān)聽器
     */
    private RecordButtonListener mRecordButtonListener;
    /**
     * 延時任務(wù)
     */
    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);
    }

    /**
     * 初始化畫筆及其他參數(shù)
     *
     * @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();

        //初始化內(nèi)部圓的畫筆
        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);

    }

    /**
     * 重置參數(shù)
     */
    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);
        //畫內(nèi)圈圓
        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;
    }

    /**
     * 按下時執(zhí)行操作
     */
    private void onActionDown() {
        if (null == mRecordButtonListener) {
            return;
        }
        //清楚上一個正在執(zhí)行的任務(wù)
        if (null != mLongClickRunnable) {
            mHandler.removeCallbacks(mLongClickRunnable);
        }
        //記錄按下時間
        mTouchDown = System.currentTimeMillis();
        //完成置為false
        isDone = false;
        //提交延時任務(wù)
        mLongClickRunnable = new LongClickRunnable();
        mHandler.postDelayed(mLongClickRunnable, mTouchDelay);
    }

    /**
     * 抬起時執(zhí)行操作
     * 手勢抬起的時間用于點擊判斷
     */
    private void onActionUp() {
        //完成開關(guān)置為true
        isDone = true;
        //清除動畫效果
        clearAnimation();

        long mTouchTime = System.currentTimeMillis() - mTouchDown;

        //如果時間小于touch_delay就是點擊事件
        if (mTouchTime < mTouchDelay) {
            mRecordButtonListener.onClick();
        }
    }

    /**
     * 結(jié)束動作后執(zhí)行的邏輯
     * 動畫結(jié)束后的時間用于長按判斷
     */
    private void onActionEndAction() {
        long mTouchTime = System.currentTimeMillis() - mTouchDown;

        //完成開關(guān)置為true
        isDone = true;
        //執(zhí)行結(jié)束動畫效果
        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() {
            //如果延遲過后,觸摸動作還沒有結(jié)束
            if (!isDone) {
                //開啟開始動畫
                startBeginCircleAnimation();
            }
        }
    }

    /**
     * 開啟開始動畫
     */
    private void startBeginCircleAnimation() {
        //每次縮放動畫之前重置參數(shù)腻暮,防止出現(xiàn)ui錯誤
        reSetParameters();
        //這里是內(nèi)圈圓半徑獲取和賦值
        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) {
                    //如果在開始動畫執(zhí)行的過程中停止觸摸動作幔翰,及時取消動畫
                    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) {
                    //如果在開始動畫執(zhí)行的過程中停止觸摸動作漩氨,及時取消動畫
                    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();
                //同時調(diào)用長按點擊事件
                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();
                //如果動作結(jié)束了,結(jié)束動畫
                if (isDone) {
                    valueAnimator.cancel();
                }
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                onActionEndAction();
            }
        });
        animator.start();
    }


    /**
     * 開啟結(jié)束動畫
     */
    private void startEndCircleAnimation() {
        //每次縮放動畫之前重置參數(shù)遗增,防止出現(xiàn)ui錯誤
        reSetParameters();
        //這里是內(nèi)圈圓半徑獲取和賦值
        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) {
                    //如果在結(jié)束動畫播放過程中再次點擊叫惊,及時停止動畫
                    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) {
                    //如果在結(jié)束動畫播放過程中再次點擊,及時停止動畫
                    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();
            }
        });
    }

    /**
     * 設(shè)置觸摸事件的監(jiān)聽器
     *
     * @param recordButtonListener 觸摸監(jiān)聽器
     */
    public void setRecordButtonListener(RecordButtonListener recordButtonListener) {
        this.mRecordButtonListener = recordButtonListener;
    }

    /**
     * 設(shè)置觸摸延遲時間做修,區(qū)分點擊還是長按
     *
     * @param touchDelay 觸摸延遲時間(毫秒)
     */
    public void setTouchDelay(long touchDelay) {
        this.mTouchDelay = touchDelay;
    }

    /**
     * 設(shè)置最長錄制時間
     *
     * @param recordTime 最長錄制時間(毫秒)
     */
    public void setRecordTime(long recordTime) {
        this.mRecordTime = recordTime;
    }

    /**
     * 設(shè)置多長時間之內(nèi)算是錄制過短
     *
     * @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里面調(diào)用就簡單了

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里面實現(xiàn)邏輯代碼霍狰,當然這里缺一個點擊ok后將數(shù)據(jù)返回的回調(diào)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末饰及,一起剝皮案震驚了整個濱河市蔗坯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌燎含,老刑警劉巖宾濒,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異屏箍,居然都是意外死亡绘梦,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門赴魁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來卸奉,“玉大人,你說我怎么就攤上這事颖御¢茫” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵潘拱,是天一觀的道長疹鳄。 經(jīng)常有香客問我,道長芦岂,這世上最難降的妖魔是什么瘪弓? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮盔腔,結(jié)果婚禮上杠茬,老公的妹妹穿的比我還像新娘月褥。我一直安慰自己弛随,他們只是感情好,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布宁赤。 她就那樣靜靜地躺著舀透,像睡著了一般。 火紅的嫁衣襯著肌膚如雪决左。 梳的紋絲不亂的頭發(fā)上愕够,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天走贪,我揣著相機與錄音,去河邊找鬼惑芭。 笑死坠狡,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的遂跟。 我是一名探鬼主播逃沿,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼幻锁!你這毒婦竟也來了凯亮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤哄尔,失蹤者是張志新(化名)和其女友劉穎假消,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體岭接,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡富拗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了亿傅。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片媒峡。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖葵擎,靈堂內(nèi)的尸體忽然破棺而出谅阿,到底是詐尸還是另有隱情,我是刑警寧澤酬滤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布签餐,位于F島的核電站,受9級特大地震影響盯串,放射性物質(zhì)發(fā)生泄漏氯檐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一体捏、第九天 我趴在偏房一處隱蔽的房頂上張望冠摄。 院中可真熱鬧,春花似錦几缭、人聲如沸河泳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拆挥。三九已至,卻和暖如春某抓,著一層夾襖步出監(jiān)牢的瞬間纸兔,已是汗流浹背惰瓜。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留汉矿,地道東北人崎坊。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像洲拇,于是被迫代替她去往敵國和親流强。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容