微信朋友圈录制小视频,效果图如下:
怎么使用,大家应该不陌生了。其中关键技术有两个:
- 录制视频技术;
- “按住拍”的动画效果;
在网上搜了几个demo,最终发现下面两个开源项目比较靠谱:
- RecordVideoDemo ← 重点推荐
- WeiXinCamera
RecordVideoDemo中实现了两种录制方法:
a. 采用系统类MediaRecorder。
b. 直接采集摄像头画面和声卡的声音,再保存为视频格式。
经过统计,6s的视频,方案a获取的视频非常清晰,大小为32M,方案比为200多k。考虑到小视频上传、加载速度的要求高于清晰度,所以果断选择了方案b。
WeiXinCamera里面实现“按住拍、线条逐步变窄为0”的动画效果,抽取封装一下也可以用。
经过试验,采用动画方案反应会慢几个几秒,体验不好,在VideoCapture里面用ProgressBar来模拟,效果很好
集成步骤
- 把
RecordVideoDemo
中的WXLikeVideoRecorderLib拷贝到项目目录 -
settings.gradle
中添加:
include ':WXLikeVideoRecorderLib'
- app项目的
build.gradle
中添加依赖:
dependencies{
compile project(':WXLikeVideoRecorderLib')
}
- 添加 摄像头、音频、存储器 的读写权限
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
- 修改WXLikeVideoRecorder,增加设置最长录制时间的接口。
// 最长录制时间private long maxRecordTime = 15000;
/**
* 设置最长录制时间
* @param maxRecordTime
*/
public void setMaxRecordTime(long maxRecordTime) {
this.maxRecordTime = maxRecordTime;
}
- 封装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));
}
}
}
- 在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;
}
}
- 添加动画的引用库
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