/*******************************************************************************
* 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 com.roboo.qiushibaike.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.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 com.roboo.qiushibaike.R;
public abstract class PullToRefreshBase<T extends View> extends LinearLayout implements IPullToRefresh<T>
{
static final boolean DEBUG = true;
/**
* 使用硬件层技术
*/
static final boolean USE_HW_LAYERS = false;
static final String LOG_TAG = "PullToRefresh";
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 Mode mMode = Mode.getDefault();
/**记录当前的模式*/
private Mode mCurrentMode;
T mRefreshableView;
private FrameLayout mRefreshableViewWrapper;
/**当刷新的时候是否显示刷新标志[箭头 or 进度圈] 默认显示*/
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();
/**下拉刷新标志的布局view*/
private LoadingLayout mHeaderLayout;
/**上拉加载标志的布局view*/
private LoadingLayout mFooterLayout;
/**只有下拉刷新的监听事件*/
private OnRefreshListener<T> mOnRefreshListener;
/**下拉刷新和上拉加载的监听事件*/
private OnRefreshListener2<T> mOnRefreshListener2;
/**在刷新开始or结束时的事件可以播放音乐等*/
private OnPullEventListener<T> mOnPullEventListener;
/**滚动Runnable*/
private SmoothScrollRunnable mCurrentSmoothScrollRunnable;
// ===========================================================
// 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 final boolean onInterceptTouchEvent(MotionEvent event)
{
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())
{
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())
{
mLastMotionY = y;
mLastMotionX = x;
mIsBeingDragged = true;
if (mMode == Mode.BOTH)
{
mCurrentMode = Mode.PULL_FROM_START;
}
}
else if (mMode.showFooterLoadingLayout() && diff <= -1f && isReadyForPullEnd())
{
mLastMotionY = y;
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;
}
@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;
}
switch (event.getAction())
{
case MotionEvent.ACTION_MOVE:
{
if (mIsBeingDragged)
{
mLastMotionY = event.getY();
mLastMotionX = event.getX();
pullEvent();
return true;
}
break;
}
case MotionEvent.ACTION_DOWN:
{
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;
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;
}
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);
}
@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<T> listener)
{
mOnPullEventListener = listener;
}
@Override
public final void setOnRefreshListener(OnRefreshListener<T> listener)
{
mOnRefreshListener = listener;
mOnRefreshListener2 = null;
}
@Override
public final void setOnRefreshListener(OnRefreshListener2<T> 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.
* <p/>
* 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;
}
}
/**
* 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
LinearLayout.LayoutParams lp = (LinearLayout.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 LinearLayout.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 LinearLayout.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)
{
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));
}
// Refreshable View
// By passing the attrs, we can add ListView/GridView params via XML
mRefreshableView = createRefreshableView(context, attrs);
addRefreshableView(context, mRefreshableView);
// 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))
{
Log.w("PTR", "请使用属性ptrRefreshableViewBackground来代替ptrAdapterViewBackground");
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);
if (newScrollValue != 0 && !isRefreshing())
{
float scale = Math.abs(newScrollValue) / (float) itemDimension;
switch (mCurrentMode)
{
case PULL_FROM_END:
mFooterLayout.onPull(scale);
break;
case PULL_FROM_START:
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))
{
setState(State.RELEASE_TO_REFRESH);
}
}
}
private LinearLayout.LayoutParams getLoadingLayoutLayoutParams()
{
switch (getPullToRefreshScrollDirection())
{
case HORIZONTAL:
return new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT);
case VERTICAL:
default:
return new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.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);
}
}
}
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 <var>state</var> is
* {@link State#PULL_TO_REFRESH} or
* {@link State#RELEASE_TO_REFRESH}.
*/
public void onPullEvent(final PullToRefreshBase<V> 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<V> 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<V> refreshView);
/**
* onPullUpToRefresh will be called only when the user has Pulled from
* the end, and released.
*/
public void onPullUpToRefresh(final PullToRefreshBase<V> 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 <strong>has</strong>
* 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();
}
}