Android的滑动分析以及各种实现
bkbv6005
8年前
<h2>一、滑动效果的产生</h2> <p>滑动一个View,本质区别就是移动一个View。改变当前View所在的坐标,原理和动画相似不断改变坐标位置实现。实现View的滑动就必须监听滑动的事件,并且根据事件传入的坐标,动态且不断改变View的坐标,从而实现View跟随用户触摸的滑动而滑动。</p> <p>(1)、Android的坐标系</p> <p>Android中将屏幕最左上角的顶点作为Android坐标系的原点,从这个点向右是X轴正方向,从这个点向下是Y轴正方向,如下图:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/451597a5ce5461b6b189d0c4bac6dc19.png"></p> <p>系统提供了getLocationOnScreen(int location[])这样的方法来获取Android坐标系中点的位置,即该视图左上角在Android坐标系中的坐标。在触控事件中使用getRawX()、getRawY()方法所获得的坐标同样是Android坐标系中的坐标。</p> <p>(2)、视图坐标系</p> <p>Android中除了上面所说的这种坐标系之外,还有一个视图坐标系,它描述了子视图在父视图中的位置关系。这两种坐标系并不矛盾也不复杂,他们的作用是相互相成的。与Android坐标系类似,视图坐标系同样是以原点向右为X轴正方向,以原点向下为Y轴正方向,只不过在视图坐标系中,原点不再是Android坐标系中的屏幕最左上角,而是以父视图左上角为坐标原点,如下图:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/efd47b20241549c0906b0e9b179e39b6.png"></p> <p>在触控事件中,通过getX()、getY()所获得的坐标系就是视图坐标系中的坐标。</p> <p>(3)、触控事件——MotionEvent</p> <p>触控事件MotionEvent在用户交互中,占着举足轻重的地位。首先看看MotionEvent封装的一些常用事件常量,定义了触控事件的不同类型。</p> <pre> <code class="language-java">//单点触摸按下动作 public static final int ACTION_DOWN = 0; //单点触摸离开动作 public static final int ACTION_UP = 1; //触摸点移动动作 public static final int ACTION_MOVE = 2; //触摸动作取消 public static final int ACTION_CANCEL = 3; //触摸动作超出边界 public static final int ACTION_OUTSIDE = 4; //多点触摸按下动作 public static final int ACTION_POINTER_DOWN = 5; //多点离开动作 public static final int ACTION_POINTER_UP = 6; </code></pre> <p>通常情况会在onTouchEvent(MotionEvent event)方法中通过event.getAction()方法来获取触控事件的类型,并使用switch-case方法来进行筛选,这个代码的模式基本固定:</p> <pre> <code class="language-java">@Override public boolean onTouchEvent(MotionEvent event) { //获取当前输入点的X、Y坐标(视图坐标) int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //处理按下事件 break; case MotionEvent.ACTION_MOVE: //处理移动事件 break; case MotionEvent.ACTION_UP: //处理离开事件 break; } return true; } </code></pre> <p>在不涉及多点操作的情况下,通常可以使用以上代码来完成触控事件的监听。</p> <p>在Android中系统提供了非常多的方法来获取坐标值、相对距离等。方法丰富固然好,下面对坐标系的API进行总结,如下图:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/58f87b556e5850172f8d2275067289f2.png"></p> <p>这些方法可以分为如下两个类别:</p> <ul> <li>View提供的获取坐标方法 <ul> <li>getTop():获取到的是View自身的顶边到其父布局顶边的距离。</li> <li>getLeft():获取到的是View自身的左边到其父布局最左边的距离。</li> <li>getRight():获取到的是View自身的右边到其父布局左边的距离。</li> <li>getBottom():获取到的是View自身的底边到其父布局顶边的距离。</li> </ul> </li> <li>MotionEvent提供的方法 <ul> <li>getX():获取点击事件距离空间左边的距离,即视图坐标。</li> <li>getY():获取点击事件距离控件顶边的距离,即视图坐标。</li> <li>getRawX():获取点击事件距离整个屏幕左边的距离,即绝对坐标。</li> <li>getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标。</li> </ul> </li> </ul> <h2>二、实现滑动的七种方式</h2> <p>当了解Android坐标系和触控事件后,我们再来看看如何使用系统提供的API来实现动态地修改一个View坐标,即实时滑动效果。而不管采用哪一种方式,其实现的思想基本是一致的,当触摸View时,系统记下当前触摸点坐标,当手指移动时,系统记下移动后的触摸点坐标,从而获取到相对于前一次坐标点的偏移量,并通过偏移量来修改View的坐标,这样不断重复,实现滑动过程。</p> <p>通过一个实例看看Android中该如何实现滑动效果,定义一个View,处于LinearLayout中,实现一个简单布局:</p> <pre> <code class="language-java"><?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.xjf.drawview.DragView1 android:layout_width="100dp" android:layout_height="100dp" /> </LinearLayout> </code></pre> <p>我们的目的就是让这个自定义的View随着手指在屏幕上的滑动而滑动。初始化时显示效果:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/64f23c944bbd05b520651ffcabda24d3.png"></p> <p>(1)、layout方法</p> <p>在View绘制时,会调用onLayout()方法来设置显示的位置。同样,可以通过修改View的left,top,right,bottom四个属性来控制View的坐标。与前面提供的模板代码一样,在每次回调onTouchEvent的时候,我们都来获取一下触摸点的坐标,代码如下:</p> <pre> <code class="language-java">//获取当前输入点的X、Y坐标(视图坐标) int x = (int) event.getX(); int y = (int) event.getY(); </code></pre> <p>接着,在Action_DOWN事件中记录触摸点的坐标,如下:</p> <pre> <code class="language-java">case MotionEvent.ACTION_DOWN: // 记录触摸点坐标 lastX = x; lastY = y; break; </code></pre> <p>最后,可以在Action_MOVE事件中计算偏移量,并将偏移量作用到Layout方法中,在目前Layout的left,top,right,bottom基础上,增加计算出来的偏移量,代码如下所示:</p> <pre> <code class="language-java">case MotionEvent.ACTION_MOVE: // 计算偏移量 int offsetX = x - lastX; int offsetY = y - lastY; // 在当前left、top、right、bottom的基础上加上偏移量 layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY); break; </code></pre> <p>这样没错移动后,View都会调用Layout方法来对自己重新布局,从而达到移动View的效果。</p> <p>上面的代码中,使用的是getX()、getY()方法来获取坐标值,即通过视图坐标来获取偏移量。当然,同样可以使用getRawX()、getRawY()来获取坐标,并使用绝对坐标来计算偏移量,代码如下:</p> <pre> <code class="language-java">// 视图坐标方式 @Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getRawX(); int y = (int) event.getRawY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 记录触摸点坐标 lastX = x; lastY = y; break; case MotionEvent.ACTION_MOVE: // 计算偏移量 int offsetX = x - lastX; int offsetY = y - lastY; // 在当前left、top、right、bottom的基础上加上偏移量 layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY); //重新设置初始化坐标 lastX = x; lastY = y; break; } return true; } </code></pre> <p>使用绝对坐标系,有一点非常需要注意的地方,就是在每次执行完ACTION_MOVE的逻辑后,一定要重新设置初始化坐标,这样才能准确地获取偏移量。</p> <p>(2)、offsetLeftAndRight()与offsetTopAndBottom()</p> <p>这个方法相当于系统提供的一个对左右、上下移动的API的封装。当计算出偏移量后,只需要使用如下代码就可以完成View的重新布局,效果与使用Layout方法一样,代码如下所示:</p> <pre> <code class="language-java">//同时对left和right进行偏移 offsetLeftAndRight(offsetX); //同时对top和bottom进行偏移 offsetTopAndBottom(offsetY); </code></pre> <p>这里的offsetX、offsetY与在layout方法中计算offset方法一样。</p> <p>(3)、LayoutParams</p> <p>LayoutParams保存了一个View的布局参数。因此可以在程序中,通过改变LayoutParams来动态地修改一个布局的位置参数,从而达到改变View位置的效果。我们可以很方便在程序中使用getLayoutParams()来获取一个View的LayoutParams。当然,计算偏移量的方法与在Layout方法中计算offset也是一样。当获取到偏移量之后,就可以通过setLayoutParams来改变其LayoutParams,代码如下:</p> <pre> <code class="language-java">LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams(); layoutParams.leftMargin = getLeft() + offsetX; layoutParams.topMargin = getTop() + offsetY; setLayoutParams(layoutParams); </code></pre> <p>这里getLayoutParams()获取LayoutParams时,需要根据View所在View父布局的类型来设置不同的类型,比如这里将View放在LinearLayout中,那么就可以使用LinearLayout.LayoutParams。如果在RelativeLayout中,就要使用RelativeLayout.LayoutParams。这一切的前提是你必须要有一个父布局,不然系统无法获取LayoutParams。</p> <p>在通过改变LayoutParams来改变一个View的位置时,通常改变的是这个View的Margin属性,所以除了使用布局的LayoutParams之外,还可以使用ViewGroup.MarginLayoutParams来实现这一一个功能,代码:</p> <pre> <code class="language-java">ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams(); layoutParams.leftMargin = getLeft() + offsetX; layoutParams.topMargin = getTop() + offsetY; setLayoutParams(layoutParams); </code></pre> <p>我们可以发现,使用ViewGroup.MarginLayoutParams更加的方便,不需要考虑父布局的类型,当然它们的本质都是一样。</p> <p>(4)、scrollTo与scrollBy</p> <p>在一个View中,系统提供了scrollTo、scrollBy两种方式来改变一个View的位置。这两个方法的区别非常好理解,与英文中To与By的区别类似,scrollTo(x,y)表示移动到一个具体的坐标点(x,y),而scrollBy(dx,dy)表示移动的增量为dx,dy。</p> <p>与前面几种方式相同,在获取偏移量后使用scrollBy来移动View,代码如下:</p> <pre> <code class="language-java">int offsetX = x - lastX; int offsetY = y - lastY; scrollBy(offsetX, offsetY); </code></pre> <p>但是,当我们拖动View的时候,你会发现View并没有移动,其实方法没错,View确实移动了,只是移动的并不是我们想要的东西。scrollTo、scrollBy方法移动的是View的content,即让View的内容移动,如果在ViewGroup中使用scrollTo、scrollBy方法,那么移动的将是所有子View,如果在View中使用,那么移动的将是View的内容,例如TextView,content就是它的文本,ImageView,content就是它的drawable对象。</p> <p>通过以上的分析,现在知道为什么不能再View中使用这两个方法来拖动这个View了。那么我们就该View所在的ViewGroup中来使用scrollBy方法,移动它的子View,代码如下:</p> <pre> <code class="language-java">((View) getParent()).scrollBy(offsetX, offsetY); </code></pre> <p>但是再次拖动View的时候,你会发现View虽然移动了,但却在乱动,并不是我们想要的跟随触摸点的移动而移动。这里先看一下视图移动,不妨这样想象一下手机屏幕是一个中空的盖板,盖板下面是一个巨大的画布,也就是我们想要显示的视图。当把这个盖板盖在画布上的某一处时,透过中间空的矩形,我们看见了手机屏幕上显示的视图,而画布上其他地方的视图,则被盖板盖住了无法看见。我们的视图与这个例子非常类似,我们没有看见视图,并不代表它就不存在,有可能只是在屏幕外面而已。当调用scrollBy方法时,可以想象为外面的盖板在移动,这么说比较抽象。</p> <p>下图一中间的矩形相当于屏幕,及可视区域。后面的content就相当于画布,代表视图。可以看到,只有视图的中间部分目前是可视的,其他部分都不可见。在可见区域中,我们设置了一个Button,它的坐标为(20,10)。</p> <p>下面使用scrollBy方法,将盖板(屏幕、可视区域),在水平方向上向X轴正方向(向右)平移20,在竖直方向上向Y轴正方向(下方)平移10,那么平移之后的可视区域如图二。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/c99b5fe3250557d5ff93ad2e18b6bc1e.png"></p> <p>图一</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/f411bedeead9c8ca05d1170c920f7bc0.png"></p> <p>图二、移动之后的可视区域</p> <p>我们发现,虽然设置scrollBy(20,10),偏移量均为X轴、Y轴正方向上的正数,但是在屏幕的可视区域内,Button却向X轴、Y轴负方向上移动了。这就是因为参考系选择的不同,而产生的不同效果。</p> <p>通过上面的分析可以发现,如果讲scrollBy中的参数dx和dy设置为正数,那么content讲向坐标轴负方向移动,如果将scrollBy中的参数dx和dy设置为负数,那么content将向坐标轴正方向移动,因此回到前面的例子,要实现跟随着手指移动而滑动的效果,就必须将偏移量改为负值,代码如下:</p> <pre> <code class="language-java">int offsetX = x - lastX; int offsetY = y - lastY; ((View) getParent()).scrollBy(-offsetX, -offsetY); </code></pre> <p>现在在运行一次发现和前面几种方式效果相同了,类似地使用绝对坐标时,也可以通过使用scrollTo发方法来实现这一效果。</p> <p>(5)、Scroller</p> <p>前面提到了scrollBy、scrollTo方法,就不得不再来说一说Scroller类。Scroller类与scrollBy、scrollTo方法十分相似。什么区别?先看例子,如果要完成这样一个效果;通过点击按钮,让一个ViewGroup的子View向右移动100个像素。问题看起来很简单,只要在按钮的点击事件中使用前面的scrollBy方法设置下偏移量就可以了吗?确实这样可以让一个子ViewGroup中的子View平移,但是不管使用scrollBy还是scrollTo方法,子view的平移都是瞬间发生的,在事件执行的时候平移就已经完成了,这样的效果会让人感觉非常突然,Google建议使用自然的过度动画来实现移动效果。因此Scroller类就这样诞生了,通过Scroller类可以实现平滑移动的效果,而不是瞬间就完成移动。</p> <p>Scroller类的实现原理,其实它与前面使用的scrollTo和scrollBy方法来实现子View跟随手指移动的原理基本类似,虽然scrollBy芳芳法是让子View瞬间从某点移动到另一个点,但是由于在ACTION_MOVE事件中不断获取手指移动的微小的偏移量,这样就将一段距离划分成了N个非常小的偏移量。虽然每个偏移量里面,通过scrollBy方法进行了瞬间移动,但是在整体上却可以获得一个平滑移动的效果。这个原理与动画的实现原理也是基本类似的,它们都是利用了人眼的视觉暂留特性。</p> <p>下面我们使用Scroller类实现平滑移动,在这个实例中,同样让子View跟随手指的滑动而滑动,但是在手指离开屏蔽时,让子View平滑的移动到初始化位置,即屏幕左上角。使用Scroller类需要如下三个步骤:</p> <ul> <li>初始化Scroller</li> </ul> <p>首先通过它的构造方法来创建一个Scroller对象,代码如下所示:</p> <pre> <code class="language-java">// 初始化Scroller mScroller = new Scroller(context); </code></pre> <ul> <li>重写computerScroller方法,实现模拟滑动</li> </ul> <p>下面我们需要重写computerScroller()芳芳法,它是使用Scroller类的核心,系统在绘制View的时候会在draw()方法中调用该方法。这个方法实际就是使用的scrollTo方法。再结合Scroller对象,帮助获取到当前滚动值。我们可以通过不断地瞬间移动一个小的距离来实现整体上的平滑移动效果。代码如下:</p> <pre> <code class="language-java">@Override public void computeScroll() { super.computeScroll(); // 判断Scroller是否执行完毕 if (mScroller.computeScrollOffset()) { ((View) getParent()).scrollTo( mScroller.getCurrX(), mScroller.getCurrY()); // 通过重绘来不断调用computeScroll invalidate(); } } </code></pre> <p>Scroller类提供了computeScrollOffset()方法来判断是否完成了整个滑动,同时也提供了getCurrX()、getCurrY()方法来获得当前的滑动坐标。在上面的代码中,唯一需要注意的是invalidate()方法,因为只能在computeScroller()方法中获取模拟过程中的scrollX和scrollY坐标。但computeScroll()方法是不会自动调用的,只能通过invalidate()->draw()->computeScroll()来间接调用compuetScroll()方法,所以需要在compuetScroll()方法中调用invaliDate()方法,实现循环获取scrollX和scrollY的目的。而当模拟过程结束后,scroller.compuetScrollOffset()方法会返回false,而中断循环,完成平滑移动过程。</p> <ul> <li>startScroll开启模拟过程</li> </ul> <p>我们在需要使用平滑移动的事件中,使用Scroller类的startScroll()方法来开启平滑移动过程。startScroll()方法具有两个重载方法。</p> <pre> <code class="language-java">public void startScroll(int startX, int startY, int dx, int dy) </code></pre> <pre> <code class="language-java">public void startScroll(int startX, int startY, int dx, int dy, int duration) </code></pre> <p>可以看到它们的区别就是一个具有指定的支持时长,而另一个没有。很好理解,与在动画中设置duration和使用默认的显示时长是一个道理。其他四个坐标,则与他们的命名含义相同,就是起始坐标与偏移量。在获取坐标时,通常可以使用getScrollX()和getScrollY()方法来获取父视图中content所滑动到的点的坐标,需要注意的是这个值的正负,它与在scrollBy、scrollTo中讲解的情况是一样的。</p> <p>根据以上三步,就可以使用Scroller类实现平滑移动,在构造方法中初始化Scroller对象,重写View的computerScroll()方法,最后监听手指离开屏蔽的事件,并在该事件中调用startScroll()方法完成平滑移动。监听手指离开屏幕的事件,只需要在onTouchEvent中增加一个ACTION_UP监听选项即可,代码如下所示:</p> <pre> <code class="language-java">case MotionEvent.ACTION_UP: // 手指离开时,执行滑动过程 View viewGroup = ((View) getParent()); mScroller.startScroll( viewGroup.getScrollX(), viewGroup.getScrollY(), -viewGroup.getScrollX(), -viewGroup.getScrollY()); invalidate(); break; </code></pre> <p>在startScroll()方法中我们获取子View移动的距离-getScrollX()、getScrollY(),并将偏移量设置为其相反数,从而将子View滑动到原位置。这里的invalidate()方法是用来通知View进行重绘,调用computeScroll()的模拟过程。当然,也可以给startScroll()方法增加一个duration的参数来设置滑动的持续时长。</p> <p>(6)、属性动画</p> <p>属性动画请参见我的另一篇:Android全套动画使用技巧</p> <p>(7)、ViewDragHelper</p> <p>Google在其support库中为我们提供了DrawerLayout和SlidingPaneLayout两个布局来帮助开发者实现侧边栏滑动的效果。这两个新的布局方便我们创建自己的滑动布局界面,在这两个强大布局背后有一个功能强大的类——ViewDragHelper。通过ViewDragHelper,基本可以实现各种不同的滑动、拖放需求,因此这个方法也是各种滑动解决方案中的终结绝招。</p> <p>下面演示一个使用ViewDragHelper创建一个QQ侧边栏滑动的布局,如图:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/264a91a0e669e0195366396418262ebf.png"></p> <p>图三</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/e028235109cba26061d2eaad8a545319.png"></p> <p>图四</p> <ul> <li>初始化ViewDragHelper</li> </ul> <p>首先需要初始化ViewDragHelper,ViewDragHelper通常定义在一个ViewGroup的内部,通过静态工厂方法进行初始化,代码如下:</p> <pre> <code class="language-java">mViewDragHelper = ViewDragHelper.create(this, callback); </code></pre> <p>第一个参数监听的View,通常需要一个ViewGroup,即parentView;第二个参数是一个Callback回调,这个回调就是整个ViewDragHelper的逻辑核心。</p> <ul> <li>拦截事件</li> </ul> <p>重写拦截事件,将事件传递给ViewDragHelper进行处理;</p> <pre> <code class="language-java">@Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mViewDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { //将触摸事件传递给ViewDragHelper,此操作必不可少 mViewDragHelper.processTouchEvent(event); return true; } </code></pre> <ul> <li>处理computeScroll()</li> </ul> <p>使用ViewDragHelper同样需要重写computeScroll()方法,因为ViewDragHelper内部也是通过Scroller来实现平滑移动的。</p> <pre> <code class="language-java">@Override public void computeScroll() { if (mViewDragHelper.continueSettling(true)) { ViewCompat.postInvalidateOnAnimation(this); } } </code></pre> <ul> <li>处理回调Callback</li> </ul> <p>创建一个ViewDragHelper.Callback</p> <pre> <code class="language-java">private ViewDragHelper.Callback getCallback = new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { return false; } }; </code></pre> <p>as自动重写tryCaptureView()方法,通过这个方法可以指定在创建ViewDragHelper时,参数parentView中的哪一个子Vieww可以被移动,例如我们在这个实例中自定义一个ViewGroup,里面定义了两个子View——Menu View和MainView,如下代码:</p> <pre> <code class="language-java">// 何时开始检测触摸事件 @Override public boolean tryCaptureView(View child, int pointerId) { //如果当前触摸的child是mMainView时开始检测 return mMainView == child; } </code></pre> <p>具体垂直滑动方法clampViewPositionVertical()和水平滑动方法clampViewPositionHorizontal()。实现滑动这个两个方法必须写,默认返回值是0,即不发生滑动,当然如果只重写clampViewPositionVertical()或clampViewPositionHorizontal()中的一个,那么就只会实现该方向上的滑动效果。</p> <pre> <code class="language-java">// 处理垂直滑动 @Override public int clampViewPositionVertical(View child, int top, int dy) { return top; } // 处理水平滑动 @Override public int clampViewPositionHorizontal(View child, int left, int dx) { return left; } </code></pre> <p>clampViewPositionVertical(View child, int top, int dy)中的参数top,代表在垂直方向上child移动的距离,dy则表示比较前一次的增量。clampViewPositionHorizontal(View child, int left, int dx)也是类似的含义,通常情况下只需要返回top和left即可,但需要更加精确地计算padding等属性的时候,就需要对left进行一些处理,并返回合适大小的值。</p> <p>通过重写上面的三个方法,就可以实现基本的滑动效果。当用手拖动MainView的时候,它就可有跟随手指的滑动而滑动了,代码:</p> <pre> <code class="language-java">private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() { // 何时开始检测触摸事件 @Override public boolean tryCaptureView(View child, int pointerId) { //如果当前触摸的child是mMainView时开始检测 return mMainView == child; } // 处理垂直滑动 @Override public int clampViewPositionVertical(View child, int top, int dy) { return 0; } // 处理水平滑动 @Override public int clampViewPositionHorizontal(View child, int left, int dx) { return left; } }; </code></pre> <p>在前面的Scroller中讲解时实现一个效果——手指离开屏幕后,View滑动回到初始位置。现在使用ViewDragHelper实现,在ViewDragHelper.Callback中,系统提供了这样的方法——onViewReleased(),通过重写这个方法,可以非常简单地实现当手指离开屏幕后实现的操作。这个方法内部是使用Scroller类实现的,这也是前面重写computeScroll()方法的原因。</p> <pre> <code class="language-java">@Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild, xvel, yvel); //手指抬起后缓慢移动到指定位置 if (mMainView.getLeft() < 500) { //关闭菜单 //等同于Scroll的startScroll方法 mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0); ViewCompat.postInvalidateOnAnimation(DragViewGroup.this); } else { //打开菜单 mViewDragHelper.smoothSlideViewTo(mMainView,300,0); ViewCompat.postInvalidateOnAnimation(DragViewGroup.this); } } </code></pre> <p>设置让MainView移动后左边距小于500像素的时候,就使用smoothSlideViewTo()方法来讲MainView还原到初始状态,即坐标(0,0),左边距大于500则将MainView移动到(300,0)坐标,即显示MainView。</p> <pre> <code class="language-java">//ViewDragHelper mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0); ViewCompat.postInvalidateOnAnimation(DragViewGroup.this); </code></pre> <pre> <code class="language-java">//Scroller mScroller.startScroll(x,y,dx,dy); invalidate(); </code></pre> <p>滑动的时候,在自定义ViewGroup的onFinishInflate()方法中,按照顺序将子View分别定义成MenuView和MainView,并在onSizeChanged方法中获得View的宽度。如果需要根据View的宽度来处理滑动后的效果,就可以使用这个值判断。</p> <pre> <code class="language-java">/*** * 加载完布局文件后调用 */ @Override protected void onFinishInflate() { super.onFinishInflate(); mMenuView = getChildAt(0); mMainView = getChildAt(1); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = mMenuView.getMeasuredWidth(); } </code></pre> <p>最后,整个通过ViewDragHelper实现QQ侧滑功能代码:</p> <pre> <code class="language-java">package com.xjf.drawview; import android.content.Context; import android.support.v4.view.ViewCompat; import android.support.v4.widget.ViewDragHelper; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; public class DragViewGroup extends FrameLayout { private ViewDragHelper mViewDragHelper; private View mMenuView, mMainView; private int mWidth; public DragViewGroup(Context context) { super(context); initView(); } public DragViewGroup(Context context, AttributeSet attrs) { super(context, attrs); initView(); } public DragViewGroup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); } /*** * 加载完布局文件后调用 */ @Override protected void onFinishInflate() { super.onFinishInflate(); mMenuView = getChildAt(0); mMainView = getChildAt(1); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = mMenuView.getMeasuredWidth(); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mViewDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { //将触摸事件传递给ViewDragHelper,此操作必不可少 mViewDragHelper.processTouchEvent(event); return true; } private void initView() { mViewDragHelper = ViewDragHelper.create(this, callback); } private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() { // 何时开始检测触摸事件 @Override public boolean tryCaptureView(View child, int pointerId) { //如果当前触摸的child是mMainView时开始检测 return mMainView == child; } // 触摸到View后回调 @Override public void onViewCaptured(View capturedChild, int activePointerId) { super.onViewCaptured(capturedChild, activePointerId); } // 当拖拽状态改变,比如idle,dragging @Override public void onViewDragStateChanged(int state) { super.onViewDragStateChanged(state); } // 当位置改变的时候调用,常用与滑动时更改scale等 @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { super.onViewPositionChanged(changedView, left, top, dx, dy); } // 处理垂直滑动 @Override public int clampViewPositionVertical(View child, int top, int dy) { return 0; } // 处理水平滑动 @Override public int clampViewPositionHorizontal(View child, int left, int dx) { return left; } // 拖动结束后调用 @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { super.onViewReleased(releasedChild, xvel, yvel); //手指抬起后缓慢移动到指定位置 if (mMainView.getLeft() < 500) { //关闭菜单 //相当于Scroller的startScroll方法 mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0); ViewCompat.postInvalidateOnAnimation(DragViewGroup.this); } else { //打开菜单 mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0); ViewCompat.postInvalidateOnAnimation(DragViewGroup.this); } } }; @Override public void computeScroll() { if (mViewDragHelper.continueSettling(true)) { ViewCompat.postInvalidateOnAnimation(this); } } } </code></pre> <p>除此之外,ViewDragHelper很多强大的功能还没得到展示,在ViewDragHelper.Callback中,系统定义了大量的监听事件来帮助我们处理各种事件,如下:</p> <ul> <li>onViewCaptured()这个事件在用户触摸到View后回调</li> <li>onViewDragStateChanged()这个事件在拖拽状态改变时回调,比如idle,dragging等状态</li> </ul> <p>STATE_IDLE:View当前没有被拖拽也没执行动画,只是安静地待在原地</p> <p>STATE_DRAGGING:View当前正在被拖动,由于用户输入或模拟用户输入导致View位置正在改变</p> <p>STATE_SETTLING:View当前正被安顿到指定位置,由fling手势或预定义的非交互动作触发</p> <ul> <li>onViewPositionChanged()//view在拖动过程坐标发生变化时会调用此方法,包括两个时间段:手动拖动和自动滚动。</li> </ul> <p> </p> <p>来自:http://mobile.51cto.com/android-538295.htm</p> <p> </p>