overscroll功能真正的实现分别在ScrollView、AbsListView、HorizontalScrollView和WebView中各有一份。ScrollView实现阻尼回弹,但是是FrameLayout布局,有些场合不适用。listview和webview适用范围也很有限。接下来,我们自定义一个LinearLayout的布局,带有回弹效果。
首先用到了OverScroller类,这个类相当于一个控制器。比如调用它的方法springBack( this.getScrollX( ), this.getScrollY( ), 0, 0, 0, 0)时,只要告诉当前的页面偏移和页面的目标偏移后,它会自动计算在回弹过程中每一个时间点的位置。它要配合computeScroll方法使用。为了易于控制滑屏过程,Android框架提供了 computeScroll()方法去控制这个流程。在绘制View时,会在draw()过程调用该方法。因此, 再配合使用Scroller实例,我们就可以获得当前应该的偏移坐标,手动使View/ViewGroup偏移至该处。
还有一个函数onOverScrolled,被overScrollBy(int, int, int, int, int, int, int, int, boolean)
调用,来对一个over-scroll操作的结果进行响应。参见overScrollBy的源代码,并不复杂。其它的参看源码吧。
package com.zte.allowance.views; import android.content.Context; import android.graphics.PointF; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.LinearLayout; import android.widget.OverScroller; public class CustomScrollView extends LinearLayout { public static final int OVERSCROLL_DISTANCE = 50; protected static final int INVALID_POINTER_ID = -1; private OverScroller fScroller; // The ‘active pointer’ is the one currently moving our object. private int fTranslatePointerId = INVALID_POINTER_ID; private PointF fTranslateLastTouch = new PointF( ); private float firstX; private float firstY; public CustomScrollView(Context context, AttributeSet attrs) { super( context, attrs ); this.initView( context, attrs ); } public CustomScrollView(Context context, AttributeSet attrs, int defStyle) { super( context, attrs, defStyle ); this.initView( context, attrs ); } protected void initView(Context context, AttributeSet attrs) { fScroller = new OverScroller( this.getContext( ) ); this.setOverScrollMode( OVER_SCROLL_ALWAYS ); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch ( action & MotionEvent.ACTION_MASK ) { case MotionEvent.ACTION_MOVE: { final float translateX = ev.getX( ); final float translateY = ev.getY( ); //距离小于5认为是单击事件,传递给子控件 if((firstX - translateX < -5) || (firstX - translateX > 5) || (firstY - translateY < -5) || (firstY - translateY > 5)) { return true; } else { return false; } } case MotionEvent.ACTION_DOWN: { if ( !fScroller.isFinished( ) ) fScroller.abortAnimation( ); final float x = ev.getX( ); final float y = ev.getY( ); firstX = x; firstY = y; fTranslateLastTouch.set( x, y ); //记录第一个手指按下时的ID fTranslatePointerId = ev.getPointerId( 0 ); return false; } default: { return false; } } } @Override public boolean onTouchEvent(MotionEvent event) { final int action = event.getAction( ); switch ( action & MotionEvent.ACTION_MASK ) { case MotionEvent.ACTION_DOWN: { if ( !fScroller.isFinished( ) ) fScroller.abortAnimation( ); final float x = event.getX( ); final float y = event.getY( ); fTranslateLastTouch.set( x, y ); //记录第一个手指按下时的ID fTranslatePointerId = event.getPointerId( 0 ); break; } case MotionEvent.ACTION_MOVE: { /** * 取第一个触摸点的位置 */ final int pointerIndexTranslate = event.findPointerIndex( fTranslatePointerId ); if ( pointerIndexTranslate >= 0 ) { float translateX = event.getX( pointerIndexTranslate ); float translateY = event.getY( pointerIndexTranslate ); //Log.i("com.zte.allowance", "fTranslatePointerId = " + fTranslatePointerId); /** * deltaX 将要在X轴方向上移动距离 * scrollX 滚动deltaX之前,x轴方向上的偏移 * scrollRangeX 在X轴方向上最多能滚动的距离 * maxOverScrollX 在x轴方向上,滚动到边界时,还能超出的滚动距离 */ //Log.i("com.zte.allowance", "delta y = " + (fTranslateLastTouch.y - translateY)); this.overScrollBy( (int) (fTranslateLastTouch.x - translateX), (int) (fTranslateLastTouch.y - translateY)/4, this.getScrollX( ), this.getScrollY( ), 0, 0, 0, OVERSCROLL_DISTANCE, true ); fTranslateLastTouch.set( translateX, translateY ); this.invalidate( ); } break; } case MotionEvent.ACTION_UP: { /** * startX 回滚开始时x轴上的偏移 * minX 和maxX 当前位置startX在minX和manX之 间时就不再回滚 * * 此配置表示X和Y上的偏移都必须复位到0 */ if (fScroller.springBack( this.getScrollX( ), this.getScrollY( ), 0, 0, 0, 0)) this.invalidate( ); fTranslatePointerId = INVALID_POINTER_ID; break; } } return true; } @Override public void computeScroll() { if ( fScroller != null && fScroller.computeScrollOffset( ) ) { int oldX = this.getScrollX( ); int oldY = this.getScrollY( ); /** * 根据动画开始及持续时间计算出当前时间下,view的X.Y方向上的偏移量 * 参见OverScroller computeScrollOffset 的SCROLL_MODE */ int x = fScroller.getCurrX( ); int y = fScroller.getCurrY( ); if ( oldX != x || oldY != y ) { //Log.i("com.zte.allowance", oldY + " " + y); this.overScrollBy( x - oldX, (y - oldY), oldX, oldY, 0, 0, 0, OVERSCROLL_DISTANCE, false ); } this.postInvalidate( ); } } @Override protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { // Treat animating scrolls differently; see #computeScroll() for why. if ( !fScroller.isFinished( ) ) { super.scrollTo( scrollX, scrollY ); if ( clampedX || clampedY ) { fScroller.springBack( this.getScrollX( ), this.getScrollY( ), 0, 0, 0, 0); } } else { super.scrollTo( scrollX, scrollY ); } awakenScrollBars( ); } @Override protected int computeHorizontalScrollExtent() { return this.getWidth( ); } @Override protected int computeHorizontalScrollOffset() { return this.getScrollX( ); } @Override protected int computeVerticalScrollExtent() { return this.getHeight( ); } @Override protected int computeVerticalScrollOffset() { return this.getScrollY( ); } }
当然,不仅仅可以是LinearLayout,还可以是别的布局。
layout文件如下:
<CustomScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/scroll_view" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <LinearLayout android:id="@+id/message_text" android:layout_width="match_parent" android:layout_height="200dp" android:layout_marginTop="-50px" android:background="@drawable/title_leaf" android:orientation="vertical"> </LinearLayout> <RelativeLayout android:id="@+id/content_id" android:layout_width="match_parent" android:layout_height="fill_parent" android:background="@color/window_bg" android:padding="5dp"> <Button android:layout_width = "wrap_content" android:layout_height = "wrap_content"/> </RelativeLayout> </CustomScrollView>
当把第一个textview设置成
android:layout_marginTop="-50px" 时就可以实现隐藏头部下拉可见的效果了。