
簡單的視頻剪切功能,支持每隔1s獲取一張縮略圖,移動(dòng)seekbar視頻會(huì)在區(qū)間里面重復(fù)播放
用到的第三方庫
圖片視頻選擇庫
//圖片選擇庫
implementation 'com.github.HuanTanSheng:EasyPhotos:3.1.3'
//圖片加載庫
implementation("com.github.bumptech.glide:glide:4.11.0") {
exclude group: "com.android.support"
}
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
implementation 'jp.wasabeef:glide-transformations:4.0.1'
FFmpeg庫
//ffmpeg
implementation 'com.github.microshow:RxFFmpeg:4.9.0'
EasyPhotos使用很簡單
EasyPhotos.createAlbum(this, false, false, GlideEngine.getInstance())
.setCount(1)//參數(shù)說明:最大可選數(shù),默認(rèn)1
.onlyVideo()
.setVideoMinSecond(11)
.setFileProviderAuthority("com.example.demo.fileprovider")
.start(REQUEST_VIDEO_BACK_CODE)
執(zhí)行上面的代碼,就能只選取手機(jī)中的視頻了,而且選擇的視頻最小時(shí)長是11s。FileProvider需要自己配置,配置好了放在setFileProviderAuthority里面。GlideEngine代碼如下:
public class GlideEngine implements ImageEngine {
//單例
private static GlideEngine instance = null;
//單例模式,私有構(gòu)造方法
private GlideEngine() {
}
//獲取單例
public static GlideEngine getInstance() {
if (null == instance) {
synchronized (GlideEngine.class) {
if (null == instance) {
instance = new GlideEngine();
}
}
}
return instance;
}
/**
* 加載圖片到ImageView
*
* @param context 上下文
* @param uri 圖片路徑Uri
* @param imageView 加載到的ImageView
*/
//安卓10推薦uri,并且path的方式不再可用
@Override
public void loadPhoto(@NonNull Context context, @NonNull Uri uri, @NonNull ImageView imageView) {
Glide.with(context).load(uri).transition(withCrossFade()).into(imageView);
}
/**
* 加載gif動(dòng)圖圖片到ImageView,gif動(dòng)圖不動(dòng)
*
* @param context 上下文
* @param gifUri gif動(dòng)圖路徑Uri
* @param imageView 加載到的ImageView
* <p>
* 備注:不支持動(dòng)圖顯示的情況下可以不寫
*/
//安卓10推薦uri,并且path的方式不再可用
@Override
public void loadGifAsBitmap(@NonNull Context context, @NonNull Uri gifUri, @NonNull ImageView imageView) {
Glide.with(context).asBitmap().load(gifUri).into(imageView);
}
/**
* 加載gif動(dòng)圖到ImageView,gif動(dòng)圖動(dòng)
*
* @param context 上下文
* @param gifUri gif動(dòng)圖路徑Uri
* @param imageView 加載動(dòng)圖的ImageView
* <p>
* 備注:不支持動(dòng)圖顯示的情況下可以不寫
*/
//安卓10推薦uri,并且path的方式不再可用
@Override
public void loadGif(@NonNull Context context, @NonNull Uri gifUri, @NonNull ImageView imageView) {
Glide.with(context).asGif().load(gifUri).transition(withCrossFade()).into(imageView);
}
/**
* 獲取圖片加載框架中的緩存Bitmap,不用拼圖功能可以直接返回null
*
* @param context 上下文
* @param uri 圖片路徑
* @param width 圖片寬度
* @param height 圖片高度
* @return Bitmap
* @throws Exception 異常直接拋出,EasyPhotos內(nèi)部處理
*/
//安卓10推薦uri,并且path的方式不再可用
@Override
public Bitmap getCacheBitmap(@NonNull Context context, @NonNull Uri uri, int width, int height) throws Exception {
return Glide.with(context).asBitmap().load(uri).submit(width, height).get();
}
}
然后在onActivityResult里面能收到我們選擇的視頻文件信息,我們拿到信息就能跳轉(zhuǎn)到剪輯頁面了。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(resultCode == RESULT_OK){
if(requestCode == REQUEST_VIDEO_BACK_CODE){
try {
val resultPhotos: ArrayList<Photo> = data!!.getParcelableArrayListExtra(EasyPhotos.RESULT_PHOTOS)!!
if(resultPhotos != null && resultPhotos.size >= 1){
var videoIntent = Intent(this, VideoCutActivity::class.java)
videoIntent.putExtra(VideoCutActivity.PATH, resultPhotos[0])
Log.d("yanjin", "path1 = ${resultPhotos[0]}")
startActivity(videoIntent)
}
}catch (e:Exception){
e.printStackTrace()
}
}
}
}
先看activity布局代碼
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/black"
tools:context=".video_cut.VideoCutActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="4"
android:gravity="center"
android:layout_gravity="center">
<android.widget.VideoView
android:id="@+id/mVideoView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
<TextView
android:id="@+id/mTvOk"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="截取"
android:textSize="18sp"
android:padding="@dimen/dp_10"
android:layout_alignParentRight="true"
android:layout_marginTop="15dp"
android:layout_marginRight="15dp"
android:textColor="@android:color/white"/>
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mRecyclerView"
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:clipToPadding="false"
android:layout_marginTop="10dp" />
<com.example.demo.video_cut.view.RangeSeekBarView
android:id="@+id/mRangeSeekBarView"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"/>
<!--為兩端的空間增加蒙層start-->
<View
android:layout_width="20dp"
android:layout_height="60dp"
android:background="@color/shadow_color"/>
<View
android:layout_width="20dp"
android:layout_height="60dp"
android:layout_alignParentRight="true"
android:background="@color/shadow_color"/>
<!--為兩端的空間增加蒙層end-->
</RelativeLayout>
</RelativeLayout>
</LinearLayout>
然后是activity代碼
class VideoCutActivity : AppCompatActivity() {
private val mCacheRootPath by lazy {//設(shè)置保存每一幀圖片保存的路徑根目錄
Environment.getExternalStorageDirectory().path + File.separator + "videoCut" + File.separator
}
private var resouce: Photo? = null
private var mp: MediaPlayer? = null
private var mFrames = 0
private val list = ArrayList<String>()
private var mWidth = Utils.dp2px(35f)
private var mHeight = Utils.dp2px(50f)
private val mAdapter by lazy {
FramesAdapter()
}
private var mMinTime:Long = 0*1000//默認(rèn)從0s開始
private var mMaxTime:Long = (MAX_TIME*1000).toLong()//默認(rèn)從10s開始,單位是毫秒
private var mFirstPosition = 0
private var timer:Timer? = null
private var timerTaskImp: TimerTaskImp? = null
private var mCurrentSubscriber:MyRxFFmpegSubscriber? = null
private var loadingDialog:AlertDialog? = null
private val outDir by lazy {
mCacheRootPath + Utils.getFileName(resouce?.name)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_video_cut)
resouce = intent.getParcelableExtra<Photo>(PATH)
mRangeSeekBarView?.post {
mWidth = (mRangeSeekBarView?.width!!/MAX_TIME).toInt()
mAdapter.setItemWidth(mWidth)//根據(jù)seekbar的長度除以我們最大幀數(shù),就是我們每一幀需要的寬度
}
mTvOk?.setOnClickListener {
trimVideo()
}
initFramesList()
//播放視頻,在視頻播放準(zhǔn)備完畢后再獲取一共有多少幀
initVideo()
}
private fun initFramesList() {
mRecyclerView?.apply {
layoutManager =
LinearLayoutManager(this@VideoCutActivity, LinearLayoutManager.HORIZONTAL, false)
adapter = mAdapter
}
mRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager:LinearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
mFirstPosition = layoutManager.findFirstVisibleItemPosition()
Log.d("yanjin", "$TAG mFirstPosition = $mFirstPosition")
mMinTime = mRangeSeekBarView.selectedMinValue + (mFirstPosition * 1000)
mMaxTime = mRangeSeekBarView.selectedMaxValue + (mFirstPosition * 1000)
mRangeSeekBarView?.setStartEndTime(mMinTime,mMaxTime)
mRangeSeekBarView?.invalidate()
reStartVideo()
}
})
}
private fun initVideo() {
mVideoView?.setVideoURI(resouce?.uri)
mVideoView?.requestFocus()
mVideoView?.start()
startTimer()
mVideoView?.setOnPreparedListener {
mp = it//可以用來seekTo哦
//設(shè)置seekbar
initSeekBar()
//解析視頻畫面幀
analysisVideo()
}
}
private fun startTimer() {
if(timer == null){
timer = Timer()
timerTaskImp = TimerTaskImp(this)
timer?.schedule(timerTaskImp,0,100)//數(shù)值越小,檢查視頻播放區(qū)間誤差越小,但是危害就是性能越卡
}
}
private fun initSeekBar() {
mRangeSeekBarView?.selectedMinValue = mMinTime
mRangeSeekBarView?.selectedMaxValue = mMaxTime
mRangeSeekBarView?.setStartEndTime(mMinTime,mMaxTime)
mRangeSeekBarView?.isNotifyWhileDragging = true
mRangeSeekBarView?.setOnRangeSeekBarChangeListener(object :RangeSeekBarView.OnRangeSeekBarChangeListener{
override fun onRangeSeekBarValuesChanged(
bar: RangeSeekBarView?,
minValue: Long,
maxValue: Long,
action: Int,
isMin: Boolean,
pressedThumb: RangeSeekBarView.Thumb?
) {
Log.d("yanjin", "$TAG mMinTime = $minValue mMaxTime = $maxValue")
mMinTime = minValue + (mFirstPosition * 1000)
mMaxTime = maxValue + (mFirstPosition * 1000)
mRangeSeekBarView?.setStartEndTime(mMinTime, mMaxTime)
reStartVideo()
}
})
}
/**
* 解析視頻
*/
private fun analysisVideo() {
//先獲取多少幀
mFrames = mVideoView?.duration!! / 1000
Log.d("yanjin", "$TAG mFrames = $mFrames")
//設(shè)定這個(gè)大小的List,String代表圖片路徑,目前先定義一個(gè)這么大的集合,圖片還沒解析就先不放。
if (!File(outDir).exists()) {
File(outDir).mkdirs()
}
//平湊解析的命令
gotoGetFrameAtTime(0)
}
/**
* 獲取畫面幀
*/
private fun gotoGetFrameAtTime(time: Int) {
if (time >= mFrames) {
return//如果超過了就返回,不要截取了
}
var outfile = outDir + File.separator + "${time}.jpg"
val cmd =
"ffmpeg -ss " + time + " -i " + resouce?.path + " -preset " + "ultrafast" + " -frames:v 1 -f image2 -s " + mWidth + "x" + mHeight + " -y " + outfile
val commands = cmd.split(" ").toTypedArray()
var nextTime = time + 1
var subscribe: Flowable<RxFFmpegProgress> = RxFFmpegInvoke.getInstance()
.runCommandRxJava(commands)
mCurrentSubscriber = object : MyRxFFmpegSubscriber() {
override fun onFinish() {
Log.d("yanjin", "$TAG 完成 time = ${time}")
if (time == 0) {
//第一次,那么全部圖片用第一幀畫面
for (x in 0 until mFrames) {
list.add(outfile)
}
mAdapter.updateList(list)
} else {
//找到對應(yīng)得條目修改
list.set(time, outfile)
mAdapter.updateItem(time, outfile)
}
gotoGetFrameAtTime(nextTime)
}
}
subscribe.subscribe(mCurrentSubscriber)
}
/**
* 重新把視頻重頭到位播一遍
*/
private fun reStartVideo() {
try {
if(mp != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
//VideoView.seekTo是有可能不在我們想要的那個(gè)時(shí)間播放的,因?yàn)槲覀兡莻€(gè)時(shí)間可能不是關(guān)鍵幀,所以為了解決
//我們用MediaPlayer.SEEK_CLOSEST,但是這個(gè)方法只能在6.0以上
mp?.seekTo(mMinTime,MediaPlayer.SEEK_CLOSEST)
}else{
mVideoView.seekTo(mMinTime.toInt())
}
}catch (e:Exception){
e.printStackTrace()
}
}
/**
* 每隔1s獲取一下視頻當(dāng)前播放位置
*/
fun getVideoProgress() {
try {
val currentPosition = mVideoView?.currentPosition
Log.d("yanjin","currentPosition = $currentPosition mMaxTime = $mMaxTime")
if(currentPosition!! >= mMaxTime){
//如果當(dāng)前時(shí)間已經(jīng)超過我們選取的最大播放位置,那么我們從頭播放。
reStartVideo()
}
}catch (e:Exception){
e.printStackTrace()
}
}
private fun trimVideo() {
loadingDialog = DialogUtiles.showLoading(this)
if(mCurrentSubscriber != null && !mCurrentSubscriber?.isDisposed!!){
mCurrentSubscriber?.dispose()
}
var outDir = mCacheRootPath
if (!File(outDir).exists()) {
File(outDir).mkdirs()
}
var outfile = mCacheRootPath + "${Utils.getFileName(resouce?.name)}_trim.mp4"
var start:Float = mMinTime/1000f
var end:Float = mMaxTime/1000f
var cmd = "ffmpeg -ss " + start + " -to " + end + " -accurate_seek" + " -i " + resouce?.path + " -to " + (end - start) + " -preset " + "superfast" + " -crf 23 -c:a copy -avoid_negative_ts 0 -y " + outfile;
val commands = cmd.split(" ").toTypedArray()
try {
RxFFmpegInvoke.getInstance()
.runCommandRxJava(commands)
.subscribe(object : MyRxFFmpegSubscriber(){
override fun onFinish() {
if(loadingDialog != null && loadingDialog?.isShowing!!){
loadingDialog?.dismiss()
}
finish()
Log.d("yanjin", "$TAG 完成截取 outfile = ${outfile}")
}
override fun onProgress(progress: Int, progressTime: Long) {
Log.d("yanjin", "$TAG 截取進(jìn)度 progress = ${progress}")
}
})
}catch (e:Exception){
e.printStackTrace()
}
}
override fun onDestroy() {
super.onDestroy()
RxFFmpegInvoke.getInstance().exit()
if(mCurrentSubscriber != null && !mCurrentSubscriber?.isDisposed!!){
mCurrentSubscriber?.dispose()
}
if(timer != null){
timer?.cancel()
timer = null
}
if(timerTaskImp != null){
timerTaskImp?.cancel()
timerTaskImp = null
}
ThreadPoolManager.getInstance().executeTask {
//刪除解析出來的圖片
val files: Array<File> = File(outDir).listFiles()
for (i in files.indices) {
if(files[i].exists()){
files[i].delete()
}
}
}
}
companion object {
public const val PATH = "path"
public var TAG = VideoCutActivity::class.java.name
public const val MAX_TIME = 10;//最大截取10s,最多展示10幀
}
}
這里的主要想法就是,先初始化videoview播放視頻,當(dāng)視頻播放準(zhǔn)備完畢,再解析視頻獲取每一幀,因?yàn)橐曨l準(zhǔn)備完畢我們才能拿到視頻一共有多長時(shí)間,時(shí)長除以1000就是多少幀,然后就能調(diào)用ffmpeg命令獲取這一秒的畫面幀了。
為什么這么做呢?是因?yàn)槲覄傞_始是直接上來就用ffmpeg代碼每隔1s截取一張畫面幀,但是會(huì)發(fā)現(xiàn),比如一個(gè)視頻30s,截取出來的圖片有32張,命令如下:
ffmpeg -y -i /storage/emulated/0/1/input.mp4 -f image2 -r 1 -q:v 10 -preset superfast /storage/emulated/0/1/%3d.jpg
為什么會(huì)多兩張搞不懂,有知道的小朋友私信告訴我一下。
RangeSeekBarView的代碼
public class RangeSeekBarView extends View {
private static final String TAG = RangeSeekBarView.class.getSimpleName();
public static final int INVALID_POINTER_ID = 255;
public static final int ACTION_POINTER_INDEX_MASK = 0x0000ff00, ACTION_POINTER_INDEX_SHIFT = 8;
private static final int TextPositionY = Utils.dp2px(7);
private static final int paddingTop = Utils.dp2px(10);
private int mActivePointerId = INVALID_POINTER_ID;
private long mMinShootTime = 3*1000;//最小剪輯3s,默認(rèn)
private double absoluteMinValuePrim, absoluteMaxValuePrim;
private double normalizedMinValue = 0d;//點(diǎn)坐標(biāo)占總長度的比例值,范圍從0-1
private double normalizedMaxValue = 1d;//點(diǎn)坐標(biāo)占總長度的比例值,范圍從0-1
private double normalizedMinValueTime = 0d;
private double normalizedMaxValueTime = 1d;// normalized:規(guī)格化的--點(diǎn)坐標(biāo)占總長度的比例值,范圍從0-1
private int mScaledTouchSlop;
private Bitmap thumbImageLeft;
private Bitmap thumbImageRight;
private Bitmap thumbPressedImage;
private Paint paint;
private Paint rectPaint;
private final Paint mVideoTrimTimePaintL = new Paint();
private final Paint mVideoTrimTimePaintR = new Paint();
private final Paint mShadow = new Paint();
private int thumbWidth;
private float thumbHalfWidth;
private final float padding = 0;
private long mStartPosition = 0;
private long mEndPosition = 0;
private float thumbPaddingTop = 0;
private boolean isTouchDown;
private float mDownMotionX;
private boolean mIsDragging;
private Thumb pressedThumb;
private boolean isMin;
private double min_width = 1;//最小裁剪距離
private boolean notifyWhileDragging = false;
private OnRangeSeekBarChangeListener mRangeSeekBarChangeListener;
private int whiteColorRes = getContext().getResources().getColor(R.color.white);
public enum Thumb {
MIN, MAX
}
public RangeSeekBarView(Context context) {
this(context,null);
}
public RangeSeekBarView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public RangeSeekBarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.absoluteMinValuePrim = 0*1000;
this.absoluteMaxValuePrim = VideoCutActivity.MAX_TIME *1000;
setFocusable(true);
setFocusableInTouchMode(true);
init();
}
private void init() {
// mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
thumbImageLeft = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_video_thumb_handle);
int width = thumbImageLeft.getWidth();
int height = thumbImageLeft.getHeight();
int newWidth = Utils.dp2px(12.5f);
int newHeight = Utils.dp2px(50f);
float scaleWidth = newWidth * 1.0f / width;
float scaleHeight = newHeight * 1.0f / height;
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
thumbImageLeft = Bitmap.createBitmap(thumbImageLeft, 0, 0, width, height, matrix, true);
thumbImageRight = thumbImageLeft;
thumbPressedImage = thumbImageLeft;
thumbWidth = newWidth;
thumbHalfWidth = thumbWidth / 2f;
int shadowColor = getContext().getResources().getColor(R.color.shadow_color);
mShadow.setAntiAlias(true);
mShadow.setColor(shadowColor);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
rectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
rectPaint.setStyle(Paint.Style.FILL);
rectPaint.setColor(whiteColorRes);
mVideoTrimTimePaintL.setStrokeWidth(3);
mVideoTrimTimePaintL.setARGB(255, 51, 51, 51);
mVideoTrimTimePaintL.setTextSize(28);
mVideoTrimTimePaintL.setAntiAlias(true);
mVideoTrimTimePaintL.setColor(whiteColorRes);
mVideoTrimTimePaintL.setTextAlign(Paint.Align.LEFT);
mVideoTrimTimePaintR.setStrokeWidth(3);
mVideoTrimTimePaintR.setARGB(255, 51, 51, 51);
mVideoTrimTimePaintR.setTextSize(28);
mVideoTrimTimePaintR.setAntiAlias(true);
mVideoTrimTimePaintR.setColor(whiteColorRes);
mVideoTrimTimePaintR.setTextAlign(Paint.Align.RIGHT);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = 300;
if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(widthMeasureSpec)) {
width = MeasureSpec.getSize(widthMeasureSpec);
}
int height = 120;
if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(heightMeasureSpec)) {
height = MeasureSpec.getSize(heightMeasureSpec);
}
setMeasuredDimension(width, height);
}
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float bg_middle_left = 0;
float bg_middle_right = getWidth() - getPaddingRight();
float rangeL = normalizedToScreen(normalizedMinValue);
float rangeR = normalizedToScreen(normalizedMaxValue);
Rect leftRect = new Rect((int) bg_middle_left, getHeight(), (int) rangeL, 0);
Rect rightRect = new Rect((int) rangeR, getHeight(), (int) bg_middle_right, 0);
canvas.drawRect(leftRect, mShadow);
canvas.drawRect(rightRect, mShadow);
//上邊框
canvas.drawRect(rangeL + thumbHalfWidth, thumbPaddingTop + paddingTop, rangeR - thumbHalfWidth, thumbPaddingTop + Utils.dp2px(2) + paddingTop, rectPaint);
//下邊框
canvas.drawRect(rangeL + thumbHalfWidth, getHeight() - Utils.dp2px(2), rangeR - thumbHalfWidth, getHeight(), rectPaint);
//畫左邊thumb
drawThumb(normalizedToScreen(normalizedMinValue), false, canvas, true);
//畫右thumb
drawThumb(normalizedToScreen(normalizedMaxValue), false, canvas, false);
//繪制文字
drawVideoTrimTimeText(canvas);
}
private void drawThumb(float screenCoord, boolean pressed, Canvas canvas, boolean isLeft) {
canvas.drawBitmap(pressed ? thumbPressedImage : (isLeft ? thumbImageLeft : thumbImageRight), screenCoord - (isLeft ? 0 : thumbWidth), paddingTop, paint);
}
private void drawVideoTrimTimeText(Canvas canvas) {
String leftThumbsTime = Utils.convertSecondsToTime(mStartPosition);
String rightThumbsTime = Utils.convertSecondsToTime(mEndPosition);
canvas.drawText(leftThumbsTime, normalizedToScreen(normalizedMinValue), TextPositionY, mVideoTrimTimePaintL);
canvas.drawText(rightThumbsTime, normalizedToScreen(normalizedMaxValue), TextPositionY, mVideoTrimTimePaintR);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isTouchDown) {
return super.onTouchEvent(event);
}
if (event.getPointerCount() > 1) {
return super.onTouchEvent(event);
}
if (!isEnabled()) return false;
if (absoluteMaxValuePrim <= mMinShootTime) {
return super.onTouchEvent(event);
}
int pointerIndex;// 記錄點(diǎn)擊點(diǎn)的index
final int action = event.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
//記住最后一個(gè)手指點(diǎn)擊屏幕的點(diǎn)的坐標(biāo)x,mDownMotionX
mActivePointerId = event.getPointerId(event.getPointerCount() - 1);
pointerIndex = event.findPointerIndex(mActivePointerId);
mDownMotionX = event.getX(pointerIndex);
// 判斷touch到的是最大值thumb還是最小值thumb
pressedThumb = evalPressedThumb(mDownMotionX);
if (pressedThumb == null) return super.onTouchEvent(event);
setPressed(true);// 設(shè)置該控件被按下了
onStartTrackingTouch();// 置mIsDragging為true,開始追蹤touch事件
trackTouchEvent(event);
attemptClaimDrag();
if (mRangeSeekBarChangeListener != null) {
mRangeSeekBarChangeListener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue(), MotionEvent.ACTION_DOWN, isMin, pressedThumb);
}
break;
case MotionEvent.ACTION_MOVE:
if (pressedThumb != null) {
if (mIsDragging) {
trackTouchEvent(event);
} else {
// Scroll to follow the motion event
pointerIndex = event.findPointerIndex(mActivePointerId);
final float x = event.getX(pointerIndex);// 手指在控件上點(diǎn)的X坐標(biāo)
// 手指沒有點(diǎn)在最大最小值上,并且在控件上有滑動(dòng)事件
if (Math.abs(x - mDownMotionX) > mScaledTouchSlop) {
setPressed(true);
Log.e(TAG, "沒有拖住最大最小值");// 一直不會(huì)執(zhí)行?
invalidate();
onStartTrackingTouch();
trackTouchEvent(event);
attemptClaimDrag();
}
}
if (notifyWhileDragging && mRangeSeekBarChangeListener != null) {
mRangeSeekBarChangeListener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue(), MotionEvent.ACTION_MOVE, isMin, pressedThumb);
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsDragging) {
trackTouchEvent(event);
onStopTrackingTouch();
setPressed(false);
} else {
onStartTrackingTouch();
trackTouchEvent(event);
onStopTrackingTouch();
}
invalidate();
if (mRangeSeekBarChangeListener != null) {
mRangeSeekBarChangeListener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue(), MotionEvent.ACTION_UP, isMin,
pressedThumb);
}
pressedThumb = null;// 手指抬起,則置被touch到的thumb為空
break;
case MotionEvent.ACTION_POINTER_DOWN:
final int index = event.getPointerCount() - 1;
// final int index = ev.getActionIndex();
mDownMotionX = event.getX(index);
mActivePointerId = event.getPointerId(index);
invalidate();
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(event);
invalidate();
break;
case MotionEvent.ACTION_CANCEL:
if (mIsDragging) {
onStopTrackingTouch();
setPressed(false);
}
invalidate(); // see above explanation
break;
default:
break;
}
return true;
}
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = (ev.getAction() & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mDownMotionX = ev.getX(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
}
private void trackTouchEvent(MotionEvent event) {
if (event.getPointerCount() > 1) return;
Log.e(TAG, "trackTouchEvent: " + event.getAction() + " x: " + event.getX());
final int pointerIndex = event.findPointerIndex(mActivePointerId);// 得到按下點(diǎn)的index
float x = 0;
try {
x = event.getX(pointerIndex);
} catch (Exception e) {
return;
}
if (Thumb.MIN.equals(pressedThumb)) {
// screenToNormalized(x)-->得到規(guī)格化的0-1的值
setNormalizedMinValue(screenToNormalized(x, 0));
} else if (Thumb.MAX.equals(pressedThumb)) {
setNormalizedMaxValue(screenToNormalized(x, 1));
}
}
private double screenToNormalized(float screenCoord, int position) {
int width = getWidth();
if (width <= 2 * padding) {
// prevent division by zero, simply return 0.
return 0d;
} else {
isMin = false;
double current_width = screenCoord;
float rangeL = normalizedToScreen(normalizedMinValue);
float rangeR = normalizedToScreen(normalizedMaxValue);
double min = mMinShootTime / (absoluteMaxValuePrim - absoluteMinValuePrim) * (width - thumbWidth * 2);
if (absoluteMaxValuePrim > 5 * 60 * 1000) {//大于5分鐘的精確小數(shù)四位
DecimalFormat df = new DecimalFormat("0.0000");
min_width = Double.parseDouble(df.format(min));
} else {
min_width = Math.round(min + 0.5d);
}
if (position == 0) {
if (isInThumbRangeLeft(screenCoord, normalizedMinValue, 0.5)) {
return normalizedMinValue;
}
float rightPosition = (getWidth() - rangeR) >= 0 ? (getWidth() - rangeR) : 0;
double left_length = getValueLength() - (rightPosition + min_width);
if (current_width > rangeL) {
current_width = rangeL + (current_width - rangeL);
} else if (current_width <= rangeL) {
current_width = rangeL - (rangeL - current_width);
}
if (current_width > left_length) {
isMin = true;
current_width = left_length;
}
if (current_width < thumbWidth * 2 / 3) {
current_width = 0;
}
double resultTime = (current_width - padding) / (width - 2 * thumbWidth);
normalizedMinValueTime = Math.min(1d, Math.max(0d, resultTime));
double result = (current_width - padding) / (width - 2 * padding);
return Math.min(1d, Math.max(0d, result));// 保證該該值為0-1之間,但是什么時(shí)候這個(gè)判斷有用呢?
} else {
if (isInThumbRange(screenCoord, normalizedMaxValue, 0.5)) {
return normalizedMaxValue;
}
double right_length = getValueLength() - (rangeL + min_width);
if (current_width > rangeR) {
current_width = rangeR + (current_width - rangeR);
} else if (current_width <= rangeR) {
current_width = rangeR - (rangeR - current_width);
}
double paddingRight = getWidth() - current_width;
if (paddingRight > right_length) {
isMin = true;
current_width = getWidth() - right_length;
paddingRight = right_length;
}
if (paddingRight < thumbWidth * 2 / 3) {
current_width = getWidth();
paddingRight = 0;
}
double resultTime = (paddingRight - padding) / (width - 2 * thumbWidth);
resultTime = 1 - resultTime;
normalizedMaxValueTime = Math.min(1d, Math.max(0d, resultTime));
double result = (current_width - padding) / (width - 2 * padding);
return Math.min(1d, Math.max(0d, result));// 保證該該值為0-1之間,但是什么時(shí)候這個(gè)判斷有用呢?
}
}
}
private int getValueLength() {
return (getWidth() - 2 * thumbWidth);
}
/**
* 計(jì)算位于哪個(gè)Thumb內(nèi)
*
* @param touchX touchX
* @return 被touch的是空還是最大值或最小值
*/
private Thumb evalPressedThumb(float touchX) {
Thumb result = null;
boolean minThumbPressed = isInThumbRange(touchX, normalizedMinValue, 2);// 觸摸點(diǎn)是否在最小值圖片范圍內(nèi)
boolean maxThumbPressed = isInThumbRange(touchX, normalizedMaxValue, 2);
if (minThumbPressed && maxThumbPressed) {
// 如果兩個(gè)thumbs重疊在一起,無法判斷拖動(dòng)哪個(gè),做以下處理
// 觸摸點(diǎn)在屏幕右側(cè),則判斷為touch到了最小值thumb,反之判斷為touch到了最大值thumb
result = (touchX / getWidth() > 0.5f) ? Thumb.MIN : Thumb.MAX;
} else if (minThumbPressed) {
result = Thumb.MIN;
} else if (maxThumbPressed) {
result = Thumb.MAX;
}
return result;
}
private boolean isInThumbRange(float touchX, double normalizedThumbValue, double scale) {
// 當(dāng)前觸摸點(diǎn)X坐標(biāo)-最小值圖片中心點(diǎn)在屏幕的X坐標(biāo)之差<=最小點(diǎn)圖片的寬度的一般
// 即判斷觸摸點(diǎn)是否在以最小值圖片中心為原點(diǎn),寬度一半為半徑的圓內(nèi)。
return Math.abs(touchX - normalizedToScreen(normalizedThumbValue)) <= thumbHalfWidth * scale;
}
private boolean isInThumbRangeLeft(float touchX, double normalizedThumbValue, double scale) {
// 當(dāng)前觸摸點(diǎn)X坐標(biāo)-最小值圖片中心點(diǎn)在屏幕的X坐標(biāo)之差<=最小點(diǎn)圖片的寬度的一般
// 即判斷觸摸點(diǎn)是否在以最小值圖片中心為原點(diǎn),寬度一半為半徑的圓內(nèi)。
return Math.abs(touchX - normalizedToScreen(normalizedThumbValue) - thumbWidth) <= thumbHalfWidth * scale;
}
/**
* 試圖告訴父view不要攔截子控件的drag
*/
private void attemptClaimDrag() {
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
void onStartTrackingTouch() {
mIsDragging = true;
}
void onStopTrackingTouch() {
mIsDragging = false;
}
public void setMinShootTime(long min_cut_time) {
this.mMinShootTime = min_cut_time;
}
private float normalizedToScreen(double normalizedCoord) {
return (float) (getPaddingLeft() + normalizedCoord * (getWidth() - getPaddingLeft() - getPaddingRight()));
}
private double valueToNormalized(long value) {
if (0 == absoluteMaxValuePrim - absoluteMinValuePrim) {
return 0d;
}
return (value - absoluteMinValuePrim) / (absoluteMaxValuePrim - absoluteMinValuePrim);
}
public void setStartEndTime(long start, long end) {
this.mStartPosition = start / 1000;
this.mEndPosition = end / 1000;
}
public void setSelectedMinValue(long value) {
if (0 == (absoluteMaxValuePrim - absoluteMinValuePrim)) {
setNormalizedMinValue(0d);
} else {
setNormalizedMinValue(valueToNormalized(value));
}
}
public void setSelectedMaxValue(long value) {
if (0 == (absoluteMaxValuePrim - absoluteMinValuePrim)) {
setNormalizedMaxValue(1d);
} else {
setNormalizedMaxValue(valueToNormalized(value));
}
}
public void setNormalizedMinValue(double value) {
normalizedMinValue = Math.max(0d, Math.min(1d, Math.min(value, normalizedMaxValue)));
invalidate();// 重新繪制此view
}
public void setNormalizedMaxValue(double value) {
normalizedMaxValue = Math.max(0d, Math.min(1d, Math.max(value, normalizedMinValue)));
invalidate();// 重新繪制此view
}
public long getSelectedMinValue() {
return normalizedToValue(normalizedMinValueTime);
}
public long getSelectedMaxValue() {
return normalizedToValue(normalizedMaxValueTime);
}
private long normalizedToValue(double normalized) {
return (long) (absoluteMinValuePrim + normalized * (absoluteMaxValuePrim - absoluteMinValuePrim));
}
/**
* 供外部activity調(diào)用,控制是都在拖動(dòng)的時(shí)候打印log信息,默認(rèn)是false不打印
*/
public boolean isNotifyWhileDragging() {
return notifyWhileDragging;
}
public void setNotifyWhileDragging(boolean flag) {
this.notifyWhileDragging = flag;
}
public void setTouchDown(boolean touchDown) {
isTouchDown = touchDown;
}
@Override
protected Parcelable onSaveInstanceState() {
final Bundle bundle = new Bundle();
bundle.putParcelable("SUPER", super.onSaveInstanceState());
bundle.putDouble("MIN", normalizedMinValue);
bundle.putDouble("MAX", normalizedMaxValue);
bundle.putDouble("MIN_TIME", normalizedMinValueTime);
bundle.putDouble("MAX_TIME", normalizedMaxValueTime);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable parcel) {
final Bundle bundle = (Bundle) parcel;
super.onRestoreInstanceState(bundle.getParcelable("SUPER"));
normalizedMinValue = bundle.getDouble("MIN");
normalizedMaxValue = bundle.getDouble("MAX");
normalizedMinValueTime = bundle.getDouble("MIN_TIME");
normalizedMaxValueTime = bundle.getDouble("MAX_TIME");
}
public interface OnRangeSeekBarChangeListener {
void onRangeSeekBarValuesChanged(RangeSeekBarView bar, long minValue, long maxValue, int action, boolean isMin, Thumb pressedThumb);
}
public void setOnRangeSeekBarChangeListener(OnRangeSeekBarChangeListener listener) {
this.mRangeSeekBarChangeListener = listener;
}
}
用到的圖片

用到的顏色
<color name="shadow_color">#7F000000</color>
DialogUtiles代碼如下:
public class DialogUtiles {
public static AlertDialog showLoading(Activity context){
try {
if (context == null || context.isFinishing()) {
return null;
}
final AlertDialog dlg = new AlertDialog.Builder(context, R.style.dialog_no_sullscreen_no_title).show();
dlg.setCanceledOnTouchOutside(false);
Window window = dlg.getWindow();
window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
window.setContentView(R.layout.loading_dialog);
WindowManager.LayoutParams lp = dlg.getWindow().getAttributes();
//這里設(shè)置居中
lp.gravity = Gravity.CENTER;
window.setAttributes(lp);
return dlg;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
}
TimerTaskImp
public class TimerTaskImp extends TimerTask {
private WeakReference<VideoCutActivity> weakReference;
public TimerTaskImp(VideoCutActivity activity){
weakReference = new WeakReference<>(activity);
}
@Override
public void run() {
if(weakReference != null && weakReference.get() != null){
weakReference.get().getVideoProgress();
}
}
}
Utils代碼
public class Utils {
public static int dp2px(float dpValue){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dpValue, App.getInstance().getResources().getDisplayMetrics());
}
/**
* second to HH:MM:ss
* @param seconds
* @return
*/
public static String convertSecondsToTime(long seconds) {
String timeStr = null;
int hour = 0;
int minute = 0;
int second = 0;
if (seconds <= 0)
return "00:00";
else {
minute = (int)seconds / 60;
if (minute < 60) {
second = (int)seconds % 60;
timeStr = unitFormat(minute) + ":" + unitFormat(second);
} else {
hour = minute / 60;
if (hour > 99)
return "99:59:59";
minute = minute % 60;
second = (int)(seconds - hour * 3600 - minute * 60);
timeStr = unitFormat(hour) + ":" + unitFormat(minute) + ":" + unitFormat(second);
}
}
return timeStr;
}
private static String unitFormat(int i) {
String retStr = null;
if (i >= 0 && i < 10)
retStr = "0" + Integer.toString(i);
else
retStr = "" + i;
return retStr;
}
public static String getFileName(String sting){
String[] split = sting.split("\\.");
if(split.length > 0){
return split[0];
}
return sting;
}
}
ThreadPoolManager代碼
public class ThreadPoolManager {
private ExecutorService service;
private final SerialExecutor serialExecutor;
private static final ThreadPoolManager manager = new ThreadPoolManager();
private ThreadPoolManager() {
int num = Runtime.getRuntime().availableProcessors();
service = Executors.newFixedThreadPool(num);
serialExecutor = new SerialExecutor(service);
}
public static ThreadPoolManager getInstance() {
return manager;
}
/**
* 順序執(zhí)行一個(gè)任務(wù)
*
* @param runnable 任務(wù)
* @param isSequentialExecution 是否順序執(zhí)行
*/
public void executeTask(Runnable runnable, boolean isSequentialExecution) {
if (isSequentialExecution) {
serialExecutor.execute(runnable);
} else {
service.execute(runnable);
}
}
/**
* 執(zhí)行一個(gè)任務(wù)
*
* @param runnable
*/
public void executeTask(Runnable runnable) {
executeTask(runnable, false);
}
public void executeTasks(ArrayList<Runnable> list, boolean isSequentialExecution) {
for (Runnable runnable : list) {
executeTask(runnable, isSequentialExecution);
}
}
/**
* 執(zhí)行AsyncTask
*
* @param task
*/
@SuppressLint("NewApi")
@SuppressWarnings("unchecked")
public void execAsync(AsyncTask<?, ?, ?> task) {
if (Build.VERSION.SDK_INT >= 11) {
//task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
task.executeOnExecutor(Executors.newCachedThreadPool());
} else {
task.execute();
}
}
}
SerialExecutor代碼
public class SerialExecutor implements Executor {
final Queue<Runnable> tasks = new ArrayDeque<Runnable>();
final Executor executor;
Runnable active;
SerialExecutor(Executor executor) {
this.executor = executor;
}
@Override
public synchronized void execute(final Runnable r) {
tasks.offer(new Runnable() {
@Override
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (active == null) {
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((active = tasks.poll()) != null) {
executor.execute(active);
}
}
}
補(bǔ)充一下,看來有同學(xué)和我一樣有寫這個(gè)功能的需求,把漏掉的類加上:
首先就是FramesAdapter,他就是將解析出來的幀水平放好,只是我們這里的圖片寬度要做動(dòng)態(tài)修改,初始狀態(tài)下,我們的裁剪框裝10張圖片。所以拿裁剪框的寬度除以10,就是每一張圖片的寬度
public class FramesAdapter extends RecyclerView.Adapter<FramesAdapter.ViewHolder> {
private List<String> list = new ArrayList<>();
private int mWidth = Utils.dp2px(35f);
public FramesAdapter(){
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.frames_item_layout,parent,false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Glide.with(holder.mIv.getContext()).load(list.get(position)).into(holder.mIv);
ViewGroup.LayoutParams layoutParams = holder.mIv.getLayoutParams();
layoutParams.width = mWidth;
holder.mIv.setLayoutParams(layoutParams);
}
@Override
public int getItemCount() {
return list.size();
}
public void updateList(@NotNull List<String> list) {
this.list.clear();
this.list.addAll(list);
notifyDataSetChanged();
}
public void updateItem(int position, @NotNull String outfile) {
this.list.set(position,outfile);
notifyItemChanged(position);
}
public void setItemWidth(int mWidth) {
this.mWidth = mWidth;
}
public class ViewHolder extends RecyclerView.ViewHolder{
private final ImageView mIv;
public ViewHolder(@NonNull View itemView) {
super(itemView);
mIv = itemView.findViewById(R.id.mIv);
}
}
}
item的布局frames_item_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/mIv"
android:layout_width="35dp"
android:layout_height="50dp"
android:layout_centerInParent="true"/>
</RelativeLayout>
還有個(gè)MyRxFFmpegSubscriber其實(shí)就是嫌棄這個(gè)RxFfmpeg的回調(diào)方法太多,繼承了他,這樣就不用全部方法都要寫一遍了。
public class MyRxFFmpegSubscriber extends RxFFmpegSubscriber {
@Override
public void onFinish() {
}
@Override
public void onProgress(int progress, long progressTime) {
}
@Override
public void onCancel() {
}
@Override
public void onError(String message) {
}
}
最后說一句,這個(gè)案例用的ffmpeg解析特別的慢,包體也大,用于項(xiàng)目肯定不行,如果要用ffmpeg的話找找其他的開源,這里吐槽一句ffmpeg解析完后退出界面,一定要記得刪除圖片哦,當(dāng)然還可以用Android自帶的MediaCode,他解析不需要存圖片在文件夾里面,拿著就能用。主頁有相應(yīng)文章介紹。