实现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>