[63→100] Android仿微信錄制短視頻

微信朋友圈錄制小視頻,效果圖如下:


拍攝小視頻.png

怎么使用,大家應(yīng)該不陌生了。其中關(guān)鍵技術(shù)有兩個(gè):

  1. 錄制視頻技術(shù);
  2. “按住拍”的動(dòng)畫效果;

在網(wǎng)上搜了幾個(gè)demo,最終發(fā)現(xiàn)下面兩個(gè)開源項(xiàng)目比較靠譜:

  1. RecordVideoDemo ← 重點(diǎn)推薦
  2. WeiXinCamera

RecordVideoDemo中實(shí)現(xiàn)了兩種錄制方法:
a. 采用系統(tǒng)類MediaRecorder。
b. 直接采集攝像頭畫面和聲卡的聲音,再保存為視頻格式。

經(jīng)過統(tǒng)計(jì),6s的視頻,方案a獲取的視頻非常清晰,大小為32M,方案比為200多k。考慮到小視頻上傳、加載速度的要求高于清晰度,所以果斷選擇了方案b。

WeiXinCamera里面實(shí)現(xiàn)“按住拍、線條逐步變窄為0”的動(dòng)畫效果,抽取封裝一下也可以用。

經(jīng)過試驗(yàn),采用動(dòng)畫方案反應(yīng)會(huì)慢幾個(gè)幾秒,體驗(yàn)不好,在VideoCapture里面用ProgressBar來模擬,效果很好

集成步驟

  1. RecordVideoDemo中的WXLikeVideoRecorderLib拷貝到項(xiàng)目目錄
  2. settings.gradle 中添加:
 include ':WXLikeVideoRecorderLib'
  1. app項(xiàng)目的build.gradle中添加依賴:
dependencies{
  compile project(':WXLikeVideoRecorderLib')
}
  1. 添加 攝像頭、音頻、存儲(chǔ)器 的讀寫權(quán)限
<uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
  1. 修改WXLikeVideoRecorder,增加設(shè)置最長(zhǎng)錄制時(shí)間的接口。
// 最長(zhǎng)錄制時(shí)間private long maxRecordTime = 15000;
    /**
     * 設(shè)置最長(zhǎng)錄制時(shí)間
     * @param maxRecordTime
     */
    public void setMaxRecordTime(long maxRecordTime) {
        this.maxRecordTime = maxRecordTime;
    }
  1. 封裝RecordFragmentHolder。
package lib;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.hardware.Camera;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
import sz.itguy.utils.FileUtil;
import sz.itguy.wxlikevideo.camera.CameraHelper;
import sz.itguy.wxlikevideo.recorder.WXLikeVideoRecorder;
import sz.itguy.wxlikevideo.views.CameraPreviewView;
import sz.itguy.wxlikevideo.views.CircleBackgroundTextView;
/**
 * Created by shitianci on 16/6/28.
 */
public class RecordFragmentHolder {
    private static final String TAG = RecordFragmentHolder.class.getSimpleName();
    private final Context mContext;
    private final OnRecordListener mListener;
    private  Camera mCamera;
    private WXLikeVideoRecorder mRecorder;
    private boolean isCancelRecord = false;
    private ValueAnimator animation;
    // 輸出寬度
    private int outputWidth = 320;
    // 輸出高度
    private int outputHeight = 240;

