滚动(Scrolling)是一个移动视图位置的普通程序(这是你在界面中看到的)。当 X 和 Y 轴同时发生滚动事称之为“平移”(panning)。例程序 InterctiveChat 中提供了简单的例子用于说明两种“滚动”(scrolling):“拖”(dragging)和“抛”(flinging)的不同:
拖(Dragging) 当用户在屏幕上拖动手指的时候发生。GestureDetector.OnGestureListener.onScroll() 经常实现拖(dragging)。关于此更多的细节请查看Dragging and Scaling。
// The current viewport. This rectangle represents the currently visible// chart domain and range. The viewport is the part of the app that the// user manipulates via touch gestures.privateRectF mCurrentViewport =newRectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX);// The current destination rectangle (in pixel coordinates) into which the// chart data should be drawn.privateRect mContentRect;privateOverScroller mScroller;privateRectF mScrollerStartViewport;...privatefinalGestureDetector.SimpleOnGestureListener mGestureListener=new GestureDetector.SimpleOnGestureListener() { @OverridepublicbooleanonDown(MotionEvent e) {// Initiates the decay phase of any active edge effects.releaseEdgeEffects();mScrollerStartViewport.set(mCurrentViewport);// Aborts any active scroll animations and invalidates.mScroller.forceFinished(true);ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);returntrue; }... @OverridepublicbooleanonFling(MotionEvent e1,MotionEvent e2,float velocityX,float velocityY) {fling((int) -velocityX, (int) -velocityY);returntrue; }};privatevoidfling(int velocityX,int velocityY) {// Initiates the decay phase of any active edge effects.releaseEdgeEffects();// Flings use math in pixels (as opposed to math based on the viewport).Point surfaceSize =computeScrollSurfaceSize();mScrollerStartViewport.set(mCurrentViewport);int startX = (int) (surfaceSize.x* (mScrollerStartViewport.left- AXIS_X_MIN) / ( AXIS_X_MAX - AXIS_X_MIN));int startY = (int) (surfaceSize.y* (AXIS_Y_MAX -mScrollerStartViewport.bottom) / ( AXIS_Y_MAX - AXIS_Y_MIN));// Before flinging, aborts the current animation.mScroller.forceFinished(true);// Begins the animationmScroller.fling(// Current scroll position startX, startY, velocityX, velocityY,/* * Minimum and maximum scroll positions. The minimum scroll * position is generally zero and the maximum scroll position * is generally the content size less the screen size. So if the * content width is 1000 pixels and the screen width is 200 * pixels, the maximum scroll offset should be 800 pixels. */0,surfaceSize.x-mContentRect.width(),0,surfaceSize.y-mContentRect.height(),// The edges of the content. This comes into play when using// the EdgeEffect class to draw "glow" overlays.mContentRect.width() /2,mContentRect.height() /2);// Invalidates to trigger computeScroll()ViewCompat.postInvalidateOnAnimation(this);}
当 onFling() 调用 postInvalidateOnAnimation() 时,computeScroll() 方法被触发用于更新 X 和 y 的值。这通常是使用 scroller 对象做滚动动画时完成的,如本例所示。
许多控件直接将 scroller 对象的 X 和 Y 坐标传递给 scrollTo() 方法。下面的代码使用了不同的实现方式——调用 computeScrollOffset() 方法获取当前位置的 X 和 Y 坐标。当满足显示过度滚动“发光”边缘效果(显示放大,X 或者 Y 超过范围,并且 app 尚未显示过多滚动)这里的代码设置滚动放光效果并且调用 postInvalidateOnAnimation() 触发一个无效的 view:
// Edge effect / overscroll tracking objects.privateEdgeEffectCompat mEdgeEffectTop;privateEdgeEffectCompat mEdgeEffectBottom;privateEdgeEffectCompat mEdgeEffectLeft;privateEdgeEffectCompat mEdgeEffectRight;privateboolean mEdgeEffectTopActive;privateboolean mEdgeEffectBottomActive;privateboolean mEdgeEffectLeftActive;privateboolean mEdgeEffectRightActive;@OverridepublicvoidcomputeScroll() { super.computeScroll();boolean needsInvalidate =false;// The scroller isn't finished, meaning a fling or programmatic pan// operation is currently active.if (mScroller.computeScrollOffset()) {Point surfaceSize =computeScrollSurfaceSize();int currX =mScroller.getCurrX();int currY =mScroller.getCurrY();boolean canScrollX = (mCurrentViewport.left>AXIS_X_MIN||mCurrentViewport.right< AXIS_X_MAX);boolean canScrollY = (mCurrentViewport.top>AXIS_Y_MIN||mCurrentViewport.bottom< AXIS_Y_MAX);/* * If you are zoomed in and currX or currY is * outside of bounds and you are not already * showing overscroll, then render the overscroll * glow edge effect. */if (canScrollX&& currX <0&&mEdgeEffectLeft.isFinished()&&!mEdgeEffectLeftActive) {mEdgeEffectLeft.onAbsorb((int)OverScrollerCompat.getCurrVelocity(mScroller)); mEdgeEffectLeftActive =true; needsInvalidate =true; } elseif (canScrollX&& currX > (surfaceSize.x-mContentRect.width())&&mEdgeEffectRight.isFinished()&&!mEdgeEffectRightActive) {mEdgeEffectRight.onAbsorb((int)OverScrollerCompat.getCurrVelocity(mScroller)); mEdgeEffectRightActive =true; needsInvalidate =true; }if (canScrollY&& currY <0&&mEdgeEffectTop.isFinished()&&!mEdgeEffectTopActive) {mEdgeEffectTop.onAbsorb((int)OverScrollerCompat.getCurrVelocity(mScroller)); mEdgeEffectTopActive =true; needsInvalidate =true; } elseif (canScrollY&& currY > (surfaceSize.y-mContentRect.height())&&mEdgeEffectBottom.isFinished()&&!mEdgeEffectBottomActive) {mEdgeEffectBottom.onAbsorb((int)OverScrollerCompat.getCurrVelocity(mScroller)); mEdgeEffectBottomActive =true; needsInvalidate =true; }... }
以下代码是实际执行缩放的:
// Custom object that is functionally similar to ScrollerZoomer mZoomer;privatePointF mZoomFocalPoint =newPointF();...// If a zoom is in progress (either programmatically or via double// touch), performs the zoom.if (mZoomer.computeZoom()) {float newWidth = (1f-mZoomer.getCurrZoom()) *mScrollerStartViewport.width();float newHeight = (1f-mZoomer.getCurrZoom()) *mScrollerStartViewport.height();float pointWithinViewportX = (mZoomFocalPoint.x-mScrollerStartViewport.left)/mScrollerStartViewport.width();float pointWithinViewportY = (mZoomFocalPoint.y-mScrollerStartViewport.top)/mScrollerStartViewport.height();mCurrentViewport.set(mZoomFocalPoint.x- newWidth * pointWithinViewportX,mZoomFocalPoint.y- newHeight * pointWithinViewportY,mZoomFocalPoint.x+ newWidth * (1- pointWithinViewportX),mZoomFocalPoint.y+ newHeight * (1- pointWithinViewportY));constrainViewport(); needsInvalidate =true;}if (needsInvalidate) {ViewCompat.postInvalidateOnAnimation(this);}