android基于PullToRefreshListView 的支持嵌套滚动的下拉刷新实现--PullToRefreshAttacher

支持嵌套滚动的下拉刷新实现

  • 相信大家都使用过PullToRefreshListView, 这个作者没有更新了5年的上古神器,至今如果使用ListView 的话仍然可以愉快的使用,但是自从RecyclerView出现之后,ListView 在我们代码里面出现的频率越来越低,甚至一个项目里面的所有布局都使用了RecyclerView实现,此时原来的PullToRefreshListView将失去它的作用,现在大多数项目会使用Google support 里的SwipeRefreshLayout 来进行下拉刷新的操作,使用过它的人多数都应该知道,其实我们还是需要做一些重写处理才能很好的进行使用;那么话说回来,如果我们想使用PullToRefreshListView 这个框架去接入RecyclerView 的上拉和下拉呢?其实上也是非常好扩展的,网上已经有很多的例子。但是,网上的大多数Demo只是简单的按照PullToRefreshListView原本的框架去扩展实现了RecyclerView 的下拉刷新,很少有考虑了RecyclerView嵌套滚动这一个特性的,以至于我们在使用了嵌套滚动布局的时候,就会出现下拉刷新和嵌套滚动冲突的情况,我觉得这样不太合理,所以根据嵌套滚动的实现逻辑,重新去继承修改了PullToRefreshListView 的源码,重新封装了一个PullToRefreshAttacher,接下来我讲讲实现逻辑。
  • NestedScroll: android NestedScroll 的支持是从5.0开始的,只要您的SDK版本在5.0以上,翻看ScrollView 的源代码就能看到它已经支持了嵌套滚动。好在android 官方给我们提供了各种兼容库,这个ScrollView 嵌套滚动的低版本兼容的代码NestedScrollView就在V4包里。接下来我们来讲讲兼容包里的嵌套滚动的实现思路:
    1. v4包里有两个接口:NestedScrollingChild,它用来定义目标视图何时开始嵌套滚动和需要嵌套滚动的偏移量, 以及执行嵌套滚动的NestedScrollingParent,它接受来自NestedScrollingChild给定的需要嵌套滚动的参数去对需要滚动的目标执行坐标偏移,达到和目标视图一同滚动的效果。 如果您要从头实现一个自定义的可以嵌套滚动的视图,那么在您需要在目标视图扩展NestedScrollingChild以及目标视图的父布局扩展NestedScrollingParent,但好在v4包已经有帮我们实现过这两个接口的类,我们可以在布局里直接添加,如果您的一个滚动布局里需要支持嵌套滚动那么您只需要在最外层加上v4包里的CoordinatorLayout布局即可。
    2. NestedScrollingChildHelperNestedScrollingParentHelper: 看名字就能看出,这两个类的作用是帮助您去实现嵌套滚动的,接下来的实现里我们会看到它的身影
    3. NestedScrollingChildNestedScrollingParent:接下来我们来看看它到底需要我们去扩展实现那些方法,代码如下:

public interface NestedScrollingChild {

    public void setNestedScrollingEnabled(boolean enabled);

    public boolean isNestedScrollingEnabled();

    public boolean startNestedScroll(int axes);

    public void stopNestedScroll();

    public boolean hasNestedScrollingParent();

    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);


    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);


    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);


    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
public interface NestedScrollingParent {

    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    public void onStopNestedScroll(View target);

    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

    public int getNestedScrollAxes();
  • 代码分析: NestedScrollingChild的所有扩展方法都可以通过NestedScrollingChildHelper的方法去实现,大家可以去看下NestedScrollingChildHelper的代码,这里讲解几个方法,以下方法默认一一对应使用NestedScrollingChildHelper里的方法, 而NestedScrollingParentHelper主要承担一些状态记录的,相对简单,我们这里把NestedScrollingChildNestedScrollingParent的方法一一对应起来分析:

    1. public void setNestedScrollingEnabled(boolean enabled);顾名思义,要实现嵌套滚动,这一步是必须的,对应 isNestedScrollingEnabled()

    2. public boolean startNestedScroll(int axes);public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);来看下NestedScrollingChildHelper里如何实现这个方法:

public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

我们可以看到,如果找到了NestedScrollingParent 那么将回调 parent 里的 onStartNestedScrollonNestedScrollAccepted,parent在这里可以做一些滚动开始之前的初始化处理,然后返回true;若没找到parent则返回false, 这个一般在手指按下的事件里调用,表示开始支持嵌套滚动啦,如果这里返回的是false,那么接下来的方法将不会起作用,因为没有NestedScrollingParent支持。
 
   3. public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);: 重点来了,这个方法实现稍稍复杂,就不贴代码了, 它围绕NestedScrollingChildNestedScrollParent展开,先来看下dispatchNestedPreScroll官方的注释:

Dispatch one step of a nested scroll in progress before this view consumes any portion of it.
*
*

Nested pre-scroll events are to nested scroll events what touch intercept is to touch.
* dispatchNestedPreScroll offers an opportunity for the parent view in a nested
* scrolling operation to consume some or all of the scroll operation before the child view
* consumes it.


*
* @param dx Horizontal scroll distance in pixels
* @param dy Vertical scroll distance in pixels
* @param consumed Output. If not null, consumed[0] will contain the consumed component of dx
* and consumed[1] the consumed dy.
* @param offsetInWindow Optional. If not null, on return this will contain the offset
* in local view coordinates of this view from before this operation
* to after it completes. View implementations may use this to adjust
* expected input coordinate tracking.
* @return true if the parent consumed some or all of the scroll delta
* @see #dispatchNestedScroll(int, int, int, int, int[])

  接下来我们解释它的作用:当目标视图,比如一个RecyclerView 或者 NestedScrollView 要进行滚动之前,目标视图会先调用此方法,然后通过NestedScrollingChildHelper 调用到NestedScrollingParentonNestedPreScroll方法,此时父布局会根剧传入的偏移量还有其子view的LayoutParams 进行相应的处理,然后把消耗的偏移量放入 consumed数组,目标视图将会根据consumed去修正偏移量,然后再去滚动自己。
  
   4. public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed)
:同样先来看下 NestedScrollingChild里的描述:

/**
* Dispatch one step of a nested scroll in progress.
*
*

Implementations of views that support nested scrolling should call this to report
* info about a scroll in progress to the current nested scrolling parent. If a nested scroll
* is not currently in progress or nested scrolling is not
* {@link #isNestedScrollingEnabled() enabled} for this view this method does nothing.
* Compatible View implementations should also call
* {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} before
* consuming a component of the scroll event themselves.
*
* @param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step
* @param dyConsumed Vertical distance in pixels consumed by this view during this scroll step
* @param dxUnconsumed Horizontal scroll distance in pixels not consumed by this view
* @param dyUnconsumed Horizontal scroll distance in pixels not consumed by this view
* @param offsetInWindow Optional. If not null, on return this will contain the offset
* in local view coordinates of this view from before this operation
* to after it completes. View implementations may use this to adjust
* expected input coordinate tracking.
* @return true if the event was dispatched, false if it could not be dispatched.
* @see #dispatchNestedPreScroll(int, int, int[], int[])
*/

解释:dispatchNestedScroll将会在目标视图(如RecyclerView)滚动完之后调用,此时目标视图将会把已经消耗和未消耗的偏移量传入方法,此时将会在NestedScrollingParent里产生onNestedScroll回调,parent根据传入的已消耗和未消耗的偏移量进行嵌套滚动,将最终偏移的结果传入offsetInWindow,目标视图通过offsetInWindow进行偏移量修正。
  
   5. public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed)public boolean onNestedPreFling(View target, float velocityX, float velocityY): fling操作发生在,手指移动后抬起的事件上,此时视图将继续滚动,velocityXvelocityY表示此时继续滚动的速度,一般会配合 VelocityTracker使用,传入一个速度,接下来的操作和dispatchNestedPreScroll类似。
  
   6. public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)
public boolean onNestedPreFling(View target, float velocityX, float velocityY):参照第


 
2. 下拉刷新实现
* 有了上面的分析,我们知道,如果是支持嵌套滚动的滚动视图,那么就会实现NestedScrollingChild方法,用来通知NestedScrollingParent如何滚动,那么我们沿着这个思路,我们想要知道何时开始下拉刷新或者上拉加载,只需要扩展NestedScrollingParent接口,从public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed)
监听里,我们就能知道他的消耗情况,如果dxUnconsumed 或者 dyUnconsumed 不为0 ,说明还没有消耗完,此时我们再扩展NestedScrollingChild 接口, 通过NestedScrollingChildHelper去执行public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)
方法去调用真正的NestedScrollingParent进行preScroll操作,之后减去通过真正parent消耗的offsetInWindow偏移量,此时如果uncounsumed还不为0.则说明,此时嵌套滚动完成,这部分偏移量已经不会继续使用,那么我们就可以通过unconsumed 去进行下拉刷新或者上拉加载(Unconsumed < 0 , 为手指向下移动)
* 如果此时下拉刷新或者上拉加载界面已经展示,此时我们手指回滚,需要收起这个界面,那么我们在NestedScrollingParent接口的public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)里实现,根据上面分析我们知道,此方法调用时,目标视图还没开始滚动,所以如果此时下拉或者上拉已经执行,那么我们根据其dx 或者 dy偏移量把上拉 / 下拉 出现的视图进行偏移,之后进行偏移量修正,通过NestedScrollingChild调用public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)方法对真正的NestedScrollingParent进行嵌套滚动执行
* 当NestedScrollingParentpublic void onStopNestedScroll(View target)方法回调执行时,说明此时嵌套滚动停止,此时手指是抬起状态,那么我们调用下拉刷新框架的setState方法处理刷新或者收起操作。
* 由于在stop时已经处理过下拉/上拉的刷新或者收起操作,我们不处理fling操作,直接实现NestedScrollingParent的fling回调通过NestedScrollingChild的fling操作给真正的NestedScrollingParent进行嵌套滚动


     
3. 文字描述不够有力量,上核心代码,所有代码请参考:PullToRefreshAttacher:

 /*******************************************************************************
 * Copyright 2011, 2012 Chris Banes.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package ptra.hacc.cc.ptr;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.IntDef;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import ptra.hacc.cc.ptr.internal.FlipLoadingLayout;
import ptra.hacc.cc.ptr.internal.LoadingLayout;
import ptra.hacc.cc.ptr.internal.RotateLoadingLayout;
import ptra.hacc.cc.ptr.internal.Utils;
import ptra.hacc.cc.ptr.internal.ViewCompat;


public abstract class PullToRefreshBase<T extends View> extends LinearLayout implements IPullToRefresh<T>{

    protected static final int EXTRA_START = 0x1;
    protected static final int EXTRA_BEFORE_PULL = 0x2;
    protected static final int EXTRA_AFTER_PULL = 0x3;

//  @IntDef({EXTRA_START, EXTRA_BEFORE_PULL, EXTRA_AFTER_PULL})
//  @Retention(RetentionPolicy.SOURCE)
//  protected  @interface EXTRA_STATE{}


    // ===========================================================
    // Constants
    // ===========================================================

    static final boolean DEBUG = true;

    static final boolean USE_HW_LAYERS = false;

    static final String LOG_TAG = "PullToRefresh";

    protected static final float FRICTION = 2.0f;

    public static final int SMOOTH_SCROLL_DURATION_MS = 200;
    public static final int SMOOTH_SCROLL_LONG_DURATION_MS = 325;
    static final int DEMO_SCROLL_INTERVAL = 225;

    static final String STATE_STATE = "ptr_state";
    static final String STATE_MODE = "ptr_mode";
    static final String STATE_CURRENT_MODE = "ptr_current_mode";
    static final String STATE_SCROLLING_REFRESHING_ENABLED = "ptr_disable_scrolling";
    static final String STATE_SHOW_REFRESHING_VIEW = "ptr_show_refreshing_view";
    static final String STATE_SUPER = "ptr_super";

    // ===========================================================
    // Fields
    // ===========================================================

    private int mTouchSlop;
    private float mLastMotionX, mLastMotionY;
    private float mInitialMotionX, mInitialMotionY;

    private boolean mIsBeingDragged = false;
    private State mState = State.RESET;
    private PullWay mPullWay = PullWay.getDefault();
    private Mode mMode = Mode.getDefault();

    Mode mCurrentMode;
    T mRefreshableView;
    private FrameLayout mRefreshableViewWrapper;

    private boolean mShowViewWhileRefreshing = true;
    private boolean mScrollingWhileRefreshingEnabled = false;
    private boolean mFilterTouchEvents = true;
    private boolean mOverScrollEnabled = true;
    private boolean mLayoutVisibilityChangesEnabled = true;

    private Interpolator mScrollAnimationInterpolator;
    private AnimationStyle mLoadingAnimationStyle = AnimationStyle.getDefault();

    private LoadingLayout mHeaderLayout;
    private LoadingLayout mFooterLayout;

    OnRefreshListener mOnRefreshListener;
    OnRefreshListener2 mOnRefreshListener2;
    private OnPullEventListener mOnPullEventListener;

    private SmoothScrollRunnable mCurrentSmoothScrollRunnable;

//  /**
//   * editor by Hale Yang
//   * mScrollingChildHelper
//   */
//  private NestedScrollingChildHelper mScrollingChildHelper;

    // ===========================================================
    // Constructors
    // ===========================================================

    public PullToRefreshBase(Context context) {
        super(context);
        init(context, null);
    }

    public PullToRefreshBase(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public PullToRefreshBase(Context context, Mode mode) {
        super(context);
        mMode = mode;
        init(context, null);
    }

    public PullToRefreshBase(Context context, Mode mode, AnimationStyle animStyle) {
        super(context);
        mMode = mode;
        mLoadingAnimationStyle = animStyle;
        init(context, null);
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        if (DEBUG) {
            Log.d(LOG_TAG, "addView: " + child.getClass().getSimpleName());
        }

        final T refreshableView = getRefreshableView();

        if (refreshableView instanceof ViewGroup) {
            ((ViewGroup) refreshableView).addView(child, index, params);
        } else {
            throw new UnsupportedOperationException("Refreshable View is not a ViewGroup so can't addView");
        }
    }

    @Override
    public final boolean demo() {
        if (mMode.showHeaderLoadingLayout() && isReadyForPullStart()) {
            smoothScrollToAndBack(-getHeaderSize() * 2);
            return true;
        } else if (mMode.showFooterLoadingLayout() && isReadyForPullEnd()) {
            smoothScrollToAndBack(getFooterSize() * 2);
            return true;
        }

        return false;
    }

    @Override
    public final Mode getCurrentMode() {
        return mCurrentMode;
    }

    @Override
    public final boolean getFilterTouchEvents() {
        return mFilterTouchEvents;
    }

    @Override
    public final ILoadingLayout getLoadingLayoutProxy() {
        return getLoadingLayoutProxy(true, true);
    }

    @Override
    public final ILoadingLayout getLoadingLayoutProxy(boolean includeStart, boolean includeEnd) {
        return createLoadingLayoutProxy(includeStart, includeEnd);
    }

    @Override
    public final Mode getMode() {
        return mMode;
    }

    @Override
    public final T getRefreshableView() {
        return mRefreshableView;
    }

    @Override
    public final boolean getShowViewWhileRefreshing() {
        return mShowViewWhileRefreshing;
    }

    @Override
    public final State getState() {
        return mState;
    }

    /**
     * @deprecated See {@link #isScrollingWhileRefreshingEnabled()}.
     */
    public final boolean isDisableScrollingWhileRefreshing() {
        return !isScrollingWhileRefreshingEnabled();
    }

    @Override
    public final boolean isPullToRefreshEnabled() {
        return mMode.permitsPullToRefresh();
    }

    @Override
    public final boolean isPullToRefreshOverScrollEnabled() {
        return VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD && mOverScrollEnabled
                && OverscrollHelper.isAndroidOverScrollEnabled(mRefreshableView);
    }

    @Override
    public final boolean isRefreshing() {
        return mState == State.REFRESHING || mState == State.MANUAL_REFRESHING;
    }

    @Override
    public final boolean isScrollingWhileRefreshingEnabled() {
        return mScrollingWhileRefreshingEnabled;
    }

    @Override
    public  boolean onInterceptTouchEvent(MotionEvent event) {
        boolean extraIntercept = false;

        if (!isPullToRefreshEnabled()) {
            return false;
        }

        final int action = event.getAction();

        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            mIsBeingDragged = false;
            return false;
        }

        if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {
            return true;
        }

        switch (action) {

            case MotionEvent.ACTION_MOVE: {
                // If we're refreshing, and the flag is set. Eat all MOVE events
                if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
                    return true;
                }

                if (isReadyForPull()) {
//                  extraIntercept = onExtraInterceptMove();
                    final float y = event.getY(), x = event.getX();
                    final float diff, oppositeDiff, absDiff;

                    // We need to use the correct values, based on scroll
                    // direction
                    switch (getPullToRefreshScrollDirection()) {
                        case HORIZONTAL:
                            diff = x - mLastMotionX;
                            oppositeDiff = y - mLastMotionY;
                            break;
                        case VERTICAL:
                        default:
                            diff = y - mLastMotionY;
                            oppositeDiff = x - mLastMotionX;
                            break;
                    }
                    absDiff = Math.abs(diff);

                    if (absDiff > mTouchSlop && (!mFilterTouchEvents || absDiff > Math.abs(oppositeDiff))) {
                        if (mMode.showHeaderLoadingLayout() && diff >= 1f && isReadyForPullStart()) {
                            mInitialMotionY = mLastMotionY = y;
                            mInitialMotionX = mLastMotionX = x;
                            mIsBeingDragged = true;
                            if (mMode == Mode.BOTH) {
                                mCurrentMode = Mode.PULL_FROM_START;
                            }
                        } else if (mMode.showFooterLoadingLayout() && diff <= -1f && isReadyForPullEnd()) {
                            mInitialMotionY = mLastMotionY = y;
                            mInitialMotionX = mLastMotionX = x;
                            mIsBeingDragged = true;
                            if (mMode == Mode.BOTH) {
                                mCurrentMode = Mode.PULL_FROM_END;
                            }
                        }
                    }
                }
                break;
            }
            case MotionEvent.ACTION_DOWN: {
                if (isReadyForPull()) {
                    mLastMotionY = mInitialMotionY = event.getY();
                    mLastMotionX = mInitialMotionX = event.getX();
                    mIsBeingDragged = false;
                }
                break;
            }
        }

        return mIsBeingDragged/* || extraIntercept*/;
    }

    @Override
    public final void onRefreshComplete() {
        if (isRefreshing()) {
            setState(State.RESET);
        }
    }

    @Override
    public final boolean onTouchEvent(MotionEvent event) {

        if (!isPullToRefreshEnabled()) {
            return false;
        }

        // If we're refreshing, and the flag is set. Eat the event
        if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
            return true;
        }

        if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) {
            return false;
        }

        MotionEvent etev = MotionEvent.obtain(event);

        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE: {
                //修复bug:如果子view没有拦截事件,则dispatchTouchEvent 将会把事件直接下发到此方法而不触发拦截
                //update by Hale Yang
                if (mIsBeingDragged || onInterceptTouchEvent(event)) {
                    int[] consumed = new int[2];
                    int[] offset = new int[2];
                    float dx =  mLastMotionX - event.getX();
                    float dy =  mLastMotionY - event.getY();
                    if(onExtractMoveEvent(dx, dy, consumed, offset, etev)){
                        etev.offsetLocation(-offset[0], -offset[1]);
                        return  true;
                    }
                    mLastMotionY = event.getY();
                    mLastMotionX = event.getX();
                    pullEvent();
                    return true;
                }
                break;
            }

            case MotionEvent.ACTION_DOWN: {
//              //editor by Hale Yang
//              //nested scroll
//              int axes = getPullToRefreshScrollDirection() == Orientation.VERTICAL ? android.support.v4.view.ViewCompat.SCROLL_AXIS_VERTICAL : android.support.v4.view.ViewCompat.SCROLL_AXIS_HORIZONTAL;
//              startNestedScroll(axes);
                if (isReadyForPull()) {
                    mLastMotionY = mInitialMotionY = event.getY();
                    mLastMotionX = mInitialMotionX = event.getX();
                    return true;
                }
                break;
            }

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP: {
                if (mIsBeingDragged) {
                    mIsBeingDragged = false;
                    onExtractUpEvent(etev);
                    if (mState == State.RELEASE_TO_REFRESH
                            && (null != mOnRefreshListener || null != mOnRefreshListener2)) {
                        setState(State.REFRESHING, true);
                        return true;
                    }

                    // If we're already refreshing, just scroll back to the top
                    if (isRefreshing()) {
                        smoothScrollTo(0);
                        return true;
                    }

                    // If we haven't returned by here, then we're not in a state
                    // to pull, so just reset
                    setState(State.RESET);

                    return true;
                }
                etev.recycle();
                break;
            }
        }

        return false;
    }

    public final void setScrollingWhileRefreshingEnabled(boolean allowScrollingWhileRefreshing) {
        mScrollingWhileRefreshingEnabled = allowScrollingWhileRefreshing;
    }

    /**
     * @deprecated See {@link #setScrollingWhileRefreshingEnabled(boolean)}
     */
    public void setDisableScrollingWhileRefreshing(boolean disableScrollingWhileRefreshing) {
        setScrollingWhileRefreshingEnabled(!disableScrollingWhileRefreshing);
    }

    @Override
    public final void setFilterTouchEvents(boolean filterEvents) {
        mFilterTouchEvents = filterEvents;
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy()}.
     */
    public void setLastUpdatedLabel(CharSequence label) {
        getLoadingLayoutProxy().setLastUpdatedLabel(label);
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy()}.
     */
    public void setLoadingDrawable(Drawable drawable) {
        getLoadingLayoutProxy().setLoadingDrawable(drawable);
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy(boolean, boolean)}.
     */
    public void setLoadingDrawable(Drawable drawable, Mode mode) {
        getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()).setLoadingDrawable(
                drawable);
    }

    @Override
    public void setLongClickable(boolean longClickable) {
        getRefreshableView().setLongClickable(longClickable);
    }

    /**
     * edit by Hale Yang
     * @param pullWay {@link PullWay}
     */
    public final void setPullWay(PullWay pullWay){
        this.mPullWay = pullWay;
    }

    @Override
    public final void setMode(Mode mode) {
        if (mode != mMode) {
            if (DEBUG) {
                Log.d(LOG_TAG, "Setting mode to: " + mode);
            }
            mMode = mode;
            updateUIForMode();
        }
    }

    public void setOnPullEventListener(OnPullEventListener listener) {
        mOnPullEventListener = listener;
    }

    @Override
    public final void setOnRefreshListener(OnRefreshListener listener) {
        mOnRefreshListener = listener;
        mOnRefreshListener2 = null;
    }

    @Override
    public final void setOnRefreshListener(OnRefreshListener2 listener) {
        mOnRefreshListener2 = listener;
        mOnRefreshListener = null;
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy()}.
     */
    public void setPullLabel(CharSequence pullLabel) {
        getLoadingLayoutProxy().setPullLabel(pullLabel);
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy(boolean, boolean)}.
     */
    public void setPullLabel(CharSequence pullLabel, Mode mode) {
        getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()).setPullLabel(pullLabel);
    }

    /**
     * @param enable Whether Pull-To-Refresh should be used
     * @deprecated This simple calls setMode with an appropriate mode based on
     *             the passed value.
     */
    public final void setPullToRefreshEnabled(boolean enable) {
        setMode(enable ? Mode.getDefault() : Mode.DISABLED);
    }

    @Override
    public final void setPullToRefreshOverScrollEnabled(boolean enabled) {
        mOverScrollEnabled = enabled;
    }

    @Override
    public final void setRefreshing() {
        setRefreshing(true);
    }

    @Override
    public final void setRefreshing(boolean doScroll) {
        if (!isRefreshing()) {
            setState(State.MANUAL_REFRESHING, doScroll);
        }
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy()}.
     */
    public void setRefreshingLabel(CharSequence refreshingLabel) {
        getLoadingLayoutProxy().setRefreshingLabel(refreshingLabel);
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy(boolean, boolean)}.
     */
    public void setRefreshingLabel(CharSequence refreshingLabel, Mode mode) {
        getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()).setRefreshingLabel(
                refreshingLabel);
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy()}.
     */
    public void setReleaseLabel(CharSequence releaseLabel) {
        setReleaseLabel(releaseLabel, Mode.BOTH);
    }

    /**
     * @deprecated You should now call this method on the result of
     *             {@link #getLoadingLayoutProxy(boolean, boolean)}.
     */
    public void setReleaseLabel(CharSequence releaseLabel, Mode mode) {
        getLoadingLayoutProxy(mode.showHeaderLoadingLayout(), mode.showFooterLoadingLayout()).setReleaseLabel(
                releaseLabel);
    }

    public void setScrollAnimationInterpolator(Interpolator interpolator) {
        mScrollAnimationInterpolator = interpolator;
    }

    @Override
    public final void setShowViewWhileRefreshing(boolean showView) {
        mShowViewWhileRefreshing = showView;
    }

    /**
     * @return Either {@link Orientation#VERTICAL} or
     *         {@link Orientation#HORIZONTAL} depending on the scroll direction.
     */
    public abstract Orientation getPullToRefreshScrollDirection();

    final void setState(State state, final boolean... params) {
        mState = state;
        if (DEBUG) {
            Log.d(LOG_TAG, "State: " + mState.name());
        }

        switch (mState) {
            case RESET:
                onReset();
                break;
            case PULL_TO_REFRESH:
                onPullToRefresh();
                break;
            case RELEASE_TO_REFRESH:
                onReleaseToRefresh();
                break;
            case REFRESHING:
            case MANUAL_REFRESHING:
                onRefreshing(params[0]);
                break;
            case OVERSCROLLING:
                // NO-OP
                break;
        }

        // Call OnPullEventListener
        if (null != mOnPullEventListener) {
            mOnPullEventListener.onPullEvent(this, mState, mCurrentMode);
        }
    }

    /**
     * Used internally for adding view. Need because we override addView to
     * pass-through to the Refreshable View
     */
    protected final void addViewInternal(View child, int index, ViewGroup.LayoutParams params) {
        super.addView(child, index, params);
    }

    /**
     * Used internally for adding view. Need because we override addView to
     * pass-through to the Refreshable View
     */
    protected final void addViewInternal(View child, ViewGroup.LayoutParams params) {
        super.addView(child, -1, params);
    }

    protected LoadingLayout createLoadingLayout(Context context, Mode mode, TypedArray attrs) {
        LoadingLayout layout = mLoadingAnimationStyle.createLoadingLayout(context, mode,
                getPullToRefreshScrollDirection(), attrs);
        layout.setVisibility(View.INVISIBLE);
        return layout;
    }

    /**
     * Used internally for {@link #getLoadingLayoutProxy(boolean, boolean)}.
     * Allows derivative classes to include any extra LoadingLayouts.
     */
    protected LoadingLayoutProxy createLoadingLayoutProxy(final boolean includeStart, final boolean includeEnd) {
        LoadingLayoutProxy proxy = new LoadingLayoutProxy();

        if (includeStart && mMode.showHeaderLoadingLayout()) {
            proxy.addLayout(mHeaderLayout);
        }
        if (includeEnd && mMode.showFooterLoadingLayout()) {
            proxy.addLayout(mFooterLayout);
        }

        return proxy;
    }

    /**
     * This is implemented by derived classes to return the created View. If you
     * need to use a custom View (such as a custom ListView), override this
     * method and return an instance of your custom class.
     * 

* Be sure to set the ID of the view in this method, especially if you're * using a ListActivity or ListFragment. * * @param context Context to create view with * @param attrs AttributeSet from wrapped class. Means that anything you * include in the XML layout declaration will be routed to the * created View * @return New instance of the Refreshable View */ protected abstract T createRefreshableView(Context context, AttributeSet attrs); protected final void disableLoadingLayoutVisibilityChanges() { mLayoutVisibilityChangesEnabled = false; } protected final LoadingLayout getFooterLayout() { return mFooterLayout; } protected final int getFooterSize() { return mFooterLayout.getContentSize(); } protected final LoadingLayout getHeaderLayout() { return mHeaderLayout; } protected final int getHeaderSize() { return mHeaderLayout.getContentSize(); } protected int getPullToRefreshScrollDuration() { return SMOOTH_SCROLL_DURATION_MS; } protected int getPullToRefreshScrollDurationLonger() { return SMOOTH_SCROLL_LONG_DURATION_MS; } protected FrameLayout getRefreshableViewWrapper() { return mRefreshableViewWrapper; } /** * Allows Derivative classes to handle the XML Attrs without creating a * TypedArray themsevles * * @param a - TypedArray of PullToRefresh Attributes */ protected void handleStyledAttributes(TypedArray a) { } /** * Implemented by derived class to return whether the View is in a state * where the user can Pull to Refresh by scrolling from the end. * * @return true if the View is currently in the correct state (for example, * bottom of a ListView) */ protected abstract boolean isReadyForPullEnd(); /** * Implemented by derived class to return whether the View is in a state * where the user can Pull to Refresh by scrolling from the start. * * @return true if the View is currently the correct state (for example, top * of a ListView) */ protected abstract boolean isReadyForPullStart(); /** * Called by {@link #onRestoreInstanceState(Parcelable)} so that derivative * classes can handle their saved instance state. * * @param savedInstanceState - Bundle which contains saved instance state. */ protected void onPtrRestoreInstanceState(Bundle savedInstanceState) { } /** * Called by {@link #onSaveInstanceState()} so that derivative classes can * save their instance state. * * @param saveState - Bundle to be updated with saved state. */ protected void onPtrSaveInstanceState(Bundle saveState) { } /** * Called when the UI has been to be updated to be in the * {@link State#PULL_TO_REFRESH} state. */ protected void onPullToRefresh() { switch (mCurrentMode) { case PULL_FROM_END: mFooterLayout.pullToRefresh(); break; case PULL_FROM_START: mHeaderLayout.pullToRefresh(); break; default: // NO-OP break; } } /** * update by Hale Yang * @param consumed save the consumed position 0 is x , 1 is y * @param offset save the offset * @return if the return is true that we can begin pull to refreshing, otherwise we can't pull it */ protected boolean onExtractMoveEvent(float dx, float dy, int[] consumed, int[] offset, MotionEvent event){ return false; } protected boolean onExtractUpEvent(MotionEvent event){ return true; } // /** // * editor by Hale Yang // * @return if it return true, then the gesture of move will be intercept // */ // protected boolean onExtraInterceptMove(){ // return false; // } /** * Called when the UI has been to be updated to be in the * {@link State#REFRESHING} or {@link State#MANUAL_REFRESHING} state. * * @param doScroll - Whether the UI should scroll for this event. */ protected void onRefreshing(final boolean doScroll) { if (mMode.showHeaderLoadingLayout()) { mHeaderLayout.refreshing(); } if (mMode.showFooterLoadingLayout()) { mFooterLayout.refreshing(); } if (doScroll) { if (mShowViewWhileRefreshing) { // Call Refresh Listener when the Scroll has finished OnSmoothScrollFinishedListener listener = new OnSmoothScrollFinishedListener() { @Override public void onSmoothScrollFinished() { callRefreshListener(); } }; switch (mCurrentMode) { case MANUAL_REFRESH_ONLY: case PULL_FROM_END: smoothScrollTo(getFooterSize(), listener); break; default: case PULL_FROM_START: smoothScrollTo(-getHeaderSize(), listener); break; } } else { smoothScrollTo(0); } } else { // We're not scrolling, so just call Refresh Listener now callRefreshListener(); } } /** * Called when the UI has been to be updated to be in the * {@link State#RELEASE_TO_REFRESH} state. */ protected void onReleaseToRefresh() { switch (mCurrentMode) { case PULL_FROM_END: mFooterLayout.releaseToRefresh(); break; case PULL_FROM_START: mHeaderLayout.releaseToRefresh(); break; default: // NO-OP break; } } /** * Called when the UI has been to be updated to be in the * {@link State#RESET} state. */ protected void onReset() { mIsBeingDragged = false; mLayoutVisibilityChangesEnabled = true; // Always reset both layouts, just in case... mHeaderLayout.reset(); mFooterLayout.reset(); smoothScrollTo(0); } @Override protected final void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; setMode(Mode.mapIntToValue(bundle.getInt(STATE_MODE, 0))); mCurrentMode = Mode.mapIntToValue(bundle.getInt(STATE_CURRENT_MODE, 0)); mScrollingWhileRefreshingEnabled = bundle.getBoolean(STATE_SCROLLING_REFRESHING_ENABLED, false); mShowViewWhileRefreshing = bundle.getBoolean(STATE_SHOW_REFRESHING_VIEW, true); // Let super Restore Itself super.onRestoreInstanceState(bundle.getParcelable(STATE_SUPER)); State viewState = State.mapIntToValue(bundle.getInt(STATE_STATE, 0)); if (viewState == State.REFRESHING || viewState == State.MANUAL_REFRESHING) { setState(viewState, true); } // Now let derivative classes restore their state onPtrRestoreInstanceState(bundle); return; } super.onRestoreInstanceState(state); } @Override protected final Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); // Let derivative classes get a chance to save state first, that way we // can make sure they don't overrite any of our values onPtrSaveInstanceState(bundle); bundle.putInt(STATE_STATE, mState.getIntValue()); bundle.putInt(STATE_MODE, mMode.getIntValue()); bundle.putInt(STATE_CURRENT_MODE, mCurrentMode.getIntValue()); bundle.putBoolean(STATE_SCROLLING_REFRESHING_ENABLED, mScrollingWhileRefreshingEnabled); bundle.putBoolean(STATE_SHOW_REFRESHING_VIEW, mShowViewWhileRefreshing); bundle.putParcelable(STATE_SUPER, super.onSaveInstanceState()); return bundle; } @Override protected final void onSizeChanged(int w, int h, int oldw, int oldh) { if (DEBUG) { Log.d(LOG_TAG, String.format("onSizeChanged. W: %d, H: %d", w, h)); } super.onSizeChanged(w, h, oldw, oldh); // We need to update the header/footer when our size changes refreshLoadingViewsSize(); // Update the Refreshable View layout refreshRefreshableViewSize(w, h); /** * As we're currently in a Layout Pass, we need to schedule another one * to layout any changes we've made here */ post(new Runnable() { @Override public void run() { requestLayout(); } }); } /** * Re-measure the Loading Views height, and adjust internal padding as * necessary */ protected final void refreshLoadingViewsSize() { final int maximumPullScroll = (int) (getMaximumPullScroll() * 1.2f); int pLeft = getPaddingLeft(); int pTop = getPaddingTop(); int pRight = getPaddingRight(); int pBottom = getPaddingBottom(); switch (getPullToRefreshScrollDirection()) { case HORIZONTAL: if (mMode.showHeaderLoadingLayout()) { mHeaderLayout.setWidth(maximumPullScroll); pLeft = -maximumPullScroll; } else { pLeft = 0; } if (mMode.showFooterLoadingLayout()) { mFooterLayout.setWidth(maximumPullScroll); pRight = -maximumPullScroll; } else { pRight = 0; } break; case VERTICAL: if (mMode.showHeaderLoadingLayout()) { mHeaderLayout.setHeight(maximumPullScroll); pTop = -maximumPullScroll; } else { pTop = 0; } if (mMode.showFooterLoadingLayout()) { mFooterLayout.setHeight(maximumPullScroll); pBottom = -maximumPullScroll; } else { pBottom = 0; } break; } if (DEBUG) { Log.d(LOG_TAG, String.format("Setting Padding. L: %d, T: %d, R: %d, B: %d", pLeft, pTop, pRight, pBottom)); } setPadding(pLeft, pTop, pRight, pBottom); } protected final void refreshRefreshableViewSize(int width, int height) { // We need to set the Height of the Refreshable View to the same as // this layout LayoutParams lp = (LayoutParams) mRefreshableViewWrapper.getLayoutParams(); switch (getPullToRefreshScrollDirection()) { case HORIZONTAL: if (lp.width != width) { lp.width = width; mRefreshableViewWrapper.requestLayout(); } break; case VERTICAL: if (lp.height != height) { lp.height = height; mRefreshableViewWrapper.requestLayout(); } break; } } /** * Helper method which just calls scrollTo() in the correct scrolling * direction. * * @param value - New Scroll value */ protected final void setHeaderScroll(int value) { if (DEBUG) { Log.d(LOG_TAG, "setHeaderScroll: " + value); } // Clamp value to with pull scroll range final int maximumPullScroll = getMaximumPullScroll(); value = Math.min(maximumPullScroll, Math.max(-maximumPullScroll, value)); if (mLayoutVisibilityChangesEnabled) { if (value < 0) { mHeaderLayout.setVisibility(View.VISIBLE); } else if (value > 0) { mFooterLayout.setVisibility(View.VISIBLE); } else { mHeaderLayout.setVisibility(View.INVISIBLE); mFooterLayout.setVisibility(View.INVISIBLE); } } if (USE_HW_LAYERS) { /** * Use a Hardware Layer on the Refreshable View if we've scrolled at * all. We don't use them on the Header/Footer Views as they change * often, which would negate any HW layer performance boost. */ ViewCompat.setLayerType(mRefreshableViewWrapper, value != 0 ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE); } switch (getPullToRefreshScrollDirection()) { case VERTICAL: scrollTo(0, value); break; case HORIZONTAL: scrollTo(value, 0); break; } } /** * Smooth Scroll to position using the default duration of * {@value #SMOOTH_SCROLL_DURATION_MS} ms. * * @param scrollValue - Position to scroll to */ protected final void smoothScrollTo(int scrollValue) { smoothScrollTo(scrollValue, getPullToRefreshScrollDuration()); } /** * Smooth Scroll to position using the default duration of * {@value #SMOOTH_SCROLL_DURATION_MS} ms. * * @param scrollValue - Position to scroll to * @param listener - Listener for scroll */ protected final void smoothScrollTo(int scrollValue, OnSmoothScrollFinishedListener listener) { smoothScrollTo(scrollValue, getPullToRefreshScrollDuration(), 0, listener); } /** * Smooth Scroll to position using the longer default duration of * {@value #SMOOTH_SCROLL_LONG_DURATION_MS} ms. * * @param scrollValue - Position to scroll to */ protected final void smoothScrollToLonger(int scrollValue) { smoothScrollTo(scrollValue, getPullToRefreshScrollDurationLonger()); } /** * Updates the View State when the mode has been set. This does not do any * checking that the mode is different to current state so always updates. */ protected void updateUIForMode() { // We need to use the correct LayoutParam values, based on scroll // direction final LayoutParams lp = getLoadingLayoutLayoutParams(); // Remove Header, and then add Header Loading View again if needed if (this == mHeaderLayout.getParent()) { removeView(mHeaderLayout); } if (mMode.showHeaderLoadingLayout()) { addViewInternal(mHeaderLayout, 0, lp); } // Remove Footer, and then add Footer Loading View again if needed if (this == mFooterLayout.getParent()) { removeView(mFooterLayout); } if (mMode.showFooterLoadingLayout()) { addViewInternal(mFooterLayout, lp); } // Hide Loading Views refreshLoadingViewsSize(); // If we're not using Mode.BOTH, set mCurrentMode to mMode, otherwise // set it to pull down mCurrentMode = (mMode != Mode.BOTH) ? mMode : Mode.PULL_FROM_START; } private void addRefreshableView(Context context, T refreshableView) { mRefreshableViewWrapper = new FrameLayout(context); mRefreshableViewWrapper.addView(refreshableView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); addViewInternal(mRefreshableViewWrapper, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } private void callRefreshListener() { if (null != mOnRefreshListener) { mOnRefreshListener.onRefresh(this); } else if (null != mOnRefreshListener2) { if (mCurrentMode == Mode.PULL_FROM_START) { mOnRefreshListener2.onPullDownToRefresh(this); } else if (mCurrentMode == Mode.PULL_FROM_END) { mOnRefreshListener2.onPullUpToRefresh(this); } } } @SuppressWarnings("deprecation") private void init(Context context, AttributeSet attrs) { // Refreshable View // By passing the attrs, we can add ListView/GridView params via XML mRefreshableView = createRefreshableView(context, attrs); addRefreshableView(context, mRefreshableView); if(getPullToRefreshScrollDirection() != null) { switch (getPullToRefreshScrollDirection()) { case HORIZONTAL: setOrientation(LinearLayout.HORIZONTAL); break; case VERTICAL: default: setOrientation(LinearLayout.VERTICAL); break; } } setGravity(Gravity.CENTER); ViewConfiguration config = ViewConfiguration.get(context); mTouchSlop = config.getScaledTouchSlop(); // Styleables from XML TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PullToRefresh); if (a.hasValue(R.styleable.PullToRefresh_ptrMode)) { mMode = Mode.mapIntToValue(a.getInteger(R.styleable.PullToRefresh_ptrMode, 0)); } if (a.hasValue(R.styleable.PullToRefresh_ptrAnimationStyle)) { mLoadingAnimationStyle = AnimationStyle.mapIntToValue(a.getInteger( R.styleable.PullToRefresh_ptrAnimationStyle, 0)); } // We need to create now layouts now mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a); mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a); /** * Styleables from XML */ if (a.hasValue(R.styleable.PullToRefresh_ptrRefreshableViewBackground)) { Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrRefreshableViewBackground); if (null != background) { mRefreshableView.setBackgroundDrawable(background); } } else if (a.hasValue(R.styleable.PullToRefresh_ptrAdapterViewBackground)) { Utils.warnDeprecation("ptrAdapterViewBackground", "ptrRefreshableViewBackground"); Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrAdapterViewBackground); if (null != background) { mRefreshableView.setBackgroundDrawable(background); } } if (a.hasValue(R.styleable.PullToRefresh_ptrOverScroll)) { mOverScrollEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrOverScroll, true); } if (a.hasValue(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled)) { mScrollingWhileRefreshingEnabled = a.getBoolean( R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled, false); } // Let the derivative classes have a go at handling attributes, then // recycle them... handleStyledAttributes(a); a.recycle(); // Finally update the UI for the modes updateUIForMode(); } private boolean isReadyForPull() { switch (mMode) { case PULL_FROM_START: return isReadyForPullStart(); case PULL_FROM_END: return isReadyForPullEnd(); case BOTH: return isReadyForPullEnd() || isReadyForPullStart(); default: return false; } } /** * Actions a Pull Event * * @return true if the Event has been handled, false if there has been no * change */ private void pullEvent() { final int newScrollValue; final int itemDimension; final float initialMotionValue, lastMotionValue; switch (getPullToRefreshScrollDirection()) { case HORIZONTAL: initialMotionValue = mInitialMotionX; lastMotionValue = mLastMotionX; break; case VERTICAL: default: initialMotionValue = mInitialMotionY; lastMotionValue = mLastMotionY; break; } switch (mCurrentMode) { case PULL_FROM_END: newScrollValue = Math.round(Math.max(initialMotionValue - lastMotionValue, 0) / FRICTION); itemDimension = getFooterSize(); break; case PULL_FROM_START: default: newScrollValue = Math.round(Math.min(initialMotionValue - lastMotionValue, 0) / FRICTION); itemDimension = getHeaderSize(); break; } setHeaderScroll(newScrollValue); onPullChange(newScrollValue, itemDimension); } /** * editor by Hale Yang * when the pull scroll changed, we will change the state with the scroll length and we * will judge how do the state change * @param newScrollValue new scroll Value * @param itemDimension the dimension of header or foot */ protected void onPullChange(int newScrollValue, int itemDimension){ if (newScrollValue != 0 && !isRefreshing()) { float scale = Math.abs(newScrollValue) / (float) itemDimension; boolean allowRelease = mPullWay == PullWay.WBOTH_PULL_ALLOW; switch (mCurrentMode) { case PULL_FROM_END: mFooterLayout.onPull(scale); if(!allowRelease) allowRelease = !(mPullWay == PullWay.WUP_PULL_ONLY || mPullWay == PullWay.WBOTH_PULL_ONLY); break; case PULL_FROM_START: if(!allowRelease) allowRelease = !(mPullWay == PullWay.WDOWN_PULL_ONLY || mPullWay == PullWay.WBOTH_PULL_ONLY); default: mHeaderLayout.onPull(scale); break; } if (mState != State.PULL_TO_REFRESH && itemDimension >= Math.abs(newScrollValue)) { setState(State.PULL_TO_REFRESH); } else if (mState == State.PULL_TO_REFRESH && itemDimension < Math.abs(newScrollValue) && allowRelease) { setState(State.RELEASE_TO_REFRESH); } } } private LayoutParams getLoadingLayoutLayoutParams() { switch (getPullToRefreshScrollDirection()) { case HORIZONTAL: return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); case VERTICAL: default: return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); } } private int getMaximumPullScroll() { switch (getPullToRefreshScrollDirection()) { case HORIZONTAL: return Math.round(getWidth() / FRICTION); case VERTICAL: default: return Math.round(getHeight() / FRICTION); } } /** * Smooth Scroll to position using the specific duration * * @param scrollValue - Position to scroll to * @param duration - Duration of animation in milliseconds */ private final void smoothScrollTo(int scrollValue, long duration) { smoothScrollTo(scrollValue, duration, 0, null); } private final void smoothScrollTo(int newScrollValue, long duration, long delayMillis, OnSmoothScrollFinishedListener listener) { if (null != mCurrentSmoothScrollRunnable) { mCurrentSmoothScrollRunnable.stop(); } final int oldScrollValue; switch (getPullToRefreshScrollDirection()) { case HORIZONTAL: oldScrollValue = getScrollX(); break; case VERTICAL: default: oldScrollValue = getScrollY(); break; } if (oldScrollValue != newScrollValue) { if (null == mScrollAnimationInterpolator) { // Default interpolator is a Decelerate Interpolator mScrollAnimationInterpolator = new DecelerateInterpolator(); } mCurrentSmoothScrollRunnable = new SmoothScrollRunnable(oldScrollValue, newScrollValue, duration, listener); if (delayMillis > 0) { postDelayed(mCurrentSmoothScrollRunnable, delayMillis); } else { post(mCurrentSmoothScrollRunnable); } } } private final void smoothScrollToAndBack(int y) { smoothScrollTo(y, SMOOTH_SCROLL_DURATION_MS, 0, new OnSmoothScrollFinishedListener() { @Override public void onSmoothScrollFinished() { smoothScrollTo(0, SMOOTH_SCROLL_DURATION_MS, DEMO_SCROLL_INTERVAL, null); } }); } public static enum AnimationStyle { /** * This is the default for Android-PullToRefresh. Allows you to use any * drawable, which is automatically rotated and used as a Progress Bar. */ ROTATE, /** * This is the old default, and what is commonly used on iOS. Uses an * arrow image which flips depending on where the user has scrolled. */ FLIP; static AnimationStyle getDefault() { return ROTATE; } /** * Maps an int to a specific mode. This is needed when saving state, or * inflating the view from XML where the mode is given through a attr * int. * * @param modeInt - int to map a Mode to * @return Mode that modeInt maps to, or ROTATE by default. */ static AnimationStyle mapIntToValue(int modeInt) { switch (modeInt) { case 0x0: default: return ROTATE; case 0x1: return FLIP; } } LoadingLayout createLoadingLayout(Context context, Mode mode, Orientation scrollDirection, TypedArray attrs) { switch (this) { case ROTATE: default: return new RotateLoadingLayout(context, mode, scrollDirection, attrs); case FLIP: return new FlipLoadingLayout(context, mode, scrollDirection, attrs); } } } /** * editor by Hale Yang * the enum's effect is how to pull */ public enum PullWay{ /** * this param's function is that when you pull down , then you can only pull down , * but however you pull , you can't see the release style, and you can't doing refreshing, * you just do pull, and your finger put up, the loading layout will going back */ WDOWN_PULL_ONLY(0x1), /** * when pull up , you just can pull, and nothing happen when you pull up that loading layout * will going back */ WUP_PULL_ONLY(0x2), WBOTH_PULL_ONLY(0x3), WBOTH_PULL_ALLOW(0x4); private int mIntValue; PullWay(int value){ this.mIntValue = value; } public int getIntValue(){ return mIntValue; } public static PullWay getDefault(){ return WBOTH_PULL_ALLOW; } } public static enum Mode { /** * Disable all Pull-to-Refresh gesture and Refreshing handling */ DISABLED(0x0), /** * Only allow the user to Pull from the start of the Refreshable View to * refresh. The start is either the Top or Left, depending on the * scrolling direction. */ PULL_FROM_START(0x1), /** * Only allow the user to Pull from the end of the Refreshable View to * refresh. The start is either the Bottom or Right, depending on the * scrolling direction. */ PULL_FROM_END(0x2), /** * Allow the user to both Pull from the start, from the end to refresh. */ BOTH(0x3), /** * Disables Pull-to-Refresh gesture handling, but allows manually * setting the Refresh state via * {@link PullToRefreshBase#setRefreshing() setRefreshing()}. */ MANUAL_REFRESH_ONLY(0x4); /** * @deprecated Use {@link #PULL_FROM_START} from now on. */ public static Mode PULL_DOWN_TO_REFRESH = Mode.PULL_FROM_START; /** * @deprecated Use {@link #PULL_FROM_END} from now on. */ public static Mode PULL_UP_TO_REFRESH = Mode.PULL_FROM_END; /** * Maps an int to a specific mode. This is needed when saving state, or * inflating the view from XML where the mode is given through a attr * int. * * @param modeInt - int to map a Mode to * @return Mode that modeInt maps to, or PULL_FROM_START by default. */ static Mode mapIntToValue(final int modeInt) { for (Mode value : Mode.values()) { if (modeInt == value.getIntValue()) { return value; } } // If not, return default return getDefault(); } static Mode getDefault() { return PULL_FROM_START; } private int mIntValue; // The modeInt values need to match those from attrs.xml Mode(int modeInt) { mIntValue = modeInt; } /** * @return true if the mode permits Pull-to-Refresh */ boolean permitsPullToRefresh() { return !(this == DISABLED || this == MANUAL_REFRESH_ONLY); } /** * @return true if this mode wants the Loading Layout Header to be shown */ public boolean showHeaderLoadingLayout() { return this == PULL_FROM_START || this == BOTH; } /** * @return true if this mode wants the Loading Layout Footer to be shown */ public boolean showFooterLoadingLayout() { return this == PULL_FROM_END || this == BOTH || this == MANUAL_REFRESH_ONLY; } int getIntValue() { return mIntValue; } } // =========================================================== // Inner, Anonymous Classes, and Enumerations // =========================================================== /** * Simple Listener that allows you to be notified when the user has scrolled * to the end of the AdapterView. See ( * {@link PullToRefreshAdapterViewBase#setOnLastItemVisibleListener}. * * @author Chris Banes */ public static interface OnLastItemVisibleListener { /** * Called when the user has scrolled to the end of the list */ public void onLastItemVisible(); } /** * Listener that allows you to be notified when the user has started or * finished a touch event. Useful when you want to append extra UI events * (such as sounds). See ( * {@link PullToRefreshAdapterViewBase#setOnPullEventListener}. * * @author Chris Banes */ public static interface OnPullEventListener<V extends View> { /** * Called when the internal state has been changed, usually by the user * pulling. * * @param refreshView - View which has had it's state change. * @param state - The new state of View. * @param direction - One of {@link Mode#PULL_FROM_START} or * {@link Mode#PULL_FROM_END} depending on which direction * the user is pulling. Only useful when state is * {@link State#PULL_TO_REFRESH} or * {@link State#RELEASE_TO_REFRESH}. */ public void onPullEvent(final PullToRefreshBase refreshView, State state, Mode direction); } /** * Simple Listener to listen for any callbacks to Refresh. * * @author Chris Banes */ public static interface OnRefreshListener<V extends View> { /** * onRefresh will be called for both a Pull from start, and Pull from * end */ public void onRefresh(final PullToRefreshBase refreshView); } /** * An advanced version of the Listener to listen for callbacks to Refresh. * This listener is different as it allows you to differentiate between Pull * Ups, and Pull Downs. * * @author Chris Banes */ public static interface OnRefreshListener2<V extends View> { // TODO These methods need renaming to START/END rather than DOWN/UP /** * onPullDownToRefresh will be called only when the user has Pulled from * the start, and released. */ public void onPullDownToRefresh(final PullToRefreshBase refreshView); /** * onPullUpToRefresh will be called only when the user has Pulled from * the end, and released. */ public void onPullUpToRefresh(final PullToRefreshBase refreshView); } public static enum Orientation { VERTICAL, HORIZONTAL; } public static enum State { /** * When the UI is in a state which means that user is not interacting * with the Pull-to-Refresh function. */ RESET(0x0), /** * When the UI is being pulled by the user, but has not been pulled far * enough so that it refreshes when released. */ PULL_TO_REFRESH(0x1), /** * When the UI is being pulled by the user, and has * been pulled far enough so that it will refresh when released. */ RELEASE_TO_REFRESH(0x2), /** * When the UI is currently refreshing, caused by a pull gesture. */ REFRESHING(0x8), /** * When the UI is currently refreshing, caused by a call to * {@link PullToRefreshBase#setRefreshing() setRefreshing()}. */ MANUAL_REFRESHING(0x9), /** * When the UI is currently overscrolling, caused by a fling on the * Refreshable View. */ OVERSCROLLING(0x10); /** * Maps an int to a specific state. This is needed when saving state. * * @param stateInt - int to map a State to * @return State that stateInt maps to */ static State mapIntToValue(final int stateInt) { for (State value : State.values()) { if (stateInt == value.getIntValue()) { return value; } } // If not, return default return RESET; } private int mIntValue; State(int intValue) { mIntValue = intValue; } int getIntValue() { return mIntValue; } } final class SmoothScrollRunnable implements Runnable { private final Interpolator mInterpolator; private final int mScrollToY; private final int mScrollFromY; private final long mDuration; private OnSmoothScrollFinishedListener mListener; private boolean mContinueRunning = true; private long mStartTime = -1; private int mCurrentY = -1; public SmoothScrollRunnable(int fromY, int toY, long duration, OnSmoothScrollFinishedListener listener) { mScrollFromY = fromY; mScrollToY = toY; mInterpolator = mScrollAnimationInterpolator; mDuration = duration; mListener = listener; } @Override public void run() { /** * Only set mStartTime if this is the first time we're starting, * else actually calculate the Y delta */ if (mStartTime == -1) { mStartTime = System.currentTimeMillis(); } else { /** * We do do all calculations in long to reduce software float * calculations. We use 1000 as it gives us good accuracy and * small rounding errors */ long normalizedTime = (1000 * (System.currentTimeMillis() - mStartTime)) / mDuration; normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0); final int deltaY = Math.round((mScrollFromY - mScrollToY) * mInterpolator.getInterpolation(normalizedTime / 1000f)); mCurrentY = mScrollFromY - deltaY; setHeaderScroll(mCurrentY); } // If we're not at the target Y, keep going... if (mContinueRunning && mScrollToY != mCurrentY) { ViewCompat.postOnAnimation(PullToRefreshBase.this, this); } else { if (null != mListener) { mListener.onSmoothScrollFinished(); } } } public void stop() { mContinueRunning = false; removeCallbacks(this); } } static interface OnSmoothScrollFinishedListener { void onSmoothScrollFinished(); } }

package ptra.hacc.cc.ptr;

import android.content.Context;
import android.support.annotation.Nullable;
import android.support.annotation.Size;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by Hale Yang on 2017/9/13.
 * This is an nested scroll pull to refresh view
 */

public abstract class PullNeoNestedToRefresh<E extends View> extends PullToRefreshBase<E> implements NestedScrollingChild, NestedScrollingParent {

    private NestedScrollingChildHelper mNestedScrollingChildHelper;
    private NestedScrollingParentHelper mNestedScorllingParentHelper;

    /*
     * 获取滚动后所消耗的距离
     */
    private int[] mParentConmused = new int[2];
    /*
     * 获取滚动消耗的偏移量
     */
    private int[] mParentOffsetWindow = new int[2];
    private int mTotalUnConmused;



    public PullNeoNestedToRefresh(Context context) {
        super(context);
        init(context);
    }

    public PullNeoNestedToRefresh(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public PullNeoNestedToRefresh(Context context, Mode mode) {
        super(context, mode);
        init(context);
    }

    public PullNeoNestedToRefresh(Context context, Mode mode, AnimationStyle animStyle) {
        super(context, mode, animStyle);
        init(context);
    }

    private void init(Context context){
        setNestedScrollingEnabled(true);
    }


    /*
    * 这个模式下,我们默认不拦截处理下拉手势,通过nested scroll 来处理下拉拖动和释放,如果这里重写了,会和nested scroll中的处理发生冲突,
    * 建议在没有嵌套滚动视图里再重写此方法
    * */
    @Override
    protected  boolean isReadyForPullStart() {
        return false;
    }

    @Override
    protected boolean isReadyForPullEnd() {
        return false;
    }

    /**
     *
     * @return if return false, you can't drag down to refresh
     */
    protected boolean isReadyForNestedScrollPullStart(){
        return true;
    }

    /**
     *
     * @return if return false, you can't drag up to refresh
     */
    protected boolean isReadyForNestedScrollPullEnd(){
        return true;
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return isEnabled() && !isRefreshing();
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int axes) {
        getNestedScrollingParentHelper().onNestedScrollAccepted(child, target, axes);
        startNestedScroll(axes);
    }

    @Override
    public void onStopNestedScroll(View child) {
        if(getState() == State.RELEASE_TO_REFRESH && (mOnRefreshListener != null || mOnRefreshListener2 != null)){
            setState(State.REFRESHING, true);
        }else if(getState() != State.RESET){
            setState(State.RESET);
        }
        mTotalUnConmused = 0;
        stopNestedScroll();
        getNestedScrollingParentHelper().onStopNestedScroll(child);
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {

        dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetWindow);
        if((dxUnconsumed < 0 || dyUnconsumed < 0)
                && getMode().showHeaderLoadingLayout()
                && isReadyForNestedScrollPullStart()) {
            mCurrentMode = Mode.PULL_FROM_START;
            switch (getPullToRefreshScrollDirection()) {
                case VERTICAL:
                    /*
                    * 滚动结束剩余消费
                    * */
                    int dy = dyUnconsumed + mParentOffsetWindow[1];
                    if (dy < 0) {
                        mTotalUnConmused += dy;
                        pullWithNestedOver(mTotalUnConmused);
                    }
                    break;
                case HORIZONTAL:
                    int dx = dxUnconsumed + mParentOffsetWindow[0];
                    if(dx < 0){
                        mTotalUnConmused += dx;
                        pullWithNestedOver(mTotalUnConmused);
                    }
                    break;
            }
        }else if(getMode().showFooterLoadingLayout() && isReadyForNestedScrollPullEnd()){
            mCurrentMode = Mode.PULL_FROM_END;
            switch (getPullToRefreshScrollDirection()){
                case VERTICAL:
                    int dy = dyUnconsumed - mParentOffsetWindow[1];
                    if(dy > 0) {
                        mTotalUnConmused += dy;
                        pullWithNestedOver(mTotalUnConmused);
                    }
                    break;
                case HORIZONTAL:
                    int dx = dxUnconsumed - mParentOffsetWindow[0];
                    if(dx > 0){
                        mTotalUnConmused += dx;
                        pullWithNestedOver(mTotalUnConmused);
                    }
                    break;
            }

        }
    }


    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
//        int offset[] = new int[2];
        /*
         *向上移动
         *此时嵌套嵌套滚动的视图已经展开
         */
        if(dy > 0 && mTotalUnConmused < 0 ){
            switch (getPullToRefreshScrollDirection()){
                case VERTICAL:
                    if(dy > mTotalUnConmused) {
                        consumed[1] = -mTotalUnConmused;
                        mTotalUnConmused = 0;
                    }else {
                        consumed[1] = dy;
                        mTotalUnConmused += dy;
                    }
                    int[] parentConsumedY = mParentConmused;
                    pullWithNestedOver(mTotalUnConmused);
                    dispatchNestedPreScroll(dx, dy - consumed[1], parentConsumedY, null);
                    consumed[0] += parentConsumedY[0];
                    consumed[1] += parentConsumedY[1];
                    break;
                case HORIZONTAL:
                    if(dx > mTotalUnConmused){
                        consumed[0] = -mTotalUnConmused;
                        mTotalUnConmused = 0;
                    }else {
                        consumed[0] = dx;
                        mTotalUnConmused += dx;
                    }
                    int parentConsumedX[] = mParentConmused;
                    pullWithNestedOver(mTotalUnConmused);
                    dispatchNestedPreScroll(dx - consumed[0], dy, parentConsumedX, null);
                    consumed[0] += parentConsumedX[0];
                    consumed[1] += parentConsumedX[1];
                    break;
            }

        }else if(dy < 0 && mTotalUnConmused > 0){
            switch (getPullToRefreshScrollDirection()){
                case VERTICAL:
                    if(Math.abs(dy) < mTotalUnConmused){
                        consumed[1] += dy;
                        mTotalUnConmused += dy;
                    }else{
                        consumed[1] -= mTotalUnConmused;
                        mTotalUnConmused = 0;
                    }
                    pullWithNestedOver(mTotalUnConmused);
                    int[] parentConsumedY = mParentConmused;
                    dispatchNestedPreScroll(dx, dy - consumed[1], parentConsumedY, null);
                    consumed[0] += parentConsumedY[0];
                    consumed[1] += parentConsumedY[1];
                    break;
                case HORIZONTAL:
                    if(Math.abs(dx) < mTotalUnConmused){
                        consumed[0] += dx;
                        mTotalUnConmused -= dx;
                    }else{
                        consumed[0] -= mTotalUnConmused;
                        mTotalUnConmused = 0;
                    }
                    pullWithNestedOver(mTotalUnConmused);
                    int[] parentConsumedX = mParentConmused;
                    dispatchNestedPreScroll(dx + consumed[0], dy, parentConsumedX, null);
                    consumed[0] += parentConsumedX[0];
                    consumed[1] += parentConsumedX[1];
                    break;
            }

        }else {
            dispatchNestedPreScroll(dx, dy, consumed, null);
        }
    }

    /**
     *这里我们直接交给真正的parent去处理fling,我们是拖动刷新,所以这里不能在fling里去执行pull
     */
    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
       return dispatchNestedFling(velocityX, velocityY, consumed);
    }

    /**
     *这里我们直接交给真正的parent去处理fling,我们是拖动刷新,所以这里不能在fling里去执行pull
     */
    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return dispatchNestedPreFling(velocityX, velocityY);
    }

    /**
     *嵌套滚动中给下拉刷新消费
     */
    private void pullWithNestedOver(int scrollConsumed){
//            mCurrentMode = Mode.PULL_FROM_START;
        if(mCurrentMode.getIntValue() ==  Mode.PULL_FROM_START.getIntValue()) {
            int headerDimension = getHeaderSize();
            int neoScrollValue = (int) (scrollConsumed / FRICTION);
            setHeaderScroll(neoScrollValue);
            onPullChange(neoScrollValue, headerDimension);
        }else if(mCurrentMode.getIntValue() == Mode.PULL_FROM_END.getIntValue()){
            int footerDimension = getFooterSize();
            int neoScrollValue = (int) (scrollConsumed / FRICTION);
            setHeaderScroll(neoScrollValue);
            onPullChange(neoScrollValue, footerDimension);
        }
    }


    @Override
    public int getNestedScrollAxes() {
        return getNestedScrollingParentHelper().getNestedScrollAxes();
    }

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        getNestedScrollingChildHelper().setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return getNestedScrollingChildHelper().isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return getNestedScrollingChildHelper().startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        getNestedScrollingChildHelper().stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return getNestedScrollingChildHelper().hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable @Size(value = 2) int[] offsetInWindow) {
        return getNestedScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable @Size(value = 2) int[] consumed, @Nullable @Size(value = 2) int[] offsetInWindow) {
        return getNestedScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return getNestedScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return getNestedScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
    }

    private NestedScrollingChildHelper getNestedScrollingChildHelper(){
        if(mNestedScrollingChildHelper == null) mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
        return mNestedScrollingChildHelper;
    }

    private NestedScrollingParentHelper getNestedScrollingParentHelper(){
        if(mNestedScorllingParentHelper == null) mNestedScorllingParentHelper = new NestedScrollingParentHelper(this);
        return mNestedScorllingParentHelper;
    }

}

你可能感兴趣的:(android开发)