首页 > 代码库 > 自定义阻尼下拉回弹布局

自定义阻尼下拉回弹布局

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的源代码,并不复杂。其它的参看源码吧。

 

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( );    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 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" 时就可以实现隐藏头部下拉可见的效果了。