先放预览图
本文主要实现的是按住按钮,开始录音同时滚动条滚动,视频同时播放,当松开按钮滚动条停止录音停止,将刚才录音的区域显示在滚动条上
首先整理思路,长按事件,因为setOnLongClickListener不能得到松开的时间 所以重写系统OnTouchListener是最好的办法。
先放代码
import android.app.Activity;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
/**
* 提供View的单击和长按事件的判断回调
*/
public class TouchUtils {
private static String TAG = "TouchUtils";
private static long DELAYED_TIME = 20;
public static void setTouchEventListener(final View view, final OnTouchEventListener onTouchEventListener) {
setTouchEventListener(view, DELAYED_TIME, onTouchEventListener);
}
/**
* 注册touch事件
*/
public static void setTouchEventListener(final View view, final long delayedTime, final OnTouchEventListener onTouchEventListener) {
final Handler uiHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (view.getContext() instanceof Activity) {
Activity activity = (Activity) view.getContext();
if (activity.isFinishing()) {
removeCallbacksAndMessages(null);
}else {
onTouchEventListener.onLongTouch(view);
sendEmptyMessageDelayed(1, delayedTime);
}
} else {
throw new RuntimeException("必须绑定Activity");
}
Log.i(TAG, "onLongTouch: ");
}
};
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
uiHandler.removeCallbacksAndMessages(null);
String touchTag = (String) view.getTag(Integer.MAX_VALUE - 1);
if (touchTag == null) {
Log.i(TAG, "onSingleTouch: ");
onTouchEventListener.onSingleTouch(view);
}
onTouchEventListener.onTouchEnd(view);
Log.i(TAG, "onTouchEnd: ");
}
});
/* 下面的不用管,值关注 OnclickListener 就可以了 */
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
view.setTag(Integer.MAX_VALUE - 1, null);
onTouchEventListener.onTouchStart(view);
Log.i(TAG, "onTouchStart: ");
} else if (event.getAction() == MotionEvent.ACTION_UP) {
uiHandler.removeCallbacksAndMessages(null);
} else if (event.getAction() == MotionEvent.ACTION_CANCEL) {
uiHandler.removeCallbacksAndMessages(null);
}
return false;
}
});
view.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
view.setTag(Integer.MAX_VALUE - 1, "onLongTouch");
uiHandler.sendEmptyMessage(1);
return false;
}
});
}
public interface OnTouchEventListener {
void onTouchStart(View view);//触发按动
void onSingleTouch(View view);//单次点击
void onLongTouch(View view);//长按
void onTouchEnd(View view);//触摸事件结束
}
}
这个类使用Handler实现了长按时间的监听,拿到长按的监听,接下来需要做的就是滚动条滚动
接下来要实现的就是滚动条和蓝色覆盖了
首先分析一下组成
1、整个滚动条分成了2个部分
第一部分是RecyclerView实现的视频每一帧的图
第二部分是蓝色覆盖条
我们把两部分加在一起封装成一个DubScrollView
import android.content.Context;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
import com.wanyueliang.avm.R;
import com.wanyueliang.avm.config.AppFilmQuickConfig;
import com.wanyueliang.avm.model.databean.DubBean;
import com.wanyueliang.avm.ui.create.editor.dub.adapter.DubAdapter;
import com.wanyueliang.avm.utils.click_util.onFastDisableClickListener;
import com.wanyueliang.avm.utils.log.AppLog;
import com.wanyueliang.avm.widget.slider.DoubleSliderView;
import com.wanyueliang.avm.widget.slider.scroll_slider.MaterialBean;
import com.wanyueliang.avm.widget.slider.scroll_slider.RangeDoubleSliderView;
import com.wanyueliang.avm.widget.textview.TimeTextView;
import org.greenrobot.greendao.annotation.NotNull;
import java.util.List;
public class DubScrollView extends FrameLayout {
private final String TAG = getClass().getSimpleName();
private Context mContext;
/*View*/
private TimeTextView mTvCurrentTime;
private RecyclerView mRvMaterial;
private Button mBtnDelete;
private DubMarkView mDmvMarkView;
/*data*/
private float mViewWidth;//整体的宽度
private float editViewHeight;//核心数据----编辑的View的高度,recyclerView和DoubleSliderView
private float relativeDuration = 5f;//核心数据----每一份editViewHeight对应的时间
private float mTotalTime;//核心数据----总时间
private float precisionValue = 1000f;//核心数据----精度
private RangeDoubleSliderView.Builder builder;
private List<DubBean> mEditBeans;//阴影相关
private DubBean mShowEditBean;
private float sliderWidth;//滑块的宽度的高度,默认为DoubleSliderView高度的三分之一 editViewHeight/3f
private float scrollCurrentTime;//当前中心轴对应的时间
private int mOffsetX;//记录RecyclerView总偏移量
private DubAdapter materialAdapter;
private List<MaterialBean> mData;
private View headView;
private View footerView;
private boolean mLimitValue;//是否有限制
private boolean isRecording;//是否正在录制配音
private float startTime;
public DubScrollView(Context context) {
this(context, null);
}
public DubScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DubScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mContext = getContext();
LayoutInflater.from(mContext).inflate(R.layout.view_dub_scroll_view_layout, this, true);
mTvCurrentTime = (TimeTextView) findViewById(R.id.tv_current_time);
mRvMaterial = (RecyclerView) findViewById(R.id.rv_material);
mRvMaterial = (RecyclerView) findViewById(R.id.rv_material);
mTvCurrentTime = (TimeTextView) findViewById(R.id.tv_current_time);
mBtnDelete = (Button) findViewById(R.id.btn_delete);
mDmvMarkView = (DubMarkView) findViewById(R.id.dmv_mark_view);
mDmvMarkView.hideSlider();
}
/*底层素材的数据*/
public void setMaterialData(List<MaterialBean> materialBeans, float totalTime) {
this.mData = materialBeans;
this.mTotalTime = totalTime;
if (materialAdapter != null) {
materialAdapter.setData(mData);
materialAdapter.notifyDataSetChanged();
}
}
/*添加的素材区域数据*/
public void setEditData(List<DubBean> editBeans) {
this.mEditBeans = editBeans;
if (mDmvMarkView != null) {
mDmvMarkView.setEditBeans(mEditBeans);
mDmvMarkView.notifyDataChange();
}
//第一次进入,先匹配
//检查是否有匹配到的区间
checkMatching();
}
/*更新添加的素材区域数据*/
public void notifyDataSetChange() {
if (mDmvMarkView != null) {
mDmvMarkView.notifyDataChange();
}
//第一次进入,先匹配
//检查是否有匹配到的区间
checkMatching();
}
public boolean isRecording() {
return isRecording;
}
public void setRecordingAndStartTime(boolean recording, float startTime) {
isRecording = recording;
this.startTime = startTime;
}
public void setBtnDeleteVisibility(int visibility) {
mBtnDelete.setVisibility(visibility);
}
public void setBtnDeleteOnClickListener(onFastDisableClickListener onClickListener) {
mBtnDelete.setOnClickListener(onClickListener);
}
/**
* 界面生成,初始化数据
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w > 0 && h > 0) {
mViewWidth = w;
editViewHeight = mRvMaterial.getMeasuredHeight();
Log.i(TAG, "onSizeChanged: editViewHeight=" + editViewHeight);
initData();
initListener();
}
}
private void initData() {
mEditBeans = AppFilmQuickConfig.getFilmDub();
materialAdapter = new DubAdapter(mContext, relativeDuration, editViewHeight);//设置底层的素材预览图
materialAdapter.setData(this.mData);
mDmvMarkView.setEditBeans(mEditBeans);
mTvCurrentTime.setTime(0, (long) (balanceValue(mTotalTime) * precisionValue));
mRvMaterial.setLayoutManager(new LinearLayoutManager(mContext, RecyclerView.HORIZONTAL, false));
/*在RecyclerView添加前面和后面的占位View*/
addPlaceholderView();
mRvMaterial.setAdapter(materialAdapter);
// 滑块的宽度等于滑块的高度/3f
sliderWidth = editViewHeight / 3f;
float totalCurrent = ((mViewWidth - sliderWidth * 2f) / editViewHeight) * relativeDuration;
int sliderMinCurrent = (int) (1 * precisionValue);//为了精准度,设置的数值增大10倍
int sliderTotalCurrent = (int) (totalCurrent * precisionValue + 0.5f);//为了精准度,设置的数值增大10倍
builder = new RangeDoubleSliderView.Builder();
builder.setMinCurrent(sliderMinCurrent)
.setStartCurrent(sliderTotalCurrent / 2)//起点为中心轴
.setDurationCurrent(sliderTotalCurrent / 2)//持续时间
.setTotalCurrent(sliderTotalCurrent);
mDmvMarkView.setBuilder(builder);//设置DoubleSliderView的参数
mDmvMarkView.setLimitView(headView, footerView);//设置限制左右滑块的坐标相关的View
//第一次进入,先匹配
//检查是否有匹配到的区间
checkMatching();
}
/**
* 检查是否有匹配到的区间
*/
private void checkMatching() {
if (mEditBeans != null && mEditBeans.size() > 0) {
int editSize = mEditBeans.size();
float leftSeekTo;
float rightSeekTo;
for (int i = editSize - 1; i >= 0; i--) {
DubBean dubBean = mEditBeans.get(i);
float startTime = Float.valueOf(dubBean.getOffsetStartTime());
float endTime = Float.valueOf(dubBean.getTimeLength()) + startTime;
if (startTime <= scrollCurrentTime && endTime >= scrollCurrentTime) {
leftSeekTo = startTime / relativeDuration * editViewHeight + mViewWidth / 2 - mOffsetX - sliderWidth;
rightSeekTo = leftSeekTo + (endTime - startTime) / relativeDuration * editViewHeight + sliderWidth;
mDmvMarkView.setTran(leftSeekTo, rightSeekTo);
mShowEditBean = dubBean;
break;
}
}
if (mOnScrollChangeListener != null) {
mOnScrollChangeListener.onMatchEditSelected(mShowEditBean);
}
}
}
/**
* 在RecyclerView添加前面和后面的占位View
*/
private void addPlaceholderView() {
headView = LayoutInflater.from(mContext).inflate(R.layout.item_placeholder_layout, mRvMaterial, false);
setViewWidth(headView, mViewWidth / 2f);
materialAdapter.addHeaderView(headView);
footerView = LayoutInflater.from(mContext).inflate(R.layout.item_placeholder_layout, mRvMaterial, false);
setViewWidth(footerView, mViewWidth / 2f);
materialAdapter.addFooterView(footerView);
}
/*占位View的宽度为重宽度的一半*/
private void setViewWidth(View view, float w) {
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
marginLayoutParams.width = (int) (w + 0.5f);
view.setLayoutParams(marginLayoutParams);
}
/*设置监听*/
private void initListener() {
mRvMaterial.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {//停止滑动的时候,寻找对应的View
//检查是否有匹配到的区间
if (mOnUseScrollChangeListener != null) {
mOnUseScrollChangeListener.onStateIdle();
}
checkMatching();
} else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
if (mOnUseScrollChangeListener != null) {
mOnUseScrollChangeListener.onUserDragging();
}
}
if (mOnScrollChangeListener != null) {
mOnScrollChangeListener.onScrollStateChanged(recyclerView, newState);
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
mOffsetX += dx;//RecyclerView整体的滑动偏移量
//得到中心轴对应的时间
scrollCurrentTime = balanceValue(mOffsetX / editViewHeight/* * relativeDuration*/ * precisionValue) / precisionValue;
AppLog.i(TAG + "_AVM", "onScrolled:预览条的偏移量 mOffsetX=" + mOffsetX + "scrollCurrentTime == " + scrollCurrentTime + "startTime" + startTime);
//设置中心轴对应的时间
mTvCurrentTime.setTime((long) (scrollCurrentTime * precisionValue), (long) (balanceValue(mTotalTime) * precisionValue));
//使ShadowDoubleSliderViewView跟随滚动
mDmvMarkView.setCustomScrollX(scrollCurrentTime, mOffsetX, dx);
mDmvMarkView.setRecordingAndStartTime(isRecording, startTime);
if (mOnScrollChangeListener != null) {
mOnScrollChangeListener.onScrolled(recyclerView, dx, dy);
mOnScrollChangeListener.onCurrentTime(scrollCurrentTime);
}
}
});
}
public void setLimit(boolean mLimitValue) {
this.mLimitValue = mLimitValue;
}
public boolean getLimit() {
return mLimitValue;
}
/*使中心轴滚动到对应的时间*/
public void seekTo(float currentTime) {
mRvMaterial.scrollBy((int) (-(scrollCurrentTime - currentTime) * editViewHeight / relativeDuration), 0);
}
public void smoothScrollToPosition(int position) {
mRvMaterial.smoothScrollToPosition(position);
}
/*平衡误差*/
private int balanceValue(float value) {
if (value % 0.5f != 0) {
value += 0.5f;
} else {
//Nothing
Log.i(TAG, "balanceValue: 有等于0的value=" + value);
}
return (int) value;
}
public float getCurrentTime() {
return scrollCurrentTime;
}
public float getTotalTime() {
return mTotalTime;
}
public List<DubBean> getEditBeans() {
return mEditBeans;
}
private OnScrollChangeListener mOnScrollChangeListener;
public void setOnEditScrollChangeListener(OnScrollChangeListener onScrollChangeListener) {
this.mOnScrollChangeListener = onScrollChangeListener;
}
private OnUseScrollChangeListener mOnUseScrollChangeListener;
public void setOnUseScrollChangeListener(OnUseScrollChangeListener onUseScrollChangeListener) {
this.mOnUseScrollChangeListener = onUseScrollChangeListener;
}
/**
* 为了可能的需要,该回调结合了{@link RecyclerView.OnScrollListener} 和 {@link DoubleSliderView.OnSliderChangerListener}
* 并且实现了接口的方法,但是没有做处理。外部有需要的可以选择复写特定的方法来处理
*/
public abstract static class OnScrollChangeListener extends RecyclerView.OnScrollListener implements DoubleSliderView.OnSliderChangerListener {
//匹配到对应的编辑区域
public abstract void onMatchEditSelected(DubBean editBean);
public abstract void onMatchEditChange(DubBean editBean);
public void onCurrentTime(float currentTime) {
}
@Override
public void onStartTouch(int touchThumb) {
}
@Override
public void onLeftSliderChange(long leftCurrent, long rightCurrent, long totalCurrent) {
}
@Override
public void onRightSliderChange(long leftCurrent, long rightCurrent, long totalCurrent) {
}
@Override
public void onStopTouch(int touchThumb) {
}
protected abstract void onEndCrop(@NotNull DubBean showEditBean);
}
public interface OnUseScrollChangeListener {
void onUserDragging();//人为滑动
void onStateIdle();//停止拖动
}
}
上面这个类更多的是处理逻辑其实可以不用去关注太多,重要的是DubMarkView
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import com.wanyueliang.avm.R;
import com.wanyueliang.avm.model.databean.DubBean;
import com.wanyueliang.avm.utils.log.AppLog;
import com.wanyueliang.avm.widget.slider.scroll_slider.RangeDoubleSliderView;
import java.util.List;
public class DubMarkView extends RangeDoubleSliderView {
private final String TAG = getClass().getSimpleName();
private Context mContext;
/*data*/
//参数
private float precisionValue = 100f;//核心数据----精度
private float mCurrentTime;//核心数据----精度
//绘制数据
private Paint mShadowPaint;//线条的画笔颜色
private Paint mLinePaint;//线条的画笔颜色
private float mLineSize;//线条的画笔宽度
private int mShadowColor;//线条的画笔颜色
protected float mViewWidth;//View的宽度
protected float mViewHeight;//View的高度
protected Rect rect = new Rect();
private List<DubBean> dubBeans;
private boolean isRecording;
private float startTime;
public DubMarkView(Context context) {
this(context, null);
}
public DubMarkView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DubMarkView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w > 0 && h > 0) {
mViewWidth = w;
mViewHeight = h;
}
}
private void initialize() {
mContext = getContext();
mShadowColor = getResources().getColor(R.color.colorPrimary);
mShadowPaint = new Paint();
mShadowPaint.setColor(mShadowColor);
mShadowPaint.setAlpha((int) (255 * 0.4));
mShadowPaint.setAntiAlias(true);
mShadowPaint.setStyle(Paint.Style.FILL);
mLineSize = 8;
int lineColor = getResources().getColor(R.color.colorPrimary);
mLinePaint = new Paint();
mLinePaint.setColor(lineColor);
mLinePaint.setStrokeWidth(mLineSize);
mLinePaint.setAlpha((int) (255 * 0.4));
mLinePaint.setAntiAlias(true);
}
public void setEditBeans(List<DubBean> dubBeans) {
this.dubBeans = dubBeans;
invalidate();
}
public boolean isRecording() {
return isRecording;
}
public void setRecordingAndStartTime(boolean recording, float startTime) {
isRecording = recording;
this.startTime = startTime;
}
public void notifyDataChange() {
invalidate();
}
private float mOffsetX;
@Override
public void setCustomScrollX(float currentTime, int offsetX, int dxValue) {
super.setCustomScrollX(currentTime, offsetX, dxValue);
mCurrentTime = currentTime;
mOffsetX = offsetX;
invalidate();
}
@Override
public void onDraw(Canvas canvas) {
//
if (isRecording) {
mShadowPaint.setColor(mShadowColor);
mShadowPaint.setAlpha((int) (255 * 0.4));
rect.left = (int) ((mViewWidth / 2f + startTime * mViewHeight) - mOffsetX);
rect.right = (int) (mViewWidth / 2f + mCurrentTime * mViewHeight - mOffsetX) + 1;
AppLog.i("DubMarkView_AVM" + "_AVM", "Recording !!!! === startTime == " + mCurrentTime + "mCurrentTime == " + mCurrentTime);
rect.top = 0;
rect.bottom = (int) mViewHeight;
canvas.drawLine(rect.left + mLineSize / 2f, 0, rect.left + mLineSize / 2f, mViewHeight, mLinePaint);
canvas.drawLine(rect.right - mLineSize / 2f, 0, rect.right - mLineSize / 2f, mViewHeight, mLinePaint);
canvas.drawRect(rect, mShadowPaint);
drawEditRect(canvas);
} else {
drawEditRect(canvas);
}
super.onDraw(canvas);
}
private void drawEditRect(Canvas canvas) {
if (dubBeans != null) {
for (int i = 0; i < dubBeans.size(); i++) {
DubBean dubBean = dubBeans.get(i);
mShadowPaint.setColor(mShadowColor);
mShadowPaint.setAlpha((int) (255 * 0.4));
rect.left = (int) (mViewWidth / 2f + (Float.valueOf(dubBean.getOffsetStartTime()) * mViewHeight) - mOffsetX);
rect.right = (int) (mViewWidth / 2f + ((Float.valueOf(dubBean.getOffsetStartTime()) + Float.valueOf(dubBean.getTimeLength())) * mViewHeight) - mOffsetX) + 1;
AppLog.i("DubMarkView_AVM" + "_AVM",
"drawEditRect === startTime == " + Float.valueOf(dubBean.getOffsetStartTime())
+ "mCurrentTime == " + (Float.valueOf(dubBean.getOffsetStartTime()) + Float.valueOf(dubBean.getTimeLength()))
);
rect.top = 0;
rect.bottom = (int) mViewHeight;
canvas.drawLine(rect.left + mLineSize / 2f, 0, rect.left + mLineSize / 2f, mViewHeight, mLinePaint);
canvas.drawLine(rect.right - mLineSize / 2f, 0, rect.right - mLineSize / 2f, mViewHeight, mLinePaint);
canvas.drawRect(rect, mShadowPaint);
}
}
}
}
核心代码就是onDraw方法在录制的时候通过外部传入的当前时间绘制两条线然后绘制Rect画出蓝色条形图
RecyclerView这里就不详细描述了我们要说的是
RangeDoubleSliderView这个类更多的是实现了一个两边可滑动条,其实我们这个功能中用不上可以继承View不用继承RangeDoubleSliderView也没关系