SwipeRefreshLayout
来进行下拉刷新的操作,使用过它的人多数都应该知道,其实我们还是需要做一些重写处理才能很好的进行使用;那么话说回来,如果我们想使用PullToRefreshListView 这个框架去接入RecyclerView 的上拉和下拉呢?其实上也是非常好扩展的,网上已经有很多的例子。但是,网上的大多数Demo只是简单的按照PullToRefreshListView原本的框架去扩展实现了RecyclerView 的下拉刷新,很少有考虑了RecyclerView嵌套滚动这一个特性的,以至于我们在使用了嵌套滚动布局的时候,就会出现下拉刷新和嵌套滚动冲突的情况,我觉得这样不太合理,所以根据嵌套滚动的实现逻辑,重新去继承修改了PullToRefreshListView 的源码,重新封装了一个PullToRefreshAttacher,接下来我讲讲实现逻辑。NestedScrollingChild
,它用来定义目标视图何时开始嵌套滚动和需要嵌套滚动的偏移量, 以及执行嵌套滚动的NestedScrollingParent
,它接受来自NestedScrollingChild
给定的需要嵌套滚动的参数去对需要滚动的目标执行坐标偏移,达到和目标视图一同滚动的效果。 如果您要从头实现一个自定义的可以嵌套滚动的视图,那么在您需要在目标视图扩展NestedScrollingChild
以及目标视图的父布局扩展NestedScrollingParent
,但好在v4包已经有帮我们实现过这两个接口的类,我们可以在布局里直接添加,如果您的一个滚动布局里需要支持嵌套滚动那么您只需要在最外层加上v4包里的CoordinatorLayout
布局即可。 NestedScrollingChildHelper
和 NestedScrollingParentHelper
: 看名字就能看出,这两个类的作用是帮助您去实现嵌套滚动的,接下来的实现里我们会看到它的身影 NestedScrollingChild
和 NestedScrollingParent
:接下来我们来看看它到底需要我们去扩展实现那些方法,代码如下:
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
主要承担一些状态记录的,相对简单,我们这里把NestedScrollingChild
和 NestedScrollingParent
的方法一一对应起来分析:
public void setNestedScrollingEnabled(boolean enabled);
顾名思义,要实现嵌套滚动,这一步是必须的,对应 isNestedScrollingEnabled()
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 里的 onStartNestedScroll
和 onNestedScrollAccepted
,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);
: 重点来了,这个方法实现稍稍复杂,就不贴代码了, 它围绕NestedScrollingChild
和 NestedScrollParent
展开,先来看下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
调用到NestedScrollingParent
的 onNestedPreScroll
方法,此时父布局会根剧传入的偏移量还有其子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操作发生在,手指移动后抬起的事件上,此时视图将继续滚动,velocityX
和 velocityY
表示此时继续滚动的速度,一般会配合 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)
:参照第5
2. 下拉刷新实现
* 有了上面的分析,我们知道,如果是支持嵌套滚动的滚动视图,那么就会实现NestedScrollingChild
方法,用来通知NestedScrollingParent
如何滚动,那么我们沿着这个思路,我们想要知道何时开始下拉刷新或者上拉加载,只需要扩展NestedScrollingParent
接口,从public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
监听里,我们就能知道他的消耗情况,如果dxUnconsumed 或者 dyUnconsumed 不为0 ,说明还没有消耗完,此时我们再扩展
int dxUnconsumed, int dyUnconsumed)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
进行嵌套滚动执行
* 当NestedScrollingParent
的public 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;
}
}