-
DragVideo是一种支持在播放过程中任意拖拽视频位置的解决方案
资源介绍
效果图1:
效果图2:
实现思路:
1、播放视频的view选择TextureView
2、ListView下方盖上自定义ViewDragHelper,当在播放视频时,通过自定义ViewDragHelper进行拖动TextureView
3、进行渐变处理,让两个view的文字能够交替显示
4、当TextureView到达右下方时,控制在水平方向上拖动,到达左边界时,如果再滑动,就销毁TextureView
代码分析:
关于ViewDragHelper要注意如下几点:
ViewDragHelper.Callback是连接ViewDragHelper与view之间的桥梁(这个view一般是指拥子view的容器即parentView);
ViewDragHelper的实例是通过静态工厂方法创建的;你能够指定拖动的方向;
ViewDragHelper可以检测到是否触及到边缘;
ViewDragHelper并不是直接作用于要被拖动的View,而是使其控制的视图容器中的子View可以被拖动,如果要指定某个子view的行为,需要在Callback中想办法;
ViewDragHelper的本质其实是分析onInterceptTouchEvent和onTouchEvent的MotionEvent参数,然后根据分析的结果去改变一个容器中被拖动子View的位置( 通过offsetTopAndBottom(int offset)和offsetLeftAndRight(int offset)方法 ),他能在触摸的时候判断当前拖动的是哪个子View;
虽然ViewDragHelper的实例方法 ViewDragHelper create(ViewGroup forParent, Callback cb) 可以指定一个被ViewDragHelper处理拖动事件的对象,但ViewDragHelper类的设计决定了其适用于被包含在一个自定义ViewGroup之中,而不是对任意一个布局上的视图容器使用ViewDragHelper。
1.自定义的CustomViewDragHelper的初始化 ViewDragHelper一般用在一个自定义ViewGroup的内部,比如下面自定义了一个直接继承于ViewGroup的类DragvideoView,DragvideoView内部有一个mDragHelper作为成员变量:
// DragVideoView.java
public DragVideoView(Context context) {
this(context, null);
}
public DragVideoView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragVideoView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mDragHelper = CustomViewDragHelper.create(this, 1f, new MyHelperCallback());
setBackgroundColor(Color.TRANSPARENT);
}
创建一个带有回调接口的ViewDragHelper,这里是用MyHelperCallback,这些都是一些基本使用方法 拖动行为的处理已在注释中给出
// DragVideoView.java
private class MyHelperCallback extends CustomViewDragHelper.Callback { //继承CustomViewDragHelper的Callback
@Override
public boolean tryCaptureView(View child, int pointerId) {//当前view是否允许拖动
return child == mPlayer; //如果是显示视频区域的view
}
@Override
public void onViewDragStateChanged(int state) { //当ViewDragHelper状态发生变化时回调(IDLE,DRAGGING,SETTING[自动滚动时])
if (state == CustomViewDragHelper.STATE_IDLE) {
if (mIsMinimum && mDragDirect == HORIZONTAL && mDisappearDirect != SLIDE_RESTORE_ORIGINAL) {
if (mCallback != null && mCallback.get() != null)
mCallback.get().onDisappear(mDisappearDirect);//水平方向上拖拽消失回调
mDisappearDirect = SLIDE_RESTORE_ORIGINAL;
restorePosition();
requestLayoutLightly();
}
mDragDirect = NONE;
}
}
@Override
public int getViewVerticalDragRange(View child) { //垂直方向拖动的最大距离
int range = 0;
if (child == mPlayer && mDragDirect == VERTICAL) {
range = mVerticalRange;
}
Log.d(TAG, ">> getViewVerticalDragRange-range:" + range);
return range;
}
@Override
public int getViewHorizontalDragRange(View child) { //横向拖动的最大距离
int range = 0;
if (child == mPlayer && mIsMinimum && mDragDirect == HORIZONTAL) {
range = mHorizontalRange;
}
Log.d(TAG, ">> getViewHorizontalDragRange-range:"+range);
return range;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {//该方法中对child移动的边界进行控制,left , top 分别为即将移动到的位置
int newTop = mTop;
Log.d(TAG, ">> clampViewPositionVertical:" + top + "," + dy);
if (child == mPlayer && mDragDirect == VERTICAL) {
int topBound = mMinTop;
int bottomBound = topBound + mVerticalRange;
newTop = Math.min(Math.max(top, topBound), bottomBound);
}
Log.d(TAG, ">> clampViewPositionVertical:newTop-"+newTop);
return newTop;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) { //返回横向坐标左右边界值
int newLeft = mLeft;
Log.d(TAG, ">> clampViewPositionHorizontal:" + left + "," + dx);
if (child == mPlayer && mIsMinimum && mDragDirect == HORIZONTAL) {
int leftBound = -mPlayer.getWidth();
int rightBound = leftBound + mHorizontalRange;
newLeft = Math.min(Math.max(left, leftBound), rightBound);
}
Log.d(TAG, ">> clampViewPositionHorizontal:newLeft-"+newLeft+",mLeft-"+mLeft);
return newLeft;
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { //view在拖动过程坐标发生变化时会调用此方法,包括两个时间段:手动拖动和自动滚动
Log.d(TAG, ">> onViewPositionChanged:" + "mDragDirect-" + mDragDirect + ",left-" + left + ",top-" + top + ",mLeft-" + mLeft);
Log.d(TAG, ">> onViewPositionChanged-mPlayer:left-"+mPlayer.getLeft()+",top-"+mPlayer.getTop());
if (mDragDirect == VERTICAL) { //垂直方向
mTop = top;
mVerticalOffset = (float) (mTop - mMinTop) / mVerticalRange;
} else if (mIsMinimum && mDragDirect == HORIZONTAL) { // 水平方向
mLeft = left;
mHorizontalOffset = Math.abs((float) (mLeft + mPlayerMinWidth) / mHorizontalRange);
}
requestLayoutLightly();
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {//
if (mDragDirect == VERTICAL) { //如果拖拽的方向是在垂直方向上
if (yvel > 0 || (yvel == 0 && mVerticalOffset >= 0.5f))
minimize();
else if (yvel < 0 || (yvel == 0 && mVerticalOffset < 0.5f))
maximize();
} else if (mIsMinimum && mDragDirect == HORIZONTAL) { //如果已经最小化窗口,并且是在水平方向上
if ((mHorizontalOffset < LEFT_DRAG_DISAPPEAR_OFFSET && xvel < 0))
slideToLeft(); //向左滑动
else if ((mHorizontalOffset > RIGHT_DRAG_DISAPPEAR_OFFSET && xvel > 0))
slideToRight();// 向右滑动
else
slideToOriginalPosition();//原地不动
}
}
}
当在MainActivity调用ViewDragHelper的setCallback方法时,以上回调就能作用了。当点击节目列表页(第一个显示listview的界面)的item时,调用playVideo()方法,方面内部通过DragVideoView.show方法,就开始显示DragVideoView。这时视频开始播放起来,并且,我们也可以对其进行拖拽了。
// MainActivity.java
private void playVideo() {
mDragVideoView.show();
if (mMediaPlayer.isPlaying())
return;
try {
mMediaPlayer.prepare();
} catch (Exception e) {
e.printStackTrace();
}
mMediaPlayer.start();
}
那么在拖动的过程中,我们要在DragVideoView中重写onTouchEvent方法,如下
// DragVideoView.java
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean isHit = mDragHelper.isViewUnder(mPlayer, (int) event.getX(), (int) event.getY());
if (isHit) {
switch (MotionEventCompat.getActionMasked(event)) {
case MotionEvent.ACTION_DOWN: {
mDownX = (int) event.getX();
mDownY = (int) event.getY();
}
break;
case MotionEvent.ACTION_MOVE:
if (mDragDirect == NONE) {
int dx = Math.abs(mDownX - (int) event.getX());//上一次getX()时和在MOVE过程中getX()的差值
int dy = Math.abs(mDownY - (int) event.getY());//上一次getY()时和在MOVE过程中getY()的差值
int slop = mDragHelper.getTouchSlop();//用户拖动的最小距离
if (Math.sqrt(dx * dx + dy * dy) >= slop) {//判断是水平方向拖拽,还是垂直方向上拖拽
if (dy >= dx)
mDragDirect = VERTICAL;
else
mDragDirect = HORIZONTAL;
}
}
break;
case MotionEvent.ACTION_UP: {
if (mDragDirect == NONE) {
int dx = Math.abs(mDownX - (int) event.getX());
int dy = Math.abs(mDownY - (int) event.getY());
int slop = mDragHelper.getTouchSlop();
if (Math.sqrt(dx * dx + dy * dy) < slop) {
mDragDirect = VERTICAL;
if (mIsMinimum)
maximize();
else
minimize();
}
}
}
break;
default:
break;
}
}
mDragHelper.processTouchEvent(event);
return isHit;
}
以上方法最后,我们调用了,mDragHelper.processTouchEvent(event);也就是我们自定义的CustomViewDragHelper类,这个方法没有改动,就是ViewDragHelper的processTouchEvent方法。(篇幅原因,建议可以看下源码)
总结下这个方法 在processTouchEvent中对ACTIONDOWN、ACTIONMOVE和ACTION_UP事件进行了处理:
1.在ACTION_DOWN中调用回调接口中的tryCaptureView方法,看当前touch的view是否允许拖动
2.在ACTION_MOVE中,view的坐标发生改变,调用回调接口中的onViewPositionChanged方法,根据坐标信息对view进行layout,通过ViewHelper这个类中的setScaleX、setScaleY方法,实现在拖动的过程中view在XY坐标上进行相应比例的缩放;
3.在ACTIONUP后调用回调接口中的onViewReleased方法,此方法中一个重要的任务是在ACTIONUP事件后,实现view的自动滑动,这里主要是使用了ViewDragHelper中smoothSlideViewTo方法,
// CustomViewDragHelper.java
public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
mCapturedView = child;
mActivePointerId = INVALID_POINTER;
boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) {
// If we're in an IDLE state to begin with and aren't moving anywhere, we
// end up having a non-null capturedView with an IDLE dragState
mCapturedView = null;
}
return continueSliding;
}
接着到达forceSettleCapturedViewAt方法
// CustomViewDragHelper.java
private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
final int startLeft = mCapturedView.getLeft();
final int startTop = mCapturedView.getTop();
final int dx = finalLeft - startLeft;
final int dy = finalTop - startTop;
if (dx == 0 && dy == 0) {
// Nothing to do. Send callbacks, be done.
mScroller.abortAnimation();
setDragState(STATE_IDLE);
return false;
}
final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
mScroller.startScroll(startLeft, startTop, dx, dy, duration);
setDragState(STATE_SETTLING);
return true;
}
上面start了ViewDragHelper中的mScroller,在滑动过程中,通过重写computeScroll方法,可用用ViewCompat.postInvalidateOnAnimation(this)方法重绘view
// DragVideoView.java
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
最后由于拖拽过程中的显示视频的TextureView会不断变化,通过设置TextureView.SurfaceTextureListener,来监听当前TextureView的变化过程。
//MainActivity.java
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
mMediaPlayer.setSurface(new Surface(surface));
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
Log.d(TAG, ">> onSurfaceTextureSizeChanged width=" + width + ", height=" + height);
if (width == 540 && height == 303) {//如果视频是最小时,
mProgramListView.setAlpha(1.0f);//让节目列表进行展现,变成不透明
} else { //TextureView在拖动过程中
float f = (float) ((1.0 - ((float)width/1080))* 1.0f);
Log.d(TAG, ">> onSurfaceTextureSizeChanged f=" + f );
mProgramListView.setAlpha(f);//通过设置比例来让节目列表的listview渐变成不透明。视频区域越小,节目列表变得越不透明(即我们能看到)
}
mProgramListView.setVisibility(View.VISIBLE);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
finish();
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}