实现View滑动的七种方法

回首111 8年前
   <h2>Android坐标系</h2>    <p>在介绍如何实现View滑动之前先了解一下Android的坐标系,我们在初中数学就学过坐标系,有原点和X轴Y轴,不过屏幕上的坐标系稍微有点区别,移动设备一般将 <strong>屏幕的左上角</strong> 定义为原点,向右为X轴正方向,向下为Y轴正方向,如下图:</p>    <p><img src="https://simg.open-open.com/show/6b1f150d4ae68cc1b0fef347022a4d1e.png"></p>    <h2>View坐标系</h2>    <p>与屏幕坐标系相同,View也有自己的坐标系,我们可以称之为视图坐标系,描述了本身和父布局的位置关系,原点在View的左上角:</p>    <p><img src="https://simg.open-open.com/show/b3f28907cd15bdff142a1b1c44ef97dd.png"></p>    <h2>View及MotionEvent坐标获取</h2>    <p>View自身坐标获取方法</p>    <ul>     <li> <p>getTop():获取到的,是view自身的顶边到其父布局顶边的距离</p> </li>     <li> <p>getLeft():获取到的,是view自身的左边到其父布局左边的距离</p> </li>     <li> <p>getRight():获取到的,是view自身的右边到其父布局左边的距离</p> </li>     <li> <p>getBottom():获取到的,是view自身的底边到其父布局顶边的距离</p> </li>    </ul>    <p>MotionEvent坐标获取</p>    <ul>     <li> <p>getX():获取点击事件相对控件左边的x轴坐标,即点击事件距离控件左边的距离</p> </li>     <li> <p>getY():获取点击事件相对控件顶边的y轴坐标,即点击事件距离控件顶边的距离</p> </li>     <li> <p>getRawX():获取点击事件相对整个屏幕左边的x轴坐标,即点击事件距离整个屏幕左边的距离</p> </li>     <li> <p>getRawY():获取点击事件相对整个屏幕顶边的y轴坐标,即点击事件距离整个屏幕顶边的距离</p> </li>    </ul>    <p>说了这么多方法都不如一张图最直接:</p>    <p><img src="https://simg.open-open.com/show/c8f6d82bab3f19be96220b5c2c9ec83c.png"></p>    <h2>触控事件onTouch</h2>    <p>学好触控事件是掌握后续内容的重要基础,触控事件回调的MotionEvent封装了一些常用的事件常量,定义了一些常见类型动作。</p>    <pre>  <code class="language-java">/**   * A pressed gesture has started, the motion contains the initial starting location.   */  public static final int ACTION_DOWN             = 0;    /**   * A pressed gesture has finished, the motion contains the final release location as well as any intermediate   * points since the last down or move event.   */  public static final int ACTION_UP               = 1;    /**   * A change has happened during a   * press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}).   */  public static final int ACTION_MOVE             = 2;    /**   * The current gesture has been aborted.   */  public static final int ACTION_CANCEL           = 3;    /**   * A movement has happened outside of the normal bounds of the UI element.    */  public static final int ACTION_OUTSIDE          = 4;    /**   * A non-primary pointer has gone down.   */  public static final int ACTION_POINTER_DOWN     = 5;    /**   * A non-primary pointer has gone up.   */  public static final int ACTION_POINTER_UP       = 6;</code></pre>    <p>我们让View滑动的大概思路是重写View的onTouchEvent(MotionEvent event)方法,来控制View的移动,这个代码模板基本固定的,show me the code :</p>    <pre>  <code class="language-java">@Override  public boolean onTouch(View v, MotionEvent event) {      // 记录当前point所在的位置      x = (int) event.getX();      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>该方法return true 代表触控事件到这里就处理完毕了,不必要再继续传递,不懂的可以去再回顾一下Android的 <strong>触摸事件分发机制</strong> 。下面我们就可以进入主题,来看一下有哪些方法可以移动View。</p>    <h2>实现滑动</h2>    <p>我们了解了Android坐标系和触控事件,接着我们可以模拟实现View的滑动了,思路是:当发生onTouch事件时,记录下位置,当手指移动时,记录移动的坐标,获得一个相对偏移量,然后修改View的位置,不断重复下去就实现了View的模拟滑动。那么,怎么改动View的位置呢,下面有介绍几种方法可以设置View的位置。</p>    <p>##layout方法</p>    <ul>     <li>在ACTION_DOWN里面,记录下按下的坐标</li>    </ul>    <pre>  <code class="language-java">case MotionEvent.ACTION_DOWN:      lastX = x;      lastY = y;      break;</code></pre>    <ul>     <li>每次onTouch回调记录下该点的坐标</li>    </ul>    <pre>  <code class="language-java">int x = (int) event.getX();  int y = (int) event.getY();</code></pre>    <ul>     <li>在ACTION_MOVE里面计算偏移量,然后调用layout方法</li>    </ul>    <pre>  <code class="language-java">case MotionEvent.ACTION_MOVE:      int offsetX = x - lastX;      int offsetY = y - lastY;      layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);      break;</code></pre>    <p>效果如图:</p>    <p><img src="https://simg.open-open.com/show/1707da6de1c47150568e67bbd71f82c0.gif"></p>    <h2>offsetLeftAndRight()和offsetTopAndBottom()</h2>    <p>看命名就知道这个方法的作用,这是系统提供的对View上下、左右同时进行移动的API,效果与上相同。就不赘述了。</p>    <pre>  <code class="language-java">case MotionEvent.ACTION_MOVE:      int offsetX = x - lastX;      int offsetY = y - lastY;      offsetLeftAndRight(offsetX);      offsetTopAndBottom(offsetY);      break;</code></pre>    <h2>LayoutParams</h2>    <p>通过改变View的LayoutParams布局参数,就可以移动View的位置,这里通常修改View的Margin属性,代码如下:</p>    <pre>  <code class="language-java">case MotionEvent.ACTION_MOVE:      int offsetX = x - lastX;      int offsetY = y - lastY;      ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();      params.leftMargin = getLeft() + offsetX;      params.topMargin = getTop() + offsetY;      break;</code></pre>    <p>其实根据父布局的类型,可以设置LinearLayout.LayoutParams或者RelativeLayout.LayoutParams,不过这样就必须先知道父布局的类型,不如ViewGroup.MarginLayoutParams来的方便。</p>    <h2>scrollTo和scrollBy</h2>    <p>前者表示移动到具体的坐标点位置,后者表示在原有的位置基础上再移动一个偏移量。但是与之前三个方法不同的是,前三个方法都是移动View自己本身,而这两个方法移动的都是 <strong>View里面的内容</strong> ,如果放在ViewGroup中使用,则移动的是ViewGroup里面 <strong>所有的子View</strong> 。</p>    <p>那我们的思路就要换一下了,我们为了移动View,那我们就来移动View所在的ViewGroup,但是要注意的是,移动的偏移量要 <strong>取反</strong> ,为什么呢?这是因为本来是该View移动dx、dy,现在View保持不动,让ViewGroup移动,则根据 <strong>相对运动原理</strong> ,就相当于ViewGroup移动了-dx、-dy。</p>    <p>下面我们来举个简单的例子解释scrollBy。如下图:ViewGroup里面是可视区域,第二个小人的坐标是(200,100),现在我们把ViewGroup移到第二个小人的位置, <strong>scrollBy(200,100)</strong> ,效果如第二张图:在可视区域内,相当于第二个小人的偏移量为 <strong>(-200,-100)</strong> 。</p>    <p><img src="https://simg.open-open.com/show/adf30eb69087c49e51634b7545ccb711.png"></p>    <p><img src="https://simg.open-open.com/show/5986d2da1e60096eed0df2af6e1475ff.png"></p>    <p>这么解释一定明白多了,我们看一下实现代码:</p>    <pre>  <code class="language-java">case MotionEvent.ACTION_MOVE:      int offsetX = x - lastX;      int offsetY = y - lastY;      ((View)getParent()).scrollBy(-offsetX,-offsetY);      break;</code></pre>    <p>效果如图:</p>    <p><img src="https://simg.open-open.com/show/c4d8a06e707f1671294d656d6289567d.gif"></p>    <h2>Scroller</h2>    <p>通过Scroller类来实现一些平滑的动画效果,可以设置动画时间等等,简直就是滑动利器!现在我们来实现一个效果: <strong>View跟着手指滑动,当松开手指时就让View回到原始位置</strong> 。</p>    <ul>     <li>在View构造函数里初始化Scroller</li>    </ul>    <pre>  <code class="language-java">scroller = new Scroller(context);</code></pre>    <ul>     <li>重写computeScroll方法</li>    </ul>    <pre>  <code class="language-java">@Override  public void computeScroll() {      super.computeScroll();      if (scroller.computeScrollOffset()) {          offsetLeftAndRight(scroller.getCurrX()-getLeft());          offsetTopAndBottom(scroller.getCurrY()-getTop());          invalidate();      }  }</code></pre>    <ul>     <li>在ACTION_UP里启动动画</li>    </ul>    <pre>  <code class="language-java">scroller.startScroll(getLeft(),getTop(),-getLeft()+initX,-getTop()+initY,2000);</code></pre>    <p>startScroll前两个参数是起始位置,后两个参数为终点位置,第五个参数是动画持续时间,可以省略。</p>    <p>效果如图:</p>    <p><img src="https://simg.open-open.com/show/458febd6afa6cb87c63b3b83188b0dd6.gif"></p>    <h2>属性动画</h2>    <p>这个后续再单独写一篇介绍属性动画的,跳过。</p>    <h2>ViewDragHelper</h2>    <p>在开发自定义ViewGroup的时候,经常要根据业务需求实现onInterceptTouchEvent和onTouch(很繁琐啊!有木有!),不过Google在support库中为我们提供了一个超级强大的类ViewDragHelper,可以实现诸多滑动布局,侧滑菜单就是之一。这里奉上 <a href="/misc/goto?guid=4959554883332729545" rel="nofollow,noindex">官方介绍</a> ,可能需国内或许不能访问?</p>    <p>下面我们举个实现侧滑菜单的例子:自定义ViewGroup布局,然后里面有MenuView和MainView,滑动MainView超过一定距离就显示MenuView。</p>    <ul>     <li>初始化</li>    </ul>    <pre>  <code class="language-java">private View mMenuView,mMainView;  private int mWidth;    //布局完成后调用  @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();//侧滑菜单的宽度  }    //构造函数  public ViewDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {      super(context, attrs, defStyleAttr);      mViewDragHelper = ViewDragHelper.create(this,callback);  }</code></pre>    <p>其中,构造函数里的callback是我们要自己实现的业务逻辑。也是该类的重要 <strong>核心内容</strong> !</p>    <ul>     <li>拦截事件交给ViewDragHelper处理</li>    </ul>    <pre>  <code class="language-java">@Override  public boolean onInterceptTouchEvent(MotionEvent event) {      return mViewDragHelper.shouldInterceptTouchEvent(event);  }    @Override  public boolean onTouchEvent(MotionEvent event) {      mViewDragHelper.processTouchEvent(event);      return true;  }</code></pre>    <ul>     <li>重写computeScroll方法</li>    </ul>    <pre>  <code class="language-java">@Override  public void computeScroll() {      if (mViewDragHelper.continueSettling(true)) {          ViewCompat.postInvalidateOnAnimation(this);      }  }</code></pre>    <ul>     <li>实现callback</li>    </ul>    <pre>  <code class="language-java">private final ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {      @Override      public boolean tryCaptureView(View child, int pointerId) {          //触摸的布局是否为MainView          return mMainView==child;      }        @Override      public int clampViewPositionVertical(View child,int top,int dy) {          //不需要检测垂直滑动,直接返回0          return 0;      }        @Override      public int clampViewPositionHorizontal(View child,int left,int dx) {          return left;      }        @Override      public void onViewReleased(View child,float xVel,float yVel) {          super.onViewReleased(child,xVel,yVel);          //核心逻辑:滑动MainView超过一定距离就显示MenuView          if (mMainView.getLeft() < mWidth) {              mViewDragHelper.smoothSlideViewTo(mMainView,0,0);              ViewCompat.postInvalidateOnAnimation(ViewDragLayout.this);          } else {              mViewDragHelper.smoothSlideViewTo(mMainView,mWidth,0);              ViewCompat.postInvalidateOnAnimation(ViewDragLayout.this);          }      }  };</code></pre>    <p>来看一下效果:</p>    <p><img src="https://simg.open-open.com/show/d383c98dd15377205024acce8e0aeda9.gif"></p>    <p>ViewDragHelper.Callback中定义有大量的回调方法,就不一一介绍了。</p>    <h2>最后</h2>    <p>到这里,我们介绍的View滑动方法就学习完了,最后我们来实现一个滑动ViewGroup,来模拟微信下拉的粘性动画,直接上代码:</p>    <pre>  <code class="language-java">@Override  public boolean onTouchEvent(MotionEvent event) {      int y = (int) event.getRawY();      switch (event.getAction()) {          case MotionEvent.ACTION_DOWN:              if (scroller.computeScrollOffset())                  scroller.forceFinished(true);              lastY = y;              break;          case MotionEvent.ACTION_MOVE:              int scrollY = y - lastY;              offsetTopAndBottom(scrollY/3-getTop());              break;          case MotionEvent.ACTION_UP:              scroller.startScroll(getLeft(),getTop(),0,-getTop()+initY,duration);              invalidate();              break;      }      return true;  }</code></pre>    <p>在ACTION_DOWN里判断动画有没有结束,可以强制结束,这样就可以连续向下拖动。在ACTION_MOVE里设置偏移量,除以3可以调节偏移量滑动的比例。最后在松手时回到原位置,一起来看一下效果吧。</p>    <p><img src="https://simg.open-open.com/show/b31fe55f6b2f073de734b290a2657ffc.gif"></p>    <p> </p>    <p>来自:http://www.biglong.cc/android/2016/09/23/实现View滑动的七种方法</p>    <p> </p>