点击上方“何俊林”,马上关注,每天早上8:50准时推送
真爱,请置顶或星标
本项目是一款Android视频编辑项目,功能有视频拍摄,视频裁剪,视频滤镜,视频压缩。
效果图
-
点击拍照
-
10s的视频大概1.9M左右
-
长按录视频(视频长度可设置)
-
长按录视频的时候,手指上滑可以放大视频
-
录制完视频可以浏览并且重复播放
-
前后摄像头的切换
-
可以设置小视频保存路径
这里实现的是仿微信的视频编辑页面,主要是播放视频和显示该视频的一系列图片,可以滑动图片的列表,视频也跟着动(seekto),然后可以拖动滑块实现视频的seekto。最后会进行视频的剪切操作,就是剪切2个滑块之前的区域,视频重复播放2个滑块之间的区域。
先看下效果图:
源码:https://github.com/ta893115871/VideoEdit
对应实现思路实现是:上面是videoview(你也可以用一些开源的视频播放器),底部是recycleview(显示视频的提取图片)+自定义view rankbar (用于制定需要截取的视频)。
一.高效率提取视频图片android提取视频多张图片和视频信息这篇文章中用
Bitmap bitmap = metadataRetriever.getFrameAtTime(time * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
返回的是bitmap图片然后保存到sd中然后用glide进行展示,因为这里展示的图片是小图且没有必要很清晰,所以需要对bitmap进行处理然后再保存再展示,以达到快速提取的目标:
/** * 设置固定的宽度,高度随之变化,使图片不会变形 * * @param bm Bitmap * @return Bitmap */ private Bitmap scaleImage(Bitmap bm) { if (bm == null) { return null; } int width = bm.getWidth(); int height = bm.getHeight(); float scaleWidth = extractW * 1.0f / width; // float scaleHeight =extractH*1.0f / height; Matrix matrix = new Matrix(); matrix.postScale(scaleWidth, scaleWidth); Bitmap newBm = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true); if (!bm.isRecycled()) { bm.recycle(); bm = null; } return newBm; }二.recycleview滑动展示的视频提取的图片
这里demo中的展示视频缩率图的整个的宽是固定的,所以以宽为准的高度随之变化,大家可以根据具体的需要做相应的处理。 因为视频的时长不固定,而且一般的短视频考虑到服务器以及成本,所以视频一般不会太长,比如微信:最长可以支持上传10s的视频 (我的需求是最长支持60s的视频),通过观察微信如下规则:
视频时长<=10s的就展示固定的10张图片,则提取图片的时间平均长度:视频时长/10=interval 视频时长>10s的展示更多的图片,则提取的图片的时间平均长度:10s/10=interval,不一定是这个算法哈,这里只是从技术的角度预定一个规则哈。一旦视频时长>10s,视频的缩率图是可以滑动的,而且可以根据左边的滑块视频自动的seekto展示那一秒的视频。 这里的demo会按60s来算,也就是将123步骤中的10s改为60s的规则。 所以我们要根据视频的时长来计算提取视频图片的个数和整个展示这些图片的宽,计算如下:
long startPosition = 0; long endPosition = duration; int thumbnailsCount; int rangeWidth; boolean isOver_60_s; //MAX_CUT_DURATION为60 秒 //MAX_COUNT_RANGE为10张 if (endPosition <= MAX_CUT_DURATION) { isOver_60_s = false; thumbnailsCount = MAX_COUNT_RANGE; rangeWidth = mMaxWidth; } else { isOver_60_s = true; thumbnailsCount = (int) (endPosition * 1.0f / (MAX_CUT_DURATION * 1.0f) * MAX_COUNT_RANGE); rangeWidth = mMaxWidth / MAX_COUNT_RANGE * thumbnailsCount; } mRecyclerView.addItemDecoration(new EditSpacingItemDecoration(UIUtil.dip2px(this, 35), thumbnailsCount));
超过60s的视频提取的图片个数为是:视频时长/60s * 10张。整个展示这些图片的宽也就是recycleview的宽rangeWidth为:mMaxWidth( 屏幕或者减去设计师的宽)/10张*视频提取的图片个数。 既然可以滑动底部的recycleview的视频缩率图,根据滑动的位置来计算视频应该展示seenkto哪一秒,所以我们需要知道滑动的距离以及每毫秒所占的px(像素)averageMsPx,所以:
averageMsPx = duration(时长) * 1.0f(乘以1.0为了精确) / rangeWidth (recycleview的宽,不是可见区域) * 1.0f;
然后就可以利用recycleview的OnScrollListener方法进行监听处理,具体的代码可以下载去看,这里只是梳理下思路。
三.videoview的使用这个没啥好说的了,就是android为了方便开发者的使用而封装的控件。 我的需求是使用的ijk播放器(地址:https://github.com/Bilibili/ijkplayer),这里就用的videoview不过都一样。获取播放的进度可以用handler实现来保证视频一直在所选中的范围内重复播放。
private Handler handler = new Handler(); private Runnable run = new Runnable() { @Override public void run() { videoProgressUpdate(); handler.postDelayed(run, 1000); } }; private void videoProgressUpdate() { long currentPosition = mVideoView.getCurrentPosition(); Log.d(TAG, "----onProgressUpdate-cp---->>>>>>>" + currentPosition); if (currentPosition >= (rightProgress)) { mVideoView.seekTo((int) leftProgress); positionIcon.clearAnimation(); if (animator != null && animator.isRunning()) { animator.cancel(); } anim(); } }四.videoview的坑和视频进度指针
就是中间那个黄色的渐变矩形,就是一个imageview通过动画不断的设置它的leftMargin即可。动画就是使用ValueAnimator来生成一系列的数字,然后监听回调即可。有人会说为什么不根据进度进行设置呢?我尝试过用视频的进度进行处理,当时用的ijk播放器,好多都是异步回调的,比如seekto方法,这个就很难控制了,导致进度不准确,而且进度顿顿的感觉。后来采用了动画就很好了,反正本地视频一般不会卡吧。这个可以去看下代码。
这个demo中我改为videoview后发现以下问题: 我测试了下:有时候seekto方法你调用完了,立马去获取进度可能为0。所以指针的位置不对了。 WTF?!why?因为视频seekto方法VideoView的seekTo是异步执行的,会有seek未完成但播放已经开始的现象。
所以我们应该在seekComplete回调当中处理,但是呢?VideoView是基于MediaPlayer实现的,但没提供setOnSeekCompleteListener方法。我们可以设置videoview的OnPrepared监听才拿到MediaPlayer,对MediaPlayer进行设置。
mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { //设置MediaPlayer的OnSeekComplete监听 mp.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() { @Override public void onSeekComplete(MediaPlayer mp) { Log.d(TAG, "------ok----real---start-----" ); if (!isSeeking) { videoStart(); } } }); } });
锁屏/home键 指针位置不正确,改为从左边开始重新播放就行了。 当前这些问题在demo中都已经修复,具体可以看下代码
五.自定义两侧拖动控件-RankBar这个算是除了视频提取图片外的比较重要的部分了。目标呢就是这个控件可以回调出来一个方法,可以知道按下去,滑动中,抬起来等状态,可以知道左右的2个视频时间,可以知道拖动的是哪个,这个回调看起来这样。
private final RangeSeekBar.OnRangeSeekBarChangeListener mOnRangeSeekBarChangeListener = new RangeSeekBar.OnRangeSeekBarChangeListener() { @Override public void onRangeSeekBarValuesChanged(RangeSeekBar bar, long minValue, long maxValue, int action, boolean isMin, RangeSeekBar.Thumb pressedThumb) { Log.d(TAG, "-----minValue----->>>>>>" + minValue); Log.d(TAG, "-----maxValue----->>>>>>" + maxValue); leftProgress = minValue + scrollPos; rightProgress = maxValue + scrollPos; Log.d(TAG, "-----leftProgress----->>>>>>" + leftProgress); Log.d(TAG, "-----rightProgress----->>>>>>" + rightProgress); switch (action) { case MotionEvent.ACTION_DOWN: Log.d(TAG, "-----ACTION_DOWN---->>>>>>"); videoPause(); break; case MotionEvent.ACTION_MOVE: Log.d(TAG, "-----ACTION_MOVE---->>>>>>"); mVideoView.seekTo((int) (pressedThumb == RangeSeekBar.Thumb.MIN ? leftProgress : rightProgress)); break; case MotionEvent.ACTION_UP: Log.d(TAG, "-----ACTION_UP--leftProgress--->>>>>>" + leftProgress); //从minValue开始播 mVideoView.seekTo((int) leftProgress); videoStart(); break; default: break; } } };
自定义view不是这篇的重点,但是只要搞清楚自定义的一些方法和步骤就可以搞定一些简单的控件。 下面是extend view的一些步骤:
-
分析控件的基本可变可扩展可暴漏的属性:attr文件
-
重写构造方法获取属性值,处理默认值
-
重写onMeasure方法,设置宽高,处理wrap_content(给定默认值)
-
重新onDraw方法 ,比如:绘制图片,文件,bitmap的缩放等
-
处理事件,如果是extend view 则一般只需要处理onTouchEvent,进行事件的消费,一般返回true,因为要消费啊,可以你需要处理多指触摸的情况,滑动的临界值等等。事件的走向。
-
善后处理,比如onSaveInstanceState,onRestoreInstanceState的处理
-
回调接口的书写