DragVideo,一种在播放视频时,可以任意拖拽的方案

前言:项目已开源到我的github,点击【阅读原文】可以对应github地址。

DragVideo

A Method to Drag the Video When Playing Video

一种在播放视频时,能够拖拽的方案

为什么有这个工程

经常在爱奇艺网站上看电影,看到如果滑动掩盖了播放窗口后,就后在最下面有一个小播放界面。并且这个播放界面,是可以任意拖拽的。感觉很酷

既然web端能实现,就想了想在移动端设备上,是否也能实现这个效果,于是就有了...

效果图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.javapublic 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.javaprivate 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.javaprivate void playVideo() {    mDragVideoView.show();    if (mMediaPlayer.isPlaying())        return;    try {        mMediaPlayer.prepare();    } catch (Exception e) {        e.printStackTrace();    }    mMediaPlayer.start();}

那么在拖动的过程中,我们要在DragVideoView中重写onTouchEvent方法,如下

// DragVideoView.java@Overridepublic 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.javapublic 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.javaprivate 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@Overridepublic void computeScroll() {    if (mDragHelper.continueSettling(true)) {        ViewCompat.postInvalidateOnAnimation(this);    }}

最后由于拖拽过程中的显示视频的TextureView会不断变化,通过设置TextureView.SurfaceTextureListener,来监听当前TextureView的变化过程。

//MainActivity.java@Overridepublic void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {    mMediaPlayer.setSurface(new Surface(surface));}@Overridepublic 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);}@Overridepublic boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {    finish();    return true;}@Overridepublic void onSurfaceTextureUpdated(SurfaceTexture surface) {}

原文发布于微信公众号 - 何俊林(DriodDeveloper)

原文发表时间:2016-12-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏jianhuicode

学问Chat UI(1)

前言 由于项目需要,最近开始借鉴学习下开源的Android即时通信聊天UI框架,为此结合市面上加上本项目需求列了ChatUI要实现的基本功能与扩展功能。 ? 融...

2499
来自专栏三好码农的三亩自留地

Android-教你写小米系统应用--"我的小米"

前面的文章中,我们已经了解了如何去自定义一个ViewGroup,可以在onLayout中自由的对子View进行位置设定,我们今天这里刚好需要对上面需求提到的三部...

1712
来自专栏Android源码框架分析

三句代码创建全屏Dialog或者DialogFragment:带你从源码角度实现全屏Dialog

Dialog是APP开发中常用的控件,同Activity类似,拥有独立的Window窗口,但是Dialog跟Activity还是有一定区别的,最明显的就是:默认...

3714
来自专栏程序员互动联盟

Android Metro风格的Launcher开发系列第三篇

前言: 各位小伙伴,又到了每周更新文章的时候了,本来是周日能发出来的,这不是赶上清明节吗,女王大人发话了,清明节前两天半陪她玩,只留给我周一下午半天时间写博客 ...

4078
来自专栏Android干货园

Android--仿淘宝商品详情(继续拖动查看详情)及标题栏渐变

版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/lyhhj/article/details/52...

1971
来自专栏郭霖

Android自定义View的实现方法,带你一步步深入了解View(四)

不知不觉中,带你一步步深入了解View系列的文章已经写到第四篇了,回顾一下,我们一共学习了LayoutInflater的原理分析、视图的绘制流程、视图的状态及重...

3029
来自专栏Android开发与分享

【Android】RecyclerView的使用

2745
来自专栏刘望舒

Android View体系(九)自定义View

相关文章 Android View体系(一)视图坐标系 Android View体系(二)实现View滑动的六种方法 Android View体系(三)属...

21810
来自专栏Android开发与分享

【Android】造轮子:轮播图

4045
来自专栏james大数据架构

Android网格视图(GridView)

GridView的一些属性: 1.android:numColumns=”auto_fit”   //GridView的列数设置为自动,也可以设置成2、3、4…...

2478

扫码关注云+社区

领取腾讯云代金券