LinearLayout scroll to the top
<p>I write a RecyclerViewRefresh follow SwipeRefreshLayout.class. When I pull the view until it doesn't move and then release,the view reset to the original. The issue is that view should trigger Timer and then that Timer reset the view. I couldn't find the reason.</p>

<p>Please tell me why offsetTopAndBottom() can make the view automatically back to the original place. Thanks.</p>

<p>I use setY() to solve this problem. But I also want to know why. And I read the offsetTopAndBottom()'s source,also can not find any clue. </p>

<p>RecyclerViewRefresh's code:</p>

<pre><code>public class RecyclerViewRefresh extends LinearLayout {
private static final String LOG_TAG=RecyclerViewRefresh.class.getSimpleName();
private static final int INVALID_POINTER=-1;
//Default offset in dips from the top of the view to where the progress
//spinner should stop
private static final int DEFAULT_CIRCLE_TARGET=64;
private static final float DRAG_RATE=.5f;

private View headerView,footerView,thisView;
private View mTarget; //the target of the gesture
private ImageView arrowIv;
private TextView refreshTv;
private ProgressBar progressBar;
private OnPullToRefresh refreshListener=null;
private OnDragToLoad loadListener=null;
float startY=0;

private int headerHeight=0;
private boolean mReturningToStart;
private boolean mRefreshing=false;
private boolean mNestedScrollInProgress;
private int mCurrentTargetOffsetTop;
protected int mOriginalOffsetTop;
private boolean mIsBeingDragged;
private int mActivePointerId=INVALID_POINTER;
private float mInitailDownY;
private int mTouchSlop;
private float mTotalDragDistance=-1;
private float mInitialMotionY;
private float mSpinnerFinalOffset;
private boolean updateHeader=true;
private Handler handler=new Handler();
private Timer timer;

public RecyclerViewRefresh(Context context) {
    super(context);
    initView(context);
}

public RecyclerViewRefresh(Context context, AttributeSet attrs) {
    super(context, attrs);
    initView(context);
}

public RecyclerViewRefresh(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initView(context);
}
private void initView(Context context)
{
    thisView=this;
    mTouchSlop= ViewConfiguration.get(context).getScaledTouchSlop();
    headerView=LayoutInflater.from(context).inflate(R.layout.header_layout,null);
    footerView=LayoutInflater.from(context).inflate(R.layout.header_layout,null);
    measureView(headerView);
    arrowIv=(ImageView)headerView.findViewById(R.id.arrow);
    refreshTv=(TextView)headerView.findViewById(R.id.tip);
    progressBar=(ProgressBar)headerView.findViewById(R.id.progress);
    headerHeight=headerView.getMeasuredHeight();
    LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
            headerView.getMeasuredHeight());
    this.addView(headerView,lp);
    setTopHeader(headerHeight);

    final DisplayMetrics metrics=getResources().getDisplayMetrics();
    mSpinnerFinalOffset=DEFAULT_CIRCLE_TARGET*metrics.density;
    mTotalDragDistance=mSpinnerFinalOffset;
}
/**
 * 通知父布局，占用的宽，高；
 *
 * @param view
 */
private void measureView(View view) {
    ViewGroup.LayoutParams p = view.getLayoutParams();
    if (p == null) {
        p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }
    int width = ViewGroup.getChildMeasureSpec(0, 0, p.width);
    int height;
    int tempHeight = p.height;
    if (tempHeight &gt; 0) {
        height = MeasureSpec.makeMeasureSpec(tempHeight,
                MeasureSpec.EXACTLY);
    } else {
        height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    }
    view.measure(width, height);
}
private void setTopHeader(int height)
{
    if(Build.VERSION.SDK_INT&gt;=Build.VERSION_CODES.HONEYCOMB)
    {
        this.setY(-height);
    }else{
        LayoutParams lp=new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,height);
        lp.topMargin=-height;
        this.setLayoutParams(lp);
    }
    headerView.invalidate();
}

/**
 * Set the listener to be notified when a refresh is triggered via the
 * pull gesture.
 * @param listener
 */
public void setOnPullToRefresh(OnPullToRefresh listener)
{
    this.refreshListener=listener;
}

/**
 * Set the listener to be notified when a load is triggered via the
 * drag gesture
 * @param listener
 */
public void setOnDragToLoad(OnDragToLoad listener)
{
    this.loadListener=listener;
}


private void ensureTarget(){
    if(mTarget==null){
        for(int i=0;i&lt;getChildCount();i++)
        {
            View child=getChildAt(i);
            if(child instanceof RecyclerView)
            {
                mTarget=child;
                break;
            }
        }
    }
}

