/*******************************************************************************
* 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.saikali.android_skwissh.widgets.pulltorefresh;
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.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.saikali.android_skwissh.R;
import com.saikali.android_skwissh.widgets.pulltorefresh.internal.LoadingLayout;
import com.saikali.android_skwissh.widgets.pulltorefresh.internal.SDK16;
public abstract class PullToRefreshBase<T extends View> extends LinearLayout implements IPullToRefresh<T> {
// ===========================================================
// Constants
// ===========================================================
static final boolean DEBUG = 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 PULL_TO_REFRESH = 0x0;
static final int RELEASE_TO_REFRESH = 0x1;
static final int REFRESHING = 0x2;
static final int MANUAL_REFRESHING = 0x3;
static final Mode DEFAULT_MODE = Mode.PULL_DOWN_TO_REFRESH;
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_DISABLE_SCROLLING_REFRESHING = "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;
private float mLastMotionY;
private float mInitialMotionY;
private boolean mIsBeingDragged = false;
private int mState = PULL_TO_REFRESH;
private Mode mMode = DEFAULT_MODE;
private Mode mCurrentMode;
T mRefreshableView;
private FrameLayout mRefreshableViewWrapper;
private boolean mShowViewWhileRefreshing = true;
private boolean mDisableScrollingWhileRefreshing = true;
private boolean mFilterTouchEvents = true;
private boolean mOverScrollEnabled = true;
private Interpolator mScrollAnimationInterpolator;
private LoadingLayout mHeaderLayout;
private LoadingLayout mFooterLayout;
private int mHeaderHeight;
private OnRefreshListener<T> mOnRefreshListener;
private OnRefreshListener2<T> mOnRefreshListener2;
private SmoothScrollRunnable mCurrentSmoothScrollRunnable;
// ===========================================================
// Constructors
// ===========================================================
public PullToRefreshBase(Context context) {
super(context);
this.init(context, null);
}
public PullToRefreshBase(Context context, AttributeSet attrs) {
super(context, attrs);
this.init(context, attrs);
}
public PullToRefreshBase(Context context, Mode mode) {
super(context);
this.mMode = mode;
this.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 = this.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 Mode getCurrentMode() {
return this.mCurrentMode;
}
@Override
public final boolean getFilterTouchEvents() {
return this.mFilterTouchEvents;
}
@Override
public final Mode getMode() {
return this.mMode;
}
@Override
public final T getRefreshableView() {
return this.mRefreshableView;
}
@Override
public final boolean getShowViewWhileRefreshing() {
return this.mShowViewWhileRefreshing;
}
@Override
public final boolean hasPullFromTop() {
return this.mCurrentMode == Mode.PULL_DOWN_TO_REFRESH;
}
@Override
public final boolean isDisableScrollingWhileRefreshing() {
return this.mDisableScrollingWhileRefreshing;
}
@Override
public final boolean isPullToRefreshEnabled() {
return this.mMode != Mode.DISABLED;
}
@Override
public final boolean isPullToRefreshOverScrollEnabled() {
if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD)
return this.mOverScrollEnabled && OverscrollHelper.isAndroidOverScrollEnabled(this.mRefreshableView);
return false;
}
@Override
public final boolean isRefreshing() {
return this.mState == REFRESHING || this.mState == MANUAL_REFRESHING;
}
@Override
public final boolean onInterceptTouchEvent(MotionEvent event) {
if (!this.isPullToRefreshEnabled())
return false;
final int action = event.getAction();
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
this.mIsBeingDragged = false;
return false;
}
if (action != MotionEvent.ACTION_DOWN && this.mIsBeingDragged)
return true;
switch (action) {
case MotionEvent.ACTION_MOVE: {
// If we're refreshing, and the flag is set. Eat all MOVE events
if (this.mDisableScrollingWhileRefreshing && this.isRefreshing())
return true;
if (this.isReadyForPull()) {
final float y = event.getY();
final float dy = y - this.mLastMotionY;
final float yDiff = Math.abs(dy);
final float xDiff = Math.abs(event.getX() - this.mLastMotionX);
if (yDiff > this.mTouchSlop && (!this.mFilterTouchEvents || yDiff > xDiff)) {
if (this.mMode.canPullDown() && dy >= 1f && this.isReadyForPullDown()) {
this.mLastMotionY = y;
this.mIsBeingDragged = true;
if (this.mMode == Mode.BOTH) {
this.mCurrentMode = Mode.PULL_DOWN_TO_REFRESH;
}
} else if (this.mMode.canPullUp() && dy <= -1f && this.isReadyForPullUp()) {
this.mLastMotionY = y;
this.mIsBeingDragged = true;
if (this.mMode == Mode.BOTH) {
this.mCurrentMode = Mode.PULL_UP_TO_REFRESH;
}
}
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
if (this.isReadyForPull()) {
this.mLastMotionY = this.mInitialMotionY = event.getY();
this.mLastMotionX = event.getX();
this.mIsBeingDragged = false;
}
break;
}
}
return this.mIsBeingDragged;
}
@Override
public final void onRefreshComplete() {
if (this.mState != PULL_TO_REFRESH) {
this.resetHeader();
}
}
@Override
public final boolean onTouchEvent(MotionEvent event) {
if (!this.isPullToRefreshEnabled())
return false;
// If we're refreshing, and the flag is set. Eat the event
if (this.mDisableScrollingWhileRefreshing && this.isRefreshing())
return true;
if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0)
return false;
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE: {
if (this.mIsBeingDragged) {
this.mLastMotionY = event.getY();
this.pullEvent();
return true;
}
break;
}
case MotionEvent.ACTION_DOWN: {
if (this.isReadyForPull()) {
this.mLastMotionY = this.mInitialMotionY = event.getY();
return true;
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
if (this.mIsBeingDragged) {
this.mIsBeingDragged = false;
if (this.mState == RELEASE_TO_REFRESH) {
if (null != this.mOnRefreshListener) {
this.setRefreshingInternal(true);
this.mOnRefreshListener.onRefresh(this);
return true;
} else if (null != this.mOnRefreshListener2) {
this.setRefreshingInternal(true);
if (this.mCurrentMode == Mode.PULL_DOWN_TO_REFRESH) {
this.mOnRefreshListener2.onPullDownToRefresh(this);
} else if (this.mCurrentMode == Mode.PULL_UP_TO_REFRESH) {
this.mOnRefreshListener2.onPullUpToRefresh(this);
}
return true;
} else {
// If we don't have a listener, just reset
this.resetHeader();
return true;
}
}
this.smoothScrollTo(0);
return true;
}
break;
}
}
return false;
}
@Override
public final void setDisableScrollingWhileRefreshing(boolean disableScrollingWhileRefreshing) {
this.mDisableScrollingWhileRefreshing = disableScrollingWhileRefreshing;
}
@Override
public final void setFilterTouchEvents(boolean filterEvents) {
this.mFilterTouchEvents = filterEvents;
}
@Override
public void setLastUpdatedLabel(CharSequence label) {
if (null != this.mHeaderLayout) {
this.mHeaderLayout.setSubHeaderText(label);
}
if (null != this.mFooterLayout) {
this.mFooterLayout.setSubHeaderText(label);
}
// Refresh Height as it may have changed
this.refreshLoadingViewsHeight();
}
@Override
public void setLoadingDrawable(Drawable drawable) {
this.setLoadingDrawable(drawable, Mode.BOTH);
}
@Override
public void setLoadingDrawable(Drawable drawable, Mode mode) {
if (null != this.mHeaderLayout && mode.canPullDown()) {
this.mHeaderLayout.setLoadingDrawable(drawable);
}
if (null != this.mFooterLayout && mode.canPullUp()) {
this.mFooterLayout.setLoadingDrawable(drawable);
}
// The Loading Height may have changed, so refresh
this.refreshLoadingViewsHeight();
}
@Override
public void setLongClickable(boolean longClickable) {
this.getRefreshableView().setLongClickable(longClickable);
}
@Override
public final void setMode(Mode mode) {
if (mode != this.mMode) {
if (DEBUG) {
Log.d(LOG_TAG, "Setting mode to: " + mode);
}
this.mMode = mode;
this.updateUIForMode();
}
}
@Override
public final void setOnRefreshListener(OnRefreshListener<T> listener) {
this.mOnRefreshListener = listener;
}
@Override
public final void setOnRefreshListener(OnRefreshListener2<T> listener) {
this.mOnRefreshListener2 = listener;
}
@Override
public void setPullLabel(String pullLabel) {
this.setPullLabel(pullLabel, Mode.BOTH);
}
@Override
public void setPullLabel(String pullLabel, Mode mode) {
if (null != this.mHeaderLayout && mode.canPullDown()) {
this.mHeaderLayout.setPullLabel(pullLabel);
}
if (null != this.mFooterLayout && mode.canPullUp()) {
this.mFooterLayout.setPullLabel(pullLabel);
}
}
@Override
public final void setPullToRefreshEnabled(boolean enable) {
this.setMode(enable ? DEFAULT_MODE : Mode.DISABLED);
}
@Override
public final void setPullToRefreshOverScrollEnabled(boolean enabled) {
this.mOverScrollEnabled = enabled;
}
@Override
public final void setRefreshing() {
this.setRefreshing(true);
}
@Override
public final void setRefreshing(boolean doScroll) {
if (!this.isRefreshing()) {
this.setRefreshingInternal(doScroll);
this.mState = MANUAL_REFRESHING;
}
}
@Override
public void setRefreshingLabel(String refreshingLabel) {
this.setRefreshingLabel(refreshingLabel, Mode.BOTH);
}
@Override
public void setRefreshingLabel(String refreshingLabel, Mode mode) {
if (null != this.mHeaderLayout && mode.canPullDown()) {
this.mHeaderLayout.setRefreshingLabel(refreshingLabel);
}
if (null != this.mFooterLayout && mode.canPullUp()) {
this.mFooterLayout.setRefreshingLabel(refreshingLabel);
}
}
@Override
public void setReleaseLabel(String releaseLabel) {
this.setReleaseLabel(releaseLabel, Mode.BOTH);
}
@Override
public void setReleaseLabel(String releaseLabel, Mode mode) {
if (null != this.mHeaderLayout && mode.canPullDown()) {
this.mHeaderLayout.setReleaseLabel(releaseLabel);
}
if (null != this.mFooterLayout && mode.canPullUp()) {
this.mFooterLayout.setReleaseLabel(releaseLabel);
}
}
@Override
public void setScrollAnimationInterpolator(Interpolator interpolator) {
this.mScrollAnimationInterpolator = interpolator;
}
@Override
public final void setShowViewWhileRefreshing(boolean showView) {
this.mShowViewWhileRefreshing = showView;
}
/**
* 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) {
return new LoadingLayout(context, mode, attrs);
}
/**
* 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 LoadingLayout getFooterLayout() {
return this.mFooterLayout;
}
protected final int getHeaderHeight() {
return this.mHeaderHeight;
}
protected final LoadingLayout getHeaderLayout() {
return this.mHeaderLayout;
}
protected FrameLayout getRefreshableViewWrapper() {
return this.mRefreshableViewWrapper;
}
protected final int getState() {
return this.mState;
}
/**
* 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 mState
* where the user can Pull to Refresh by scrolling down.
*
* @return true if the View is currently the correct mState (for example,
* top of a ListView)
*/
protected abstract boolean isReadyForPullDown();
/**
* Implemented by derived class to return whether the View is in a mState
* where the user can Pull to Refresh by scrolling up.
*
* @return true if the View is currently in the correct mState (for example,
* bottom of a ListView)
*/
protected abstract boolean isReadyForPullUp();
/**
* Called when the UI needs to be updated to the 'Pull to Refresh' state
*/
protected void onPullToRefresh() {
switch (this.mCurrentMode) {
case PULL_UP_TO_REFRESH:
this.mFooterLayout.pullToRefresh();
break;
case PULL_DOWN_TO_REFRESH:
this.mHeaderLayout.pullToRefresh();
break;
}
}
/**
* Called when the UI needs to be updated to the 'Release to Refresh' state
*/
protected void onReleaseToRefresh() {
switch (this.mCurrentMode) {
case PULL_UP_TO_REFRESH:
this.mFooterLayout.releaseToRefresh();
break;
case PULL_DOWN_TO_REFRESH:
this.mHeaderLayout.releaseToRefresh();
break;
}
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
this.mMode = Mode.mapIntToMode(bundle.getInt(STATE_MODE, 0));
this.mCurrentMode = Mode.mapIntToMode(bundle.getInt(STATE_CURRENT_MODE, 0));
this.mDisableScrollingWhileRefreshing = bundle.getBoolean(STATE_DISABLE_SCROLLING_REFRESHING, true);
this.mShowViewWhileRefreshing = bundle.getBoolean(STATE_SHOW_REFRESHING_VIEW, true);
// Let super Restore Itself
super.onRestoreInstanceState(bundle.getParcelable(STATE_SUPER));
final int viewState = bundle.getInt(STATE_STATE, PULL_TO_REFRESH);
if (viewState == REFRESHING) {
this.setRefreshingInternal(true);
this.mState = viewState;
}
return;
}
super.onRestoreInstanceState(state);
}
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putInt(STATE_STATE, this.mState);
bundle.putInt(STATE_MODE, this.mMode.getIntValue());
bundle.putInt(STATE_CURRENT_MODE, this.mCurrentMode.getIntValue());
bundle.putBoolean(STATE_DISABLE_SCROLLING_REFRESHING, this.mDisableScrollingWhileRefreshing);
bundle.putBoolean(STATE_SHOW_REFRESHING_VIEW, this.mShowViewWhileRefreshing);
bundle.putParcelable(STATE_SUPER, super.onSaveInstanceState());
return bundle;
}
protected void resetHeader() {
this.mState = PULL_TO_REFRESH;
this.mIsBeingDragged = false;
if (this.mMode.canPullDown()) {
this.mHeaderLayout.reset();
}
if (this.mMode.canPullUp()) {
this.mFooterLayout.reset();
}
this.smoothScrollTo(0);
}
protected final void setHeaderScroll(int y) {
this.scrollTo(0, y);
}
protected void setRefreshingInternal(boolean doScroll) {
this.mState = REFRESHING;
if (this.mMode.canPullDown()) {
this.mHeaderLayout.refreshing();
}
if (this.mMode.canPullUp()) {
this.mFooterLayout.refreshing();
}
if (doScroll) {
if (this.mShowViewWhileRefreshing) {
this.smoothScrollTo(this.mCurrentMode == Mode.PULL_DOWN_TO_REFRESH ? -this.mHeaderHeight : this.mHeaderHeight);
} else {
this.smoothScrollTo(0);
}
}
}
/**
* Smooth Scroll to Y position using the default duration of
* {@value #SMOOTH_SCROLL_DURATION_MS} ms.
*
* @param y
* - Y position to scroll to
*/
protected final void smoothScrollTo(int y) {
this.smoothScrollTo(y, SMOOTH_SCROLL_DURATION_MS);
}
/**
* Smooth Scroll to Y position using the specific duration
*
* @param y
* - Y position to scroll to
* @param duration
* - Duration of animation in milliseconds
*/
protected final void smoothScrollTo(int y, long duration) {
if (null != this.mCurrentSmoothScrollRunnable) {
this.mCurrentSmoothScrollRunnable.stop();
}
if (this.getScrollY() != y) {
if (null == this.mScrollAnimationInterpolator) {
// Default interpolator is a Decelerate Interpolator
this.mScrollAnimationInterpolator = new DecelerateInterpolator();
}
this.mCurrentSmoothScrollRunnable = new SmoothScrollRunnable(this.getScrollY(), y, duration);
this.post(this.mCurrentSmoothScrollRunnable);
}
}
/**
* 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() {
// Remove Header, and then add Header Loading View again if needed
if (this == this.mHeaderLayout.getParent()) {
this.removeView(this.mHeaderLayout);
}
if (this.mMode.canPullDown()) {
this.addViewInternal(this.mHeaderLayout, 0, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
// Remove Footer, and then add Footer Loading View again if needed
if (this == this.mFooterLayout.getParent()) {
this.removeView(this.mFooterLayout);
}
if (this.mMode.canPullUp()) {
this.addViewInternal(this.mFooterLayout, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
// Hide Loading Views
this.refreshLoadingViewsHeight();
// If we're not using Mode.BOTH, set mCurrentMode to mMode, otherwise
// set it to pull down
this.mCurrentMode = (this.mMode != Mode.BOTH) ? this.mMode : Mode.PULL_DOWN_TO_REFRESH;
}
private void addRefreshableView(Context context, T refreshableView) {
this.mRefreshableViewWrapper = new FrameLayout(context);
this.mRefreshableViewWrapper.addView(refreshableView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
this.addViewInternal(this.mRefreshableViewWrapper, new LinearLayout.LayoutParams(android.view.ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f));
}
@SuppressWarnings("deprecation")
private void init(Context context, AttributeSet attrs) {
this.setOrientation(LinearLayout.VERTICAL);
ViewConfiguration config = ViewConfiguration.get(context);
this.mTouchSlop = config.getScaledTouchSlop();
// Styleables from XML
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PullToRefresh);
if (a.hasValue(R.styleable.PullToRefresh_ptrMode)) {
this.mMode = Mode.mapIntToMode(a.getInteger(R.styleable.PullToRefresh_ptrMode, 0));
}
// Refreshable View
// By passing the attrs, we can add ListView/GridView params via XML
this.mRefreshableView = this.createRefreshableView(context, attrs);
this.addRefreshableView(context, this.mRefreshableView);
// We need to create now layouts now
this.mHeaderLayout = this.createLoadingLayout(context, Mode.PULL_DOWN_TO_REFRESH, a);
this.mFooterLayout = this.createLoadingLayout(context, Mode.PULL_UP_TO_REFRESH, a);
// Styleables from XML
if (a.hasValue(R.styleable.PullToRefresh_ptrHeaderBackground)) {
Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrHeaderBackground);
if (null != background) {
this.setBackgroundDrawable(background);
}
}
if (a.hasValue(R.styleable.PullToRefresh_ptrAdapterViewBackground)) {
Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrAdapterViewBackground);
if (null != background) {
this.mRefreshableView.setBackgroundDrawable(background);
}
}
if (a.hasValue(R.styleable.PullToRefresh_ptrOverScroll)) {
this.mOverScrollEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrOverScroll, true);
}
// Let the derivative classes have a go at handling attributes, then
// recycle them...
this.handleStyledAttributes(a);
a.recycle();
// Finally update the UI for the modes
this.updateUIForMode();
}
private boolean isReadyForPull() {
switch (this.mMode) {
case PULL_DOWN_TO_REFRESH:
return this.isReadyForPullDown();
case PULL_UP_TO_REFRESH:
return this.isReadyForPullUp();
case BOTH:
return this.isReadyForPullUp() || this.isReadyForPullDown();
}
return false;
}
private void measureView(View child) {
ViewGroup.LayoutParams p = child.getLayoutParams();
if (p == null) {
p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0, p.width);
int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
}
/**
* Actions a Pull Event
*
* @return true if the Event has been handled, false if there has been no
* change
*/
private boolean pullEvent() {
final int newHeight;
final int oldHeight = this.getScrollY();
switch (this.mCurrentMode) {
case PULL_UP_TO_REFRESH:
newHeight = Math.round(Math.max(this.mInitialMotionY - this.mLastMotionY, 0) / FRICTION);
break;
case PULL_DOWN_TO_REFRESH:
default:
newHeight = Math.round(Math.min(this.mInitialMotionY - this.mLastMotionY, 0) / FRICTION);
break;
}
this.setHeaderScroll(newHeight);
if (newHeight != 0) {
float scale = Math.abs(newHeight) / (float) this.mHeaderHeight;
switch (this.mCurrentMode) {
case PULL_UP_TO_REFRESH:
this.mFooterLayout.onPullY(scale);
break;
case PULL_DOWN_TO_REFRESH:
this.mHeaderLayout.onPullY(scale);
break;
}
if (this.mState == PULL_TO_REFRESH && this.mHeaderHeight < Math.abs(newHeight)) {
this.mState = RELEASE_TO_REFRESH;
this.onReleaseToRefresh();
return true;
} else if (this.mState == RELEASE_TO_REFRESH && this.mHeaderHeight >= Math.abs(newHeight)) {
this.mState = PULL_TO_REFRESH;
this.onPullToRefresh();
return true;
}
}
return oldHeight != newHeight;
}
/**
* Re-measure the Loading Views height, and adjust internal padding as
* necessary
*/
private void refreshLoadingViewsHeight() {
if (this.mMode.canPullDown()) {
this.measureView(this.mHeaderLayout);
this.mHeaderHeight = this.mHeaderLayout.getMeasuredHeight();
} else if (this.mMode.canPullUp()) {
this.measureView(this.mFooterLayout);
this.mHeaderHeight = this.mFooterLayout.getMeasuredHeight();
} else {
this.mHeaderHeight = 0;
}
// Hide Loading Views
switch (this.mMode) {
case DISABLED:
this.setPadding(0, 0, 0, 0);
case BOTH:
this.setPadding(0, -this.mHeaderHeight, 0, -this.mHeaderHeight);
break;
case PULL_UP_TO_REFRESH:
this.setPadding(0, 0, 0, -this.mHeaderHeight);
break;
case PULL_DOWN_TO_REFRESH:
default:
this.setPadding(0, -this.mHeaderHeight, 0, 0);
break;
}
}
public static enum Mode {
/**
* Disable all Pull-to-Refresh gesture handling
*/
DISABLED(0x0),
/**
* Only allow the user to Pull Down from the top to refresh, this is the
* default.
*/
PULL_DOWN_TO_REFRESH(0x1),
/**
* Only allow the user to Pull Up from the bottom to refresh.
*/
PULL_UP_TO_REFRESH(0x2),
/**
* Allow the user to both Pull Down from the top, and Pull Up from the
* bottom to refresh.
*/
BOTH(0x3);
/**
* 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_DOWN_TO_REFRESH by
* default.
*/
public static Mode mapIntToMode(int modeInt) {
switch (modeInt) {
case 0x0:
return DISABLED;
case 0x1:
default:
return PULL_DOWN_TO_REFRESH;
case 0x2:
return PULL_UP_TO_REFRESH;
case 0x3:
return BOTH;
}
}
private int mIntValue;
// The modeInt values need to match those from attrs.xml
Mode(int modeInt) {
this.mIntValue = modeInt;
}
/**
* @return true if this mode permits Pulling Down from the top
*/
boolean canPullDown() {
return this == PULL_DOWN_TO_REFRESH || this == BOTH;
}
/**
* @return true if this mode permits Pulling Up from the bottom
*/
boolean canPullUp() {
return this == PULL_UP_TO_REFRESH || this == BOTH;
}
int getIntValue() {
return this.mIntValue;
}
}
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
/**
* 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();
}
/**
* 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 Pull Down from top, and Pull Up
* from Bottom
*/
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> {
/**
* onPullDownToRefresh will be called only when the user has Pulled Down
* from the top, and released.
*/
public void onPullDownToRefresh(final PullToRefreshBase<V> refreshView);
/**
* onPullUpToRefresh will be called only when the user has Pulled Up
* from the bottom, and released.
*/
public void onPullUpToRefresh(final PullToRefreshBase<V> refreshView);
}
final class SmoothScrollRunnable implements Runnable {
static final int ANIMATION_DELAY = 10;
private final Interpolator mInterpolator;
private final int mScrollToY;
private final int mScrollFromY;
private final long mDuration;
private boolean mContinueRunning = true;
private long mStartTime = -1;
private int mCurrentY = -1;
public SmoothScrollRunnable(int fromY, int toY, long duration) {
this.mScrollFromY = fromY;
this.mScrollToY = toY;
this.mInterpolator = PullToRefreshBase.this.mScrollAnimationInterpolator;
this.mDuration = duration;
}
@Override
public void run() {
/**
* Only set mStartTime if this is the first time we're starting,
* else actually calculate the Y delta
*/
if (this.mStartTime == -1) {
this.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() - this.mStartTime)) / this.mDuration;
normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0);
final int deltaY = Math.round((this.mScrollFromY - this.mScrollToY) * this.mInterpolator.getInterpolation(normalizedTime / 1000f));
this.mCurrentY = this.mScrollFromY - deltaY;
PullToRefreshBase.this.setHeaderScroll(this.mCurrentY);
}
// If we're not at the target Y, keep going...
if (this.mContinueRunning && this.mScrollToY != this.mCurrentY) {
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
SDK16.postOnAnimation(PullToRefreshBase.this, this);
} else {
PullToRefreshBase.this.postDelayed(this, ANIMATION_DELAY);
}
}
}
public void stop() {
this.mContinueRunning = false;
PullToRefreshBase.this.removeCallbacks(this);
}
}
}