    public interface OnRecordListener{
        void onEnd(String videoPath);
    }
    public RecordFragmentHolder(Context context, OnRecordListener listener) {
        mContext = context;
        mListener = listener;
    }
    /**
     * 初始化空間
     * @param preview 攝像頭預(yù)覽界面
     * @param btnRecord 錄制按鈕
     * @param animationLine 控制線
     * @param duration 時(shí)長(zhǎng)
     * @return
     */
    public boolean init(CameraPreviewView preview, CircleBackgroundTextView btnRecord, final View animationLine, final long duration) {
        // Create an instance of Camera
        int cameraId = CameraHelper.getDefaultCameraID();
        mCamera = CameraHelper.getCameraInstance(cameraId);
        if (null == mCamera) {
            Toast.makeText(mContext, "打開相機(jī)失敗!", Toast.LENGTH_SHORT).show();
            return false;
        }
        // 初始化錄像機(jī)
        mRecorder = new WXLikeVideoRecorder(mContext, FileUtil.MEDIA_FILE_DIR);
        mRecorder.setOutputSize(outputWidth, outputHeight);
        preview.setCamera(mCamera, cameraId);
        mRecorder.setCameraPreviewView(preview);
        btnRecord.setOnTouchListener(new CircleBackgroundTextView.OnTouchListener() {
            @Override
            public void onDownListener(MotionEvent event) {
            }
            @Override
            public void onLongListener(final MotionEvent event) {
                Log.d(TAG, "onLongListener");
                isCancelRecord = false;
                startRecord();
                animation = AnimationUtil.startAnimation(animationLine, duration, new Animator.AnimatorListener() {
                    @Override
                    public void onAnimationStart(Animator animator) {
                    }
                    @Override
                    public void onAnimationEnd(Animator animator) {
                        stopRecord();
                    }
                    @Override
                    public void onAnimationCancel(Animator animator) {
                    }
                    @Override
                    public void onAnimationRepeat(Animator animator) {
                    }
                });
            }
            @Override
            public void onUpListener(MotionEvent event) {
                animation.cancel();
                stopRecord();
            }
        });
        return true;
    }

    /**
     * 設(shè)置輸出的寬高
     * @param outputWidth
     * @param outputHeight
     */
    public void setOutputWidthAndHeight(int outputWidth, int outputHeight) {
        this.outputWidth = outputWidth;
        this.outputHeight = outputHeight;
    }

    public void onPause() {
        if (mRecorder != null) {
            boolean recording = mRecorder.isRecording();
            // 頁(yè)面不可見就要停止錄制
            mRecorder.stopRecording();
            // 錄制時(shí)退出,直接舍棄視頻
            if (recording) {
                FileUtil.deleteFile(mRecorder.getFilePath());
            }
        }
        releaseCamera();              // release the camera immediately on pause event
    }

    private void releaseCamera() {
        if (mCamera != null) {
            mCamera.setPreviewCallback(null);
            // 釋放前先停止預(yù)覽
            mCamera.stopPreview();
            mCamera.release();        // release the camera for other applications
            mCamera = null;
        }
    }

    /**
     * 開始錄制
     */
    public void startRecord() {
        if (mRecorder.isRecording()) {
            Log.d(TAG, "startRecord");
            Toast.makeText(mContext, "正在錄制中…", Toast.LENGTH_SHORT).show();
            return;
        }

        // initialize video camera
        if (prepareVideoRecorder()) {
            // 錄制視頻
            if (!mRecorder.startRecording())
                Toast.makeText(mContext, "錄制失敗…", Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * 準(zhǔn)備視頻錄制器
     *
     * @return
     */
    private boolean prepareVideoRecorder() {
        if (!FileUtil.isSDCardMounted()) {
            Toast.makeText(mContext, "SD卡不可用!", Toast.LENGTH_SHORT).show();
            return false;
        }
        return true;
    }

    /**
     * 停止錄制
     */
    public void stopRecord() {
        mRecorder.stopRecording();
        String videoPath = mRecorder.getFilePath();
        mListener.onEnd(videoPath);
        // 沒有錄制視頻
        if (null == videoPath) {
            return;
        }
        // 若取消錄制,則刪除文件,否則通知宿主頁(yè)面發(fā)送視頻
        if (isCancelRecord) {
            FileUtil.deleteFile(videoPath);
        } else {
            // 告訴宿主頁(yè)面錄制視頻的路徑
//            mContext.startActivity(new Intent(mContext, PlayVideoActiviy.class).putExtra(PlayVideoActiviy.KEY_FILE_PATH, videoPath));
        }
    }
}
  1. 在Fragment引用就可以了
package com.hbbohan.growmemory.view;
import android.Manifest;
import android.os.Bundle;
import android.view.View;
import com.hbbohan.growmemory.B;
import com.hbbohan.growmemory.R;
import java.io.File;
import butterfork.Bind;
import lib.RecordFragmentHolder;
import panda.android.lib.base.ui.fragment.BaseFragment;
import panda.android.lib.base.util.DevUtil;
import panda.android.lib.base.util.IntentUtil;
import sz.itguy.wxlikevideo.views.CameraPreviewView;
import sz.itguy.wxlikevideo.views.CircleBackgroundTextView;
/**
 * Created by shitianci on 16/6/28.
 */
public class RecordVideoFragment extends BaseFragment {
    @Bind(B.id.view_camera_preview)
    CameraPreviewView mViewCameraPreview;
    @Bind(B.id.btn_record)
    CircleBackgroundTextView mBtnRecord;
    @Bind(B.id.view_animation_line)
    View mViewAnimationLine;
    private RecordFragmentHolder mRecordFragmentHolder;
    @Override
    public String[] getPermissions() {
        return new String[]{
                Manifest.permission.CAMERA,
                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.RECORD_AUDIO
        };
    }
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mRecordFragmentHolder = new RecordFragmentHolder(getActivity(), new RecordFragmentHolder.OnRecordListener() {
            @Override
            public void onEnd(String videoPath) {
                DevUtil.showInfo(getActivity(), "視頻存放在:" + videoPath);
                IntentUtil.openFile(getActivity(), new File(videoPath));
            }
        });
        if (!mRecordFragmentHolder.init(mViewCameraPreview, mBtnRecord, mViewAnimationLine, 15000)){
            getActivity().finish();
        }
    }
    @Override
    public void onPause() {
        super.onPause();
        mRecordFragmentHolder.onPause();
        getActivity().finish();
    }
    @Override
    public int getLayoutId() {
        return R.layout.fragment_record_video;
    }
}
  1. 添加動(dòng)畫的引用庫(kù)
package lib;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
/**
 * Created by shitianci on 16/6/28.
 */
public class AnimationUtil {
    private static final String TAG = AnimationUtil.class.getSimpleName();
    /**
     * 動(dòng)畫效果:開始的寬度為父容器的寬度,逐步向中間縮減為0。
     * 使用場(chǎng)景:微信錄制小視頻
     *
     */
    public static ValueAnimator startAnimation(final View view, final long duration, final Animator.AnimatorListener animatorListener) {
        ValueAnimator va = ObjectAnimator.ofInt(view.getWidth(), 0);
        va.setDuration(duration);
        va.addListener(animatorListener);
        va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int value = (int) animation.getAnimatedValue();
                ViewGroup.LayoutParams params = view.getLayoutParams();
                params.width = value;
                view.setLayoutParams(params);
                view.requestLayout();
            }
        });
        //結(jié)束時(shí)恢復(fù)寬高
        final int width = view.getWidth();
        final int height = view.getHeight();
        va.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {
                Log.d(TAG, "onAnimationStart");
            }
            @Override
            public void onAnimationEnd(Animator animator) {
                Log.d(TAG, "onAnimationEnd");
                setViewLayoutParams(view, width, height);
            }
            @Override
            public void onAnimationCancel(Animator animator) {
                Log.d(TAG, "onAnimationCancel");
            }
            @Override
            public void onAnimationRepeat(Animator animator) {
                Log.d(TAG, "onAnimationRepeat");
            }
        });
        va.start();
        return va;
    }

    /**
     * 設(shè)置view的寬高
     * @param view
     * @param width
     * @param height
     */
    public static void setViewLayoutParams(View view, int width, int height) {
        ViewGroup.LayoutParams params = view.getLayoutParams();
        params.width = width;
        params.height = height;
        view.setLayoutParams(params);
        view.requestLayout();
    }
}

備注:如果采用23以上的sdk編譯,在6.0設(shè)備上會(huì)碰到權(quán)限問題,具體解決方案,參考Android M上的權(quán)限獲取問題

Panda
2016-06-28

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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