/**
 * @return Whether it is possible for the child view of this layout to
 * scroll up.Override this if the child view is a custom view.
 */
public boolean canChildScrollUp(){
    if(mTarget==null)
    {
        ensureTarget();
    }
    if(Build.VERSION.SDK_INT&lt;14)
    {
        if(mTarget instanceof AbsListView)
        {
            final AbsListView absListView=(AbsListView)mTarget;
            return absListView.getChildCount()&gt;0
                    &amp;&amp;(absListView.getFirstVisiblePosition()&gt;0
                    ||absListView.getChildAt(0).getTop()&lt;absListView.getPaddingTop());
        }else{
            return ViewCompat.canScrollVertically(mTarget,-1)|| mTarget.getScrollY()&gt;0;
        }
    }else{
        return ViewCompat.canScrollVertically(mTarget,-1);
    }
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    ensureTarget();
    final int action=MotionEventCompat.getActionMasked(ev);

    if(mReturningToStart &amp;&amp; action == MotionEvent.ACTION_DOWN){
        mReturningToStart = false;
    }

    if(!isEnabled() || mReturningToStart || canChildScrollUp()
            ||mRefreshing || mNestedScrollInProgress){
        return false;
    }

    switch (action){
        case MotionEvent.ACTION_DOWN:
            setTargetOffsetTopAndBottom(mOriginalOffsetTop-headerView.getTop(),true);
            mActivePointerId=MotionEventCompat.getPointerId(ev,0);
            mIsBeingDragged=false;
            final float initialDownY=getMotionEventY(ev,mActivePointerId);
            if(initialDownY==-1){
                return false;
            }
            mInitailDownY=initialDownY;
            updateHeader=true;
            break;
        case MotionEvent.ACTION_MOVE:
            if(mActivePointerId==INVALID_POINTER){
                Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
                return false;
            }
            final float y=getMotionEventY(ev,mActivePointerId);
            if(y==-1){
                return false;
            }
            final float yDiff=y-mInitailDownY;
            if(yDiff&gt;mTouchSlop &amp;&amp; !mIsBeingDragged){
                mInitialMotionY=mInitailDownY+mTouchSlop;
                mIsBeingDragged=true;
            }
            break;
        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mIsBeingDragged=false;
            mActivePointerId=INVALID_POINTER;
            break;
    }
    return mIsBeingDragged;
}

