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

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


拍攝小視頻.png

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

  1. 錄制視頻技術(shù)厕妖;
  2. “按住拍”的動畫效果首尼;

在網(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”的動畫效果刻伊,抽取封裝一下也可以用。

經(jīng)過試驗(yàn)椒功,采用動畫方案反應(yīng)會慢幾個(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. 添加 攝像頭丁屎、音頻、存儲器 的讀寫權(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è)置最長錄制時(shí)間的接口悦屏。
// 最長錄制時(shí)間private long maxRecordTime = 15000;
    /**
     * 設(shè)置最長錄制時(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í)長
     * @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();
            // 頁面不可見就要停止錄制
            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;
        }
        // 若取消錄制看蚜,則刪除文件,否則通知宿主頁面發(fā)送視頻
        if (isCancelRecord) {
            FileUtil.deleteFile(videoPath);
        } else {
            // 告訴宿主頁面錄制視頻的路徑
//            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. 添加動畫的引用庫
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();
    /**
     * 動畫效果:開始的寬度為父容器的寬度赔桌,逐步向中間縮減為0供炎。
     * 使用場景:微信錄制小視頻
     *
     */
    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è)備上會碰到權(quán)限問題疾党,具體解決方案音诫,參考Android M上的權(quán)限獲取問題

Panda
2016-06-28

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末雪位,一起剝皮案震驚了整個(gè)濱河市竭钝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌雹洗,老刑警劉巖香罐,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異时肿,居然都是意外死亡庇茫,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門螃成,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炭分,“玉大人,你說我怎么就攤上這事参淹×锻牛” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵击吱,是天一觀的道長淋淀。 經(jīng)常有香客問我,道長覆醇,這世上最難降的妖魔是什么朵纷? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮永脓,結(jié)果婚禮上袍辞,老公的妹妹穿的比我還像新娘。我一直安慰自己常摧,他們只是感情好搅吁,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布威创。 她就那樣靜靜地躺著,像睡著了一般谎懦。 火紅的嫁衣襯著肌膚如雪肚豺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天界拦,我揣著相機(jī)與錄音吸申,去河邊找鬼。 笑死享甸,一個(gè)胖子當(dāng)著我的面吹牛截碴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蛉威,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼日丹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蚯嫌?” 一聲冷哼從身側(cè)響起哲虾,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎齐帚,沒想到半個(gè)月后妒牙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡对妄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年湘今,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片剪菱。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡摩瞎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出孝常,到底是詐尸還是另有隱情旗们,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布构灸,位于F島的核電站上渴,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏喜颁。R本人自食惡果不足惜稠氮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望半开。 院中可真熱鬧隔披,春花似錦、人聲如沸寂拆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鬓长,卻和暖如春谒拴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背痢士。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工彪薛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人怠蹂。 一個(gè)月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像少态,于是被迫代替她去往敵國和親城侧。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345

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