View的滑动是我们开发中需要的一项基本技能,当然,Android在这方面做的还是比较出色, 提供了多种实现方式。
不论哪种方式,其实质都是可以调用scrollTo/scrollBy,或者setLayoutParams()或layout()来实现的。
下面我们对这几种情况进行详细说明:
由于onTouchEvent和setOnTouchListener()可实现相同的功能,但setOnTouchListener()使用起来更简单,因为不需要自定义View, 同时setOnTouchListener中的onTouch比onTouchEvent优先执行,从View的dispatchTouchEvent()就可看出:
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; }
那么下面就以setOnToucheListener()为例,来实现第一种滑动方案:
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; } });
上面的代码是实现滑动的一个基本框架,其滑动方式的差异就存在于处理滑动事件的部分。下面我们通过具体的方式来实现:
在使用这种方式的时候需要注意以下问题:
它引起的移动仅仅是内容的移动,View本身是不移动的。例如,对于TextView,使用这种方式滑动时,移动的只是文字,而TextView本身不会移动;而对于ViewGroup, 移动的当然也就是childView了。
scrollTo表示移动至指定位置,scrollBy表示移动指定偏移量。这两个函数中的参数需要特别注意一下,参数>0表示沿着坐标轴反方向移动,反之向坐标轴正方向移动,与我们理解的坐标总是反的。
由于移动的是View的内容而不是View本身,所以移动之后View的事件触发区域还停留在移动之前的位置,也就是说,移动之后,View的内容所在的位置可能会出现无法响应事件的情况。
了解了上面的基本知识,下面我们来通过这种方式来完善我们的代码,下面我们只展示处理滑动事件的代码:
scrollBy(distanceX, distanceY); /* 当然也可以通过scrollTo来实现 int scrollX = view.getScrollX(); int scrollY = view.getScrollY(); scrollTo(scrollX + distanceX, scrollY + distanceY); */
毫无疑问,scrollBy比scrollTo实现起来要简单,毕竟scrollBy也是通过scrollTo来实现的,源码如下:
public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }
这种方式实现滑动很简单,但局限性也很大,因为它只能移动View的内容,而不能移动View本身,所以经常使用在ViewGroup的滑动处理中。
在开发过程中,我们经常遇到需要动态修改View位置或大小的情况,一般我们都是通过获取其LayoutParams,然后设置相应的属性,最后调用setLayoutParams()来完成。那在滑动处理过程中,也可以使用这种方式,动态地修改其LayoutParams的属性来实现滑动。具体代码如下:
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)view.getLayoutParams(); params.leftMargin += distanceX; params.topMargin += distanceY; view.setLayoutParams(params);
与scrollBy/scrollTo不同的是,这种方式移动的是View本身,而不是其内容。同时,其中的distanceX/distanceY大于0表示向坐标轴正方向移动,反之表示向坐标轴反方向移动。
View的layout()方法用来动态设置View的边距,通过重复调用这个方法,也可以达到滑动的方式。具体代码如下:
view.layout((int)(view.getLeft() + distanceX), (int)(view.getTop() + distanceY, 0, 0)); view.requestLayout(); // 注意调用layout后需要调用requestLayout刷新布局
与LayoutParams方式一样,这种方式移动的也是View本身。从本质上来讲,这两种方式没什么区别,都是通过影响Layout过程来实现的。
从onLayout()的源码中我们可以看出, 影响View位置的几个因素:
理论上来将,修改这几种因素都可引起View位置的变化,但是对齐方式局限性太大,而且各ViewGroup还存在差异,显然不可取。修改边距其实就是layout()方式,params参数当然也就是LayoutParams方式。而parentView的padding属性影响所有的childView, 所以不能通过这种方式来修改某个View的位置。
以上方式都是在涉及滑动手势的情况下的解决方案,其实在日常开发中,我们还会遇到其他需要View移动的场景,如点击引起View的移动。在这种情况下,我们就可以考虑使用动画方式来解决了。
Android提供了缩放,渐变,平移和旋转四种View动画,这种动画的特点是可重复播放,但动画结束后View将恢复位置。
属性动画可认为是View动画的扩充,因为它可以通过渐变的方式来修改View的属性。这样就可解决scrollBy/scrollTo/LayoutParmas/layout()这几种方式单次移动引起的动画的生硬感。例如:将一个view向右移动200px,这里我们选择layoutParams的方式,那么伪代码应该如下所示:
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)view.getLayoutParams(); params.leftMargin += 200; view.setLayoutParams(leftMargin);
如果这种改变是在点击事件下触发的,那么就会发现view瞬间向右移动了200px, 这种感觉很生硬,那如何解决了,这里属性动画就派上用场了。修改代码如下:
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; } });
属性动画将根据动画的执行时间和插值器将200px分成若干份,这样通过多次setLayoutParams的方式就实现了平滑移动的效果。
注意:本文的主线是分析View的滑动方案的选择,所以不对View动画和属性动画做更详细的介绍,对于这部分,更详细的内容可自行了解。
通过对Android提供的几种移动方案的分析,我们可以总结出以下结论:
以上仅是个人使用过程中的一些简单总结,如有不恰当之处,欢迎指正~