private float getMotionEventY(MotionEvent ev,int activePointerId){
    final int index=MotionEventCompat.findPointerIndex(ev,activePointerId);
    if(index&lt;0){
        return -1;
    }
    return MotionEventCompat.getY(ev,index);
}
private void setTargetOffsetTopAndBottom(int offset,boolean requiresUpdate){
    if(this.getTop()&lt;headerHeight+5)
    {
        this.offsetTopAndBottom(offset);
        mCurrentTargetOffsetTop=this.getTop();
        if(requiresUpdate &amp;&amp; Build.VERSION.SDK_INT&lt;11){
            invalidate();
        }

        if(this.getTop()&gt;headerHeight)
        {
            if(updateHeader){
                updateHeader=false;
                refreshTv.setText(getResources().getText(R.string.releasetorefresh));
                RotateAnimation animation=new RotateAnimation(0,180,
                        Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
                animation.setDuration(800);
                animation.setFillAfter(true);
                arrowIv.startAnimation(animation);
            }
        }
    }

}

private void onSecondaryPointerUp(MotionEvent ev){
    final int pointerIndex=MotionEventCompat.getActionIndex(ev);
    final int pointerId=MotionEventCompat.getPointerId(ev,pointerIndex);
    if(pointerId==mActivePointerId){
        //This was our active pointer going up. Choose a new
        //active pointer and adjust accordingly.
        final int newPointerIndex=pointerIndex==0?1:0;
        mActivePointerId=MotionEventCompat.getPointerId(ev,newPointerIndex);
    }
}
@Override
public boolean onTouchEvent(MotionEvent event)
{
    final int action=MotionEventCompat.getActionMasked(event);
    int pointerIndex=-1;

    if(mReturningToStart&amp;&amp;action==MotionEvent.ACTION_DOWN){
        mReturningToStart=false;
    }
    if(!isEnabled() || mReturningToStart
            || canChildScrollUp() || mNestedScrollInProgress){
        //Fail fast if we're not in a state where a swipe is possible
        return false;
    }
    switch(action){
        case MotionEvent.ACTION_DOWN:
            mActivePointerId=MotionEventCompat.getPointerId(event,0);
            mIsBeingDragged=false;
            break;
        case MotionEvent.ACTION_MOVE:{
            pointerIndex=MotionEventCompat.findPointerIndex(event,mActivePointerId);
            if(pointerIndex&lt;0){
                Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                return false;
            }
            final float y=MotionEventCompat.getY(event,pointerIndex);
            final float overscrollTop=(y-mInitialMotionY)*DRAG_RATE;
            if(mIsBeingDragged){
                if(overscrollTop&gt;0){
                    moveSpinner(overscrollTop);
                }else{
                    return false;
                }
            }
            break;
        }
        case MotionEventCompat.ACTION_POINTER_DOWN:{
            pointerIndex=MotionEventCompat.getActionIndex(event);
            if(pointerIndex&lt;0){
                Log.e(LOG_TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");
                return false;
            }
            mActivePointerId=MotionEventCompat.getPointerId(event,pointerIndex);
            break;
        }
        case MotionEvent.ACTION_POINTER_UP:
            onSecondaryPointerUp(event);
            break;
        case MotionEvent.ACTION_UP:{
            pointerIndex=MotionEventCompat.findPointerIndex(event,mActivePointerId);
            if(pointerIndex&lt;0){
                Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
                return false;
            }
            final float y=MotionEventCompat.getY(event,pointerIndex);
            mIsBeingDragged=false;
            finishSpinner();
            mActivePointerId=INVALID_POINTER;
            return false;
        }
        case MotionEvent.ACTION_CANCEL:
            return false;
    }
    return true;
}

private void moveSpinner(float overscrollTop){
    float originalDragPercent=overscrollTop/mTotalDragDistance;
    float dragPercent=Math.min(1f,Math.abs(originalDragPercent));
    float adjustedPercent=(float)Math.max(dragPercent-.4,0)*5/3;
    float extraOS=Math.abs(overscrollTop)-mTotalDragDistance;
    float slingshotDist=mSpinnerFinalOffset;
    float tensionSlingshotPercent=Math.max(0,Math.min(extraOS,slingshotDist*2)/slingshotDist);
    float tensionPercent=(float)((tensionSlingshotPercent/4)-Math.pow(
            (tensionSlingshotPercent/4),2))*2f;
    float extraMove=(slingshotDist)*tensionPercent*2;

    int targetY=mOriginalOffsetTop+(int)((slingshotDist*dragPercent)+extraMove);
    setTargetOffsetTopAndBottom(targetY-mCurrentTargetOffsetTop,true);
}
private void finishSpinner(){
    if(this.getTop()&gt;headerHeight){
        setRefreshing(true,true);
    }else{
        //cancel refresh
        mRefreshing=false;
        animateOffsetToStartPosition();
    }
}
private void setRefreshing(boolean refreshing,final boolean notify)
{
    if(mRefreshing!=refreshing){
        ensureTarget();
        mRefreshing=refreshing;
        if(mRefreshing){
            refreshListener.onRefresh();
            arrowIv.setVisibility(View.GONE);
            progressBar.setVisibility(View.VISIBLE);
        }else{
            arrowIv.setVisibility(View.VISIBLE);
            progressBar.setVisibility(View.GONE);
            animateOffsetToStartPosition();
        }
    }
}
public void setRefreshing(boolean refreshing){
    if(!refreshing){
        setRefreshing(refreshing,false);
    }
}
private void animateOffsetToStartPosition(){
    refreshTv.setText(getResources().getText(R.string.pulltorefresh));
    arrowIv.clearAnimation();
    Log.d(LOG_TAG,"getTop="+this.getTop()+"  timer="+((timer==null)?"null":"notnumm"));
    if(timer==null&amp;&amp;this.getTop()&gt;0)
    {
        timer=new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        if(thisView.getTop()&gt;0)
                        {
                            thisView.offsetTopAndBottom(-1);
                            mCurrentTargetOffsetTop = headerView.getTop();
                            if ( Build.VERSION.SDK_INT &lt; 11) {
                                invalidate();
                            }
                        }else{
                            Log.d(LOG_TAG,"cancel");
                            timer.cancel();
                            timer=null;
                        }
                    }
                });
            }
        },10,10);
    }
}

/**
 * Classes that wish to be notified when the pull gesture correctly
 * triggers a refresh should implement this interface.
 */
public interface OnPullToRefresh{
    public void onRefresh();
}

/**
 * Classes that wish to be notified when the drag gesture correctly
 * triggers a load should implement this interface.
 */
public interface OnDragToLoad{
    public void onLoad();
}}
</code></pre>