ViewDragHelper让你的app动起来

成功案例

在电脑上的程序,我们是可以通过鼠标拖动它,把它拖放到我们想要放的地方,这样有个好处就是,我们可以同时
干多样的事情,例如,一边看视频的同时在寻找其他有趣的视频。这样的用户体验是非常爽的,但似乎我们平常又
没有察觉到这种爽,试想一下,如果我们每次找视频,都必须关掉当前的视频又会是什么样的体验。结果不言而
喻,就像最近[老罗讲的相声](http://v.youku.com/v_show/id_XMTMyMTU2MTA1Mg==.html?from=s1.8-1-1.2)一样,
最好的用户体验就是让用户感觉不到,没有了,就会不适应。

使用过Youtube的同学应该都知道,它的播放器是可以拖动,拖动到最下面的时候,播放列表是呈现给我的,我们
还可以找其他的视频。先上个图看下效果:

Youtube

看到上的图很炫吧,实际上在Android上的app,大多数的布局都是固定的,很少有可以灵活的像PC端一样拖动的,顶多就是添加些动画效果。那么接下来,我们就看看怎么在Android平台上去实现拖拽。

Simple Demo

在库support-v4里面有两个类叫ViewDragHelper,以及ViewDragHelper.Callback。他们是实现view拖动的关键。下面是一个最简单的拖动的实现和效果图:

public class DragLayout extends LinearLayout {
    private final ViewDragHelper mDragHelper;
    private View mHeadView;
    private DragCallBack mDragCallBack;
    private Point mAutoBackOriginPos = new Point();

    public DragLayout(Context context) {
        this(context, null);
    }

    public DragLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mDragCallBack = new DragCallBack();
        mDragHelper = ViewDragHelper.create(this, 1.0f, mDragCallBack);
    }

    public class DragCallBack extends ViewDragHelper.Callback {
        @Override
        public boolean tryCaptureView (View view , int index) {
            return view == mHeadView;
        }
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx)
        {
            final int leftBound = getPaddingLeft();
            final int rightBound = getWidth() - getPaddingRight() - child.getWidth();
            final int newLeft = Math.min(rightBound, Math.max(left, leftBound));
            return newLeft;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy)
        {
            final int topBound = getPaddingTop();
            final int bottomBound = getHeight() - getPaddingBottom() - child.getHeight();
            final int newtop = Math.min(bottomBound, Math.max(top, topBound));
            return newtop;
        }
        //手指释放的时候回调
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel)
        {
            if (releasedChild == mHeadView)
            {
                invalidate();
            }
        }
        //在边界拖动时回调
        @Override
        public void onEdgeDragStarted(int edgeFlags, int pointerId)
        {

        }

        @Override
        public int getViewHorizontalDragRange(View child)
        {
            return getMeasuredWidth()-child.getMeasuredWidth();
        }

        @Override
        public int getViewVerticalDragRange(View child)
        {
            return getMeasuredHeight()-child.getMeasuredHeight();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event)
    {
        return mDragHelper.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        mDragHelper.processTouchEvent(event);
        return true;
    }

    @Override
    public void computeScroll()
    {
        if(mDragHelper.continueSettling(true))
        {
            invalidate();
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b)
    {
        super.onLayout(changed, l, t, r, b);
    }

    @Override
    protected void onFinishInflate()
    {
        super.onFinishInflate();
        mHeadView = getChildAt(0);
    }
}

这里写图片描述

创建ViewDragHelper

mDragHelper = ViewDragHelper.create(this, 1.0f, mDragCallBack);

create源码如下

    public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
        final ViewDragHelper helper = create(forParent, cb);
        helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
        return helper;
    }

从公式中可以看出mTouchSlop是和sensitivy(灵敏度)成反比的,mTouchSlop是拖动的最小距离,只有当子View拖动距离大于mTouchSlop的时候,才会真正触发子View的位移。理论上sensitivity取值范围(0,1],值越大越灵敏。

 第三个参数是一个回调函数,用来处理相应的事件。
 先来预览一下ViewDragHelper.Callback里面的主要方法:

1. public boolean tryCaptureView (View view , int index)
2. public int clampViewPositionHorizontal(View child, int left, int dx)
3. public int clampViewPositionVertical(View child, int top, int dy)
4. public void onViewReleased(View releasedChild, float xvel, float yvel)
5. public void onEdgeDragStarted(int edgeFlags, int pointerId)
6. public int getViewHorizontalDragRange(View child)
7. public int getViewVerticalDragRange(View child)
当我们手指触碰子view的时候,首先会调用方法1,在该方法中判断当前的view是否是我们要处理的,如果是,就返回true,反之亦然。
函数2,3是用来限制左右和上下的移动范围的,单位是像素,在这里我们要考虑父view的padding等问题。方法4是在手指释放的时候调用的,如果,我们想要被拖动的子View恢复到某个位置,或者根据当前位置移动到相应的某个位置,那么我们就可以在这里实现。把一个view移动到指定位置的函数是

private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel)

当然也可以用封装这个函数的方法:

public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop)

方法5是当拖动屏幕边界的时候触发的回调方法:

       public void onEdgeDragStarted(int edgeFlags, int pointerId)
        {
            mDragHelper.captureChildView(mEdgeTrackerView, pointerId);
        }

如果,要想使拖拽屏幕边缘的时候触发子view的移动一定要加上

mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT);

其中边缘有4类,EDGE_LEFT,EDGE_RIGHT,EDGE_TOP,EDGE_BOTTOM,当然这些边界可以组合生效,例如EDGE_LEFT|EDGE_RIGHT。
函数6,7有点意思,如果,不读源码的话,很难能知道他们是干嘛的(至少对我来说是这样的),从字面意思上看是获取水平或竖直方向的拖拽范围,但是,这个有什么用呢?答案马上揭晓!
当我们的子控件是button的时候,那么问题来了,当手指按下的时候,我们是要触发button的点击事件呢还是拖拽呢?
当我们没有重写函数6,7的时候,会发现拖拽button是没有任何效果的,因为button是可以消费这个事件的,事件被消费后,也就不会在回传给ViewGroup(本文中使用的是LinearLayout)的onTouchEvent去处理了,自然也就不会调用mDragHelper.processTouchEvent(event);所以就没有拖拽效果。当然一种解决办法是可以在button的onClick方法中返回false,表示没有消费这个事件。还有另一办法就是直接截取该事件,不传递到子控件中。读源码可以发现当函数6,7的返回值大于0的时候,可以成功截取该事件。关于onInterceptTouchEvent与onTouchEvent的关系,在我的另一篇博客中有详细介绍《onInterceptTouchEvent与onTouchEvent辨析》

模拟Youtube

上面的小例子已经实现了最简单的拖拽的效果,接下来,我们就实现一下文章开头那个很炫的效果:
这里写图片描述
有了simple demo 的基础,实现上面的效果就不难了。
从这个效果上看,我们整个UI有两个View,上面的我命名为mHeaderView,下面的为mBodyView。当我们触摸header(mHeaderView后面简称header),并拖动他的时候,我们能看到上面的效果,触摸其他地方的时候是没有效果的。因此,需要一个函数去判断是我们手指的触点是否在header的范围内,该函数为:

        private boolean isViewHit(View view, int x, int y) {
            int[] viewLocation = new int[2];
            view.getLocationOnScreen(viewLocation);
            int[] parentLocation = new int[2];
            this.getLocationOnScreen(parentLocation);
            int screenX = parentLocation[0] + x;
            int screenY = parentLocation[1] + y;
            return screenX >= viewLocation[0] && screenX < viewLocation[0] + view.getWidth() &&
                    screenY >= viewLocation[1] && screenY < viewLocation[1] + view.getHeight();
        }

接下来就是滑动动作,在这里需要处理两件事,一个是需要移动header和body,另一件事就是要把header缩放到我们想要的大小。

  • 移动

    移动的函数,我们可以封装ViewDragHelper类的smoothSlideViewTo()方法,来处理一些例如带有padding的情况,函数如下,他的参数是浮点型,取值范围是[0,1]。通过公式可以发现,top的值是每次header布局后的活动范围乘以sliderOffset再加上上边的padding值。mDragRange=父view的高度-被捕获View(这里指mHeaderView)的高度。

        boolean smoothSlideTo(float slideOffset) {
            final int topBound = getPaddingTop();
            int y = (int) (topBound + slideOffset * mDragRange);

            if (mDragHelper.smoothSlideViewTo(mHeaderView, mHeaderView.getLeft(), y)) {
                ViewCompat.postInvalidateOnAnimation(this);
                return true;
            }
            return false;
        }
  • 缩放
    解决了滑动,下面看看缩放,通过效果图,我们能看到header在从上到下和从下到上的滑动过程中的大小是不断变化的。而且是随着位置变化而缩放的。回忆一下在ViewDragHelper类中,我曾讲到过一个类内部类叫做ViewDragHelper.Callback,它还有个方法我没有介绍,那就是public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy)。当tryCaptureView捕捉到的view发生位置变化的时候,会回调该方法,这就为我们在移动的同时改变header的大小提供了便利。该方法实现如下,缩放是以header的右下角为轴心,因为mDragOffset的范围是[0,1]所以当header滑倒最下面的时候宽度为原始宽度的一半,随着上下滑动,body的透明度也在变化。
 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
                mTop = top;

                mDragOffset = (float) top / mDragRange;

                mHeaderView.setPivotX(mHeaderView.getWidth());
                mHeaderView.setPivotY(mHeaderView.getHeight());
                mHeaderView.setScaleX(1 - mDragOffset / 2);
                mHeaderView.setScaleY(1 - mDragOffset / 2);
                mBodyView.setAlpha(1 - mDragOffset);
                requestLayout();
            }

总结

本文的例子仅作展示说明用,还有很多地方需要完善。
本人知识有限,不免在文中有理解不准确的地方,请指出来,我们一同研究学习~~~ 欢迎转载
本文的源码在(https://github.com/sminger1202/VDHDemo),有需要的同学可以参考一下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值