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" 時就可以實現隱藏頭部下拉可見的效果了。