ViewDragHelper(三)— 打造仿陌陌视频播放页(深入篇)

本篇为该系列的第三篇,将通过一个实际的业务需求来讲述ViewDragHelper的实际运用。

目录

ViewDragHelper 的介绍以及初步使用请阅读这篇:
ViewDragHelper (一)- 介绍及简单用例(入门篇)
ViewDragHelper 的源码以及Callback的详情介绍请阅读这篇:
ViewDragHelper (二)- 源码及原理解读(进阶篇)
利用DrageHelper 打造仿陌陌APP视频播放页的demo请阅读这篇:
ViewDragHelper (三)- 打造仿陌陌视频播放页(深入篇)

介绍

首先,系统的DrawerLayout 抽屉想必大家都不陌生,它的侧重点在于左右滑动。鉴于已经有很多大牛写过类似的,咱们就不再过多地讲述这个了,有兴趣的朋友可以自行查找相关的文章。本篇文章主要讲解如何利用ViewDragHelper来打造一个可以下拉拖拽关闭以及左右滑动切换的功能。

效果演示

1. QZone

QQ空间视频播放页也有这个下拉关闭的功能,效果图如下:

QZone.gif

若想要的功能仅仅只是它,那么可以直接参考第一篇文章的代码,会简洁很多,文章链接:ViewDragHelper (一)- 介绍及简单用例(入门篇)

2. 陌陌播放页

陌陌播放页的效果图:

Momo.gif

3. 实际效果

下面是本项目的效果图:

draggableView.gif

真机展示的效果可能会好点儿

horiztonal.gif

vertical.gif

正文

本文主要讲解的点有如下几个:

  1. 滑动方向判定。
  2. 如何限制为单个方向的拖拽。
  3. 事件分发以及拦截。
  4. 平移动画问题。
  5. 下拉时缩放及背景透明处理。
  6. 背景高斯图片替换处理。
  7. 嵌套ScrollView / RecyclerView事件冲突处理。
  8. 多点触控 Invalid pointerId 问题解决。

初始化

首先, 我们还是和第一篇文章一样,创建一个DragView(继承自ViewGroup),以及一个CallBack(继承自 ViewDragHelper.Callback)。
然后进行相关初始化操作。

  1. 初始化ViewDragHelper。
  2. 初始化CallBack ,用于监听ViewDragHelper相关事件,回调给DraggableView。
  3. 初始化DraggableListener,用于回调给外部。

具体代码可参考demo,项目地址贴在文章底部。

事件分发拦截处理

代码总篇幅太长,就不贴完整源代码了,用伪代码描述大致实现思路。有需要完整代码的朋友可以自行下载GitHub 上面的demo。

onInterceptTouchEvent 方法里面判断了手势方向,以及滑动冲突,多点触控导预防处理。伪代码如下:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

    if (!isEnabled()) {
            return false;
        }

    switch(event) {
    case ACTION_DOWN:
          if (activePointerId == INVALID_POINTER) {
             return false;
           }
    case ACTION_UP or ACTION_CANCEL:
         viewDragHelper.cancel();
     }

return  isViewUnderChild || shouldInterceptTouchEvent;
}

onTouchEvent方法伪代码如下:

    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        if (activePointerId == INVALID_POINTER_POINTER) {
            return false;
        }

        if (ev.getPointerCount() > 1) { //屏蔽多指操作
                    Log.e(TAG, "onTouchEvent, getPointerCount > 1 (多点触控,屏蔽掉)");
                    mDragView.dispatchTouchEvent(cloneMotionEventWithAction(ev, MotionEvent.ACTION_CANCEL));
                    viewDragHelper.cancel();
                    return false;
                }

        switch (EVENT.ACTION) {
            case ACTION_DOWN:
               //记录按下位置的XY坐标
                viewDragHelper.processTouchEvent(ev);      
                mDragView.dispatchTouchEvent(ev);
                break;
            case ACTION_MOVE:
                
               //当前手指的XY坐标与按下坐标相减。
                if (!isJudgeWay) {//还未判定滑动方向
                  
                    //判断滑动方向

                    //从按下 ,到手指当前位置的移动距离 = 根号(X^2 + Y^2)
                      //若移动距离超过系统移动默认阈值
                    if (mMoveDistance >= viewDragHelper.getTouchSlop()) {
                        isJudgeWay = true;
                    }
                }

                /**
                 * 将事件分发给子控件的条件:
                 * 1.没有被关闭
                 * 2.非顶部下滑动
                 * 3.向上滑动
                 */
                if (isFullScreen) {//全屏状态
                    mDragView.dispatchTouchEvent(ev);
                } else {//非全屏状态下
                   
                   if(不在顶部下滑,或者上滑){
                         // 分发事件给子控件
                          mDragView.dispatchTouchEvent(ev);
                     }else if (已在ScrollView顶部,并且手势为下滑) { 
                        //顶部下拉拖拽,分发事件给DragHelper
                        viewDragHelper.processTouchEvent(ev); 
                         //分发CANCEL事件给子控件,以取消DOWN事件
                    } else if (MOVE_LEFT || MOVE_RIGHT) {//左右滑动
                        //分发事件给DragHelper进行左右拖拽
                        viewDragHelper.processTouchEvent(ev); 
                        //分发CANCEL事件给子控件,以取消DOWN事件
                    } else { //都不符合条件
                        //分发 ACTION_CANCEL给子控件;
                    }
                }
                break;
            default:
                if (ev.getAction() == MotionEvent.ACTION_UP) {
                    //还原方向判定
                }
                mDragView.dispatchTouchEvent(ev);
                viewDragHelper.processTouchEvent(ev);
                break;
        }
        //可见时消费掉触摸事件,避免底层其他控件触发
        return !isClosedAtBottom();
    }

在onInterceptTouchEvent 方法中处理事件的拦截逻辑,当手指点在子控件的有效范围区域,DraggableView 将会拦截事件,触发自身的onTouchEvent方法。

在onTouchEvent方法中判断滑动方向,并根据方向以及是否在ScrollView的顶部来分发事件。若在ScrollView顶部并且符合下拉拖拽,则将事件分发给viewDragHelper,否则分发给子控件。具体细节可参考源代码。

回调监听的处理

Y轴方向改变监听

当子控件的位置发生改变时,会触发ViewDragHelper.Callback的onViewPositionChanged方法。
我们在创建CallBack对象时,在构造方法传入了DraggableView对象进来。此时我们调用 draggableView.onViewPositionChanged()方法回调View的坐标参数。
参数根据实际需求添加即可,这里我们只需要用上top,即顶部位置,因此只写了一个参数。然后在draggableView中通过listener将其回调给外部。

限制垂直方向只能下拉,不能往上拖拽。
    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
        Log.d(TAG, "clampViewPositionVertical" + ", top" + top + ", dy:" + dy);

        //水平滑动时触发了竖直方向,屏蔽掉
        if (draggableView.Move_Way.equals(draggableView.MOVE_LEFT)
                || draggableView.Move_Way.equals(draggableView.MOVE_RIGHT)) {
            return 0;
        }
        mRangeY += dy;
        return Math.max(mRangeY, 0);
    }

由于viewDragHelper内部,会因为滑动嵌套的原因,在滑动距离未超过系统的mTouchSlop值时,会触发cancel 导致重置到原位,因此这里我们通过累加dy(单次滑动距离)来实现。并且保证 return 大于0,不让子控件向上拖拽。从而实现单向地下拉拖拽的功能。

水平方向滑动

若手势为左右滑动,则屏蔽掉垂直方向,限制只能左右滑动。


 @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        // Log.d(TAG, "clampViewPositionHorizontal" + ", left" + left + ", dx:" + dx);

        //竖直滑动时触发了水平方向,屏蔽掉
        if (draggableView.Move_Way.equals(draggableView.MOVE_TOP)
                || draggableView.Move_Way.equals(draggableView.MOVE_BOTTOM)) {
            return 0;
        }

        return left;
    }
手指松开,计算并处理滑动

ViewDragHelper的 ACTION_UP 或CANCEL事件触发时,会回调onViewReleased方法。我们需要在该方法里面判断已移动距离和方向,让子控件继续自动滑动到指定的位置。


   @Override
    public void onViewReleased(View releasedChild, float xVel, float yVel) {
        super.onViewReleased(releasedChild, xVel, yVel);
        //Log.d(TAG, "onViewReleased" + "xVel:" + xVel + ", yVel:" + yVel);
        mRangeY = 0;
        int top = releasedChild.getTop(); //获取子控件Y值
        int left = releasedChild.getLeft(); //获取子控件X值

        if (Math.abs(left) <= Math.abs(top)) {//竖直滑动
            triggerOnReleaseActionsWhileVerticalDrag(top);
        } else if (Math.abs(top) < Math.abs(left)) {//水平滑动
            triggerOnReleaseActionsWhileHorizontalDrag(left);
        }
    }

  /**
     * 计算竖直方向的滑动
     */
    private void triggerOnReleaseActionsWhileVerticalDrag(float moveY) {
        //Log.d(TAG, "ReleaseVerticalDrag"+", moveY:" + moveY);
        if (moveY < 0 && moveY <= -Y_MIN_DISTANCE) {
            draggableView.onReset();
        } else if (moveY > 0 && moveY >= Y_MIN_DISTANCE) {
            draggableView.closeToBottom();
        } else {
            draggableView.onReset();
        }
    }

    /**
     * 计算水平方向的滑动
     */
    private void triggerOnReleaseActionsWhileHorizontalDrag(float moveX) {
//        Log.d(TAG, "ReleaseHorizontalDrag"+", moveX:" + moveX);
        if (moveX < 0 && moveX <= -X_MIN_DISTANCE) {
            draggableView.closeToLeft();
        } else if (moveX > 0 && moveX >= X_MIN_DISTANCE) {
            draggableView.closeToRight();
        } else {
            draggableView.onReset();
        }
    }

其中,viewDragHelper的 settleCapturedViewAt() 以及 smoothSlideViewTo()方法都需要利用 computeScroll() 来实时刷新位置。

   @Override
    public void computeScroll() {
        if (viewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

回调外部处理

在onViewReleased 方法触发并计算滑动方向之后。就会根据计算结果判定是否回弹或者滑动到屏幕指定位置,并通过listener回调通知外部。

/**
   * Called when the view is minimized.
   */
  void onClosedToBottom();

  /**
   * Called when the view is closed to the left.
   */
  void onClosedToLeft();

  /**
   * Called when the view is closed to the right.
   */
  void onClosedToRight();

  /**
   * Called when the child view location changed
   * @param top
   */
  void onBackgroundChanged(int top);

DraggableListener接口的四个方法分别是下拉关闭,向左切换,向右切换,位置改变时回调。

透明度、缩放动画及背景动态替换

我们通过onBackgroundChanged回调方法获取 子控件top位置的改变值。然后设置透明及缩放动画。具体代码如下:

    @Override
    public void onBackgroundChanged(int top) {
        int newAlpha = 255 - (int) (255 * ((float) top / (float) dragView.getRootView().getHeight()));

        if (newAlpha == 255) {
            dragView.setBackgroundResource(R.mipmap.bg_gauss_blur);
        } else {
            dragView.setBackgroundColor(ContextCompat.getColor(this, R.color.colorBackground));
        }

        dragView.getBackground().setAlpha(newAlpha);
        if (newAlpha < 216) { //达到子控件缩放最小值,原大小的0.85倍
            scrollView.setScaleX(0.85f);
            scrollView.setScaleY(0.85f);
        } else {// newAlpha >= 204 平滑缩放
            scrollView.setScaleX(1 - (255.0f - (float) newAlpha) / 255);
            scrollView.setScaleY(1 - (255.0f - (float) newAlpha) / 255);
        }
    }

总结

ViewDragHelper实质上也是通过分析MotionEvent 事件来进行操控子控件的移动。在实际使用过程中,我们特别需要注意控件嵌套滑动的问题。通过这个demo,我们想必也会发现,利用ViewDragHelper可以帮助我们省去一部分滑动动画的繁琐逻辑。但我们需要更加注意事件的滑动冲突,合理分发事件。通过这个系列,相信大家对于View事件分发机制及滑动冲突处理也会有一个更加深刻的认识。若有疑问,欢迎留言讨论~

GitHub 项目地址传送门:ViewDragHelperDemo
有兴趣的朋友可以下载完整喜欢的朋友可以 star 一波~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容