Osheep

时光不回头,当下最重要。

[63→100] Android仿微信录制短视频

微信朋友圈录制小视频,效果图如下:

《[63→100] Android仿微信录制短视频》

拍摄小视频.png

怎么使用,大家应该不陌生了。其中关键技术有两个:

  1. 录制视频技术;
  2. “按住拍”的动画效果;

在网上搜了几个demo,最终发现下面两个开源项目比较靠谱:

  1. RecordVideoDemo ← 重点推荐
  2. WeiXinCamera

RecordVideoDemo中实现了两种录制方法:
a. 采用系统类MediaRecorder。
b. 直接采集摄像头画面和声卡的声音,再保存为视频格式。

经过统计,6s的视频,方案a获取的视频非常清晰,大小为32M,方案比为200多k。考虑到小视频上传、加载速度的要求高于清晰度,所以果断选择了方案b。

WeiXinCamera里面实现“按住拍、线条逐步变窄为0”的动画效果,抽取封装一下也可以用。

经过试验,采用动画方案反应会慢几个几秒,体验不好,在VideoCapture里面用ProgressBar来模拟,效果很好

集成步骤

  1. RecordVideoDemo中的WXLikeVideoRecorderLib拷贝到项目目录
  2. settings.gradle 中添加:

    include ':WXLikeVideoRecorderLib'
  3. app项目的build.gradle中添加依赖:

    dependencies{
    compile project(':WXLikeVideoRecorderLib')
    }
  4. 添加 摄像头、音频、存储器 的读写权限

    <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
  5. 修改WXLikeVideoRecorder,增加设置最长录制时间的接口。

    // 最长录制时间private long maxRecordTime = 15000;
     /**
      * 设置最长录制时间
      * @param maxRecordTime
      */
     public void setMaxRecordTime(long maxRecordTime) {
         this.maxRecordTime = maxRecordTime;
     }
  6. 封装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 摄像头预览界面
      * @param btnRecord 录制按钮
      * @param animationLine 控制线
      * @param duration 时长
      * @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, "打开相机失败!", Toast.LENGTH_SHORT).show();
             return false;
         }
         // 初始化录像机
         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;
     }
    
     /**
      * 设置输出的宽高
      * @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();
             // 录制时退出,直接舍弃视频
             if (recording) {
                 FileUtil.deleteFile(mRecorder.getFilePath());
             }
         }
         releaseCamera();              // release the camera immediately on pause event
     }
    
     private void releaseCamera() {
         if (mCamera != null) {
             mCamera.setPreviewCallback(null);
             // 释放前先停止预览
             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();
         }
     }
    
     /**
      * 准备视频录制器
      *
      * @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;
         }
         // 若取消录制,则删除文件,否则通知宿主页面发送视频
         if (isCancelRecord) {
             FileUtil.deleteFile(videoPath);
         } else {
             // 告诉宿主页面录制视频的路径
    //            mContext.startActivity(new Intent(mContext, PlayVideoActiviy.class).putExtra(PlayVideoActiviy.KEY_FILE_PATH, videoPath));
         }
     }
    }
  7. 在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;
     }
    }
  8. 添加动画的引用库

    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();
             }
         });
         //结束时恢复宽高
         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;
     }
    
     /**
      * 设置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设备上会碰到权限问题,具体解决方案,参考Android M上的权限获取问题

Panda
2016-06-28

点赞