Android中View滑动方式的选择
ALM
8年前
<p>View的滑动是我们开发中需要的一项基本技能,当然,Android在这方面做的还是比较出色, 提供了多种实现方式。</p> <ol> <li>重写View的onTouchEvent或设置View的setOnTouchListener(),在MotionEvent.MotionEvent.ACTION_MOVE中做相应的滑动处理;</li> <li>采用动画的方式(View动画或属性动画)实现;</li> </ol> <p>不论哪种方式,其实质都是可以调用scrollTo/scrollBy,或者setLayoutParams()或layout()来实现的。</p> <p>下面我们对这几种情况进行详细说明:</p> <p>由于onTouchEvent和setOnTouchListener()可实现相同的功能,但setOnTouchListener()使用起来更简单,因为不需要自定义View, 同时setOnTouchListener中的onTouch比onTouchEvent优先执行,从View的dispatchTouchEvent()就可看出:</p> <pre> <code class="language-java">public boolean dispatchTouchEvent(MotionEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { return true; } // 没有设置mOnTouchListener的时候再执行onToucheEvent() if (onTouchEvent(event)) { return true; } } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } return false; }</code></pre> <p>那么下面就以setOnToucheListener()为例,来实现第一种滑动方案:</p> <pre> <code class="language-java">view.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { switch (motionEvent.getActionMasked()) { case MotionEvent.ACTION_DOWN: startX = motionEvent.getX(); startY = motionEvent.getY(); break; case MotionEvent.ACTION_MOVE: float distanceX = motionEvent.getX() - startX; float distanceY = motionEvent.getY() - startY; // 处理滑动事件,其滑动距离为distance startX = motionEvent.getX(); startY = moitonEvent.getY(); break; default: break; } return true; } });</code></pre> <p>上面的代码是实现滑动的一个基本框架,其滑动方式的差异就存在于处理滑动事件的部分。下面我们通过具体的方式来实现:</p> <h3>scrollTo/scrollBy方式</h3> <p>在使用这种方式的时候需要注意以下问题:</p> <ol> <li> <p>它引起的移动仅仅是内容的移动,View本身是不移动的。例如,对于TextView,使用这种方式滑动时,移动的只是文字,而TextView本身不会移动;而对于ViewGroup, 移动的当然也就是childView了。</p> </li> <li> <p>scrollTo表示移动至指定位置,scrollBy表示移动指定偏移量。这两个函数中的参数需要特别注意一下,参数>0表示沿着坐标轴反方向移动,反之向坐标轴正方向移动,与我们理解的坐标总是反的。</p> </li> <li> <p>由于移动的是View的内容而不是View本身,所以移动之后View的事件触发区域还停留在移动之前的位置,也就是说,移动之后,View的内容所在的位置可能会出现无法响应事件的情况。</p> </li> </ol> <p>了解了上面的基本知识,下面我们来通过这种方式来完善我们的代码,下面我们只展示处理滑动事件的代码:</p> <pre> <code class="language-java">scrollBy(distanceX, distanceY); /* 当然也可以通过scrollTo来实现 int scrollX = view.getScrollX(); int scrollY = view.getScrollY(); scrollTo(scrollX + distanceX, scrollY + distanceY); */</code></pre> <p>毫无疑问,scrollBy比scrollTo实现起来要简单,毕竟scrollBy也是通过scrollTo来实现的,源码如下:</p> <pre> <code class="language-java">public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }</code></pre> <p>这种方式实现滑动很简单,但局限性也很大,因为它只能移动View的内容,而不能移动View本身,所以经常使用在ViewGroup的滑动处理中。</p> <h3>LayoutParams方式</h3> <p>在开发过程中,我们经常遇到需要动态修改View位置或大小的情况,一般我们都是通过获取其LayoutParams,然后设置相应的属性,最后调用setLayoutParams()来完成。那在滑动处理过程中,也可以使用这种方式,动态地修改其LayoutParams的属性来实现滑动。具体代码如下:</p> <pre> <code class="language-java">ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)view.getLayoutParams(); params.leftMargin += distanceX; params.topMargin += distanceY; view.setLayoutParams(params);</code></pre> <p>与scrollBy/scrollTo不同的是,这种方式移动的是View本身,而不是其内容。同时,其中的distanceX/distanceY大于0表示向坐标轴正方向移动,反之表示向坐标轴反方向移动。</p> <h3>调用View的layout()方法</h3> <p>View的layout()方法用来动态设置View的边距,通过重复调用这个方法,也可以达到滑动的方式。具体代码如下:</p> <pre> <code class="language-java">view.layout((int)(view.getLeft() + distanceX), (int)(view.getTop() + distanceY, 0, 0)); view.requestLayout(); // 注意调用layout后需要调用requestLayout刷新布局</code></pre> <p>与LayoutParams方式一样,这种方式移动的也是View本身。从本质上来讲,这两种方式没什么区别,都是通过影响Layout过程来实现的。</p> <p>从onLayout()的源码中我们可以看出, 影响View位置的几个因素:</p> <ol> <li>对齐方式;</li> <li>边距,如left, top, right, bottom;</li> <li>params参数的margin属性;</li> <li>parentView的padding属性;</li> </ol> <p>理论上来将,修改这几种因素都可引起View位置的变化,但是对齐方式局限性太大,而且各ViewGroup还存在差异,显然不可取。修改边距其实就是layout()方式,params参数当然也就是LayoutParams方式。而parentView的padding属性影响所有的childView, 所以不能通过这种方式来修改某个View的位置。</p> <p>以上方式都是在涉及滑动手势的情况下的解决方案,其实在日常开发中,我们还会遇到其他需要View移动的场景,如点击引起View的移动。在这种情况下,我们就可以考虑使用动画方式来解决了。</p> <h3>View动画方式</h3> <p>Android提供了缩放,渐变,平移和旋转四种View动画,这种动画的特点是可重复播放,但动画结束后View将恢复位置。</p> <h3>属性动画方式</h3> <p>属性动画可认为是View动画的扩充,因为它可以通过渐变的方式来修改View的属性。这样就可解决scrollBy/scrollTo/LayoutParmas/layout()这几种方式单次移动引起的动画的生硬感。例如:将一个view向右移动200px,这里我们选择layoutParams的方式,那么伪代码应该如下所示:</p> <pre> <code class="language-java">ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)view.getLayoutParams(); params.leftMargin += 200; view.setLayoutParams(leftMargin);</code></pre> <p>如果这种改变是在点击事件下触发的,那么就会发现view瞬间向右移动了200px, 这种感觉很生硬,那如何解决了,这里属性动画就派上用场了。修改代码如下:</p> <pre> <code class="language-java">ValueAnimator animator = ValueAnimator.ofInt(200); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { int value = (int) valueAnimator.getAnimatedValue(); ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)view.getLayoutParams(); params.leftMargin += (value - lastValue); view.setLayoutParams(leftMargin); lastValue = value; } });</code></pre> <p>属性动画将根据动画的执行时间和插值器将200px分成若干份,这样通过多次setLayoutParams的方式就实现了平滑移动的效果。</p> <p>注意:本文的主线是分析View的滑动方案的选择,所以不对View动画和属性动画做更详细的介绍,对于这部分,更详细的内容可自行了解。</p> <h2>总结:</h2> <p>通过对Android提供的几种移动方案的分析,我们可以总结出以下结论:</p> <ol> <li>如果需要跟随手势移动,移动的如果是View的内容(针对ViewGroup是childView), 那么选择scrollBy/scrollTo来处理;如果移动的是View本身,那么选择LayoutParams或者layout()的方式来实现;</li> <li>如果是单次移动,且移动完成后恢复位置或重复性的移动效果,使用View动画或动画集合实现;</li> <li>如果是点击,长按,双击等事件引起的View移动,使用属性动画是一个不错的选择;</li> </ol> <p>以上仅是个人使用过程中的一些简单总结,如有不恰当之处,欢迎指正~</p> <p> </p> <p>来自:http://juhonggang.github.io/2017/01/07/Android中View滑动方式的选择/</p> <p> </p>