package com.tweetlanes.android.core.widget.pulltorefresh;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.LinearLayout;
import com.tweetlanes.android.core.R;
import com.tweetlanes.android.core.widget.pulltorefresh.internal.LoadingLayout;
public abstract class PullToRefreshBase<T extends View> extends LinearLayout {
final class SmoothScrollRunnable implements Runnable {
static final int ANIMATION_DURATION_MS = 190;
static final int ANIMATION_FPS = 1000 / 60;
private final Interpolator mInterpolator;
private final int mScrollToY;
private final int mScrollFromY;
private final Handler mHandler;
private boolean mContinueRunning = true;
private long mStartTime = -1;
private int mCurrentY = -1;
public SmoothScrollRunnable(Handler handler, int fromY, int toY) {
this.mHandler = handler;
this.mScrollFromY = fromY;
this.mScrollToY = toY;
this.mInterpolator = new AccelerateDecelerateInterpolator();
}
@Override
public void run() {
/**
* Only set startTime 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))
/ ANIMATION_DURATION_MS;
normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0);
final int deltaY = Math.round((mScrollFromY - mScrollToY)
* mInterpolator
.getInterpolation(normalizedTime / 1000f));
this.mCurrentY = mScrollFromY - deltaY;
setHeaderScroll(mCurrentY);
}
// If we're not at the target Y, keep going...
if (mContinueRunning && mScrollToY != mCurrentY) {
mHandler.postDelayed(this, ANIMATION_FPS);
}
}
public void stop() {
this.mContinueRunning = false;
this.mHandler.removeCallbacks(this);
}
}
// ===========================================================
// Constants
// ===========================================================
private static final float FRICTION = 2.0f;
private static final int PULL_TO_REFRESH = 0x0;
private static final int RELEASE_TO_REFRESH = 0x1;
private static final int REFRESHING = 0x2;
private static final int MANUAL_REFRESHING = 0x3;
public static final int MODE_PULL_DOWN_TO_REFRESH = 0x1;
public static final int MODE_PULL_UP_TO_REFRESH = 0x2;
static final int MODE_BOTH = 0x3;
// ===========================================================
// Fields
// ===========================================================
private int mTouchSlop;
private float mInitialMotionY;
private float mLastMotionX;
private float mLastMotionY;
private boolean mIsBeingDragged = false;
private int mState = PULL_TO_REFRESH;
private int mMode = MODE_PULL_DOWN_TO_REFRESH;
private int mCurrentMode;
private boolean mDisableScrollingWhileRefreshing = true;
T mRefreshableView;
private boolean mIsPullToRefreshEnabled = true;
private LoadingLayout mHeaderLayout;
private LoadingLayout mFooterLayout;
private int mHeaderHeight;
private final Handler mHandler = new Handler();
private OnRefreshListener mOnRefreshListener;
private SmoothScrollRunnable mCurrentSmoothScrollRunnable;
// ===========================================================
// Constructors
// ===========================================================
PullToRefreshBase(Context context) {
super(context);
init(context, null);
}
PullToRefreshBase(Context context, int mode) {
super(context);
this.mMode = mode;
init(context, null);
}
PullToRefreshBase(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
// ===========================================================
// Getter & Setter
// ===========================================================
/**
* Deprecated. Use {@link #getRefreshableView()} from now on.
*
* @return The Refreshable View which is currently wrapped
* @deprecated
*/
public final T getAdapterView() {
return mRefreshableView;
}
/**
* Get the Wrapped Refreshable View. Anything returned here has already been
* added to the content view.
*
* @return The View which is currently wrapped
*/
public final T getRefreshableView() {
return mRefreshableView;
}
/**
* Whether Pull-to-Refresh is enabled
*
* @return enabled
*/
public final boolean isPullToRefreshEnabled() {
return mIsPullToRefreshEnabled;
}
/**
* Returns whether the widget has disabled scrolling on the Refreshable View
* while refreshing.
*
* @param true if the widget has disabled scrolling while refreshing
*/
public final boolean isDisableScrollingWhileRefreshing() {
return mDisableScrollingWhileRefreshing;
}
/**
* Returns whether the Widget is currently in the Refreshing state
*
* @return true if the Widget is currently refreshing
*/
final boolean isRefreshing() {
return mState == REFRESHING || mState == MANUAL_REFRESHING;
}
/**
* By default the Widget disabled scrolling on the Refreshable View while
* refreshing. This method can change this behaviour.
*
* @param disableScrollingWhileRefreshing - true if you want to disable scrolling while refreshing
*/
final void setDisableScrollingWhileRefreshing(
boolean disableScrollingWhileRefreshing) {
this.mDisableScrollingWhileRefreshing = disableScrollingWhileRefreshing;
}
/**
* Mark the current Refresh as complete. Will Reset the UI and hide the
* Refreshing View
*/
public final void onRefreshComplete() {
if (mState != PULL_TO_REFRESH) {
resetHeader();
}
}
/**
* Set OnRefreshListener for the Widget
*
* @param listener - Listener to be used when the Widget is set to Refresh
*/
public final void setOnRefreshListener(OnRefreshListener listener) {
mOnRefreshListener = listener;
}
/**
* A mutator to enable/disable Pull-to-Refresh for the current View
*
* @param enable Whether Pull-To-Refresh should be used
*/
public final void setPullToRefreshEnabled(boolean enable) {
this.mIsPullToRefreshEnabled = enable;
}
/**
* Set Text to show when the Widget is being pulled, and will refresh when
* released
*
* @param releaseLabel - String to display
*/
void setReleaseLabel(String releaseLabel) {
if (null != mHeaderLayout) {
mHeaderLayout.setReleaseLabel(releaseLabel);
}
if (null != mFooterLayout) {
mFooterLayout.setReleaseLabel(releaseLabel);
}
}
/**
* Set Text to show when the Widget is being Pulled
*
* @param pullLabel - String to display
*/
void setPullLabel(String pullLabel) {
if (null != mHeaderLayout) {
mHeaderLayout.setPullLabel(pullLabel);
}
if (null != mFooterLayout) {
mFooterLayout.setPullLabel(pullLabel);
}
}
/**
* Set Text to show when the Widget is refreshing
*
* @param refreshingLabel - String to display
*/
void setRefreshingLabel(String refreshingLabel) {
if (null != mHeaderLayout) {
mHeaderLayout.setRefreshingLabel(refreshingLabel);
}
if (null != mFooterLayout) {
mFooterLayout.setRefreshingLabel(refreshingLabel);
}
}
final void setRefreshing() {
this.setRefreshing(true);
}
/**
* Sets the Widget to be in the refresh state. The UI will be updated to
* show the 'Refreshing' view.
*
* @param doScroll - true if you want to force a scroll to the Refreshing view.
*/
public final void setRefreshing(boolean doScroll) {
if (!isRefreshing()) {
setRefreshingInternal(doScroll);
mState = MANUAL_REFRESHING;
}
}
public final boolean hasPullFromTop() {
return mCurrentMode != MODE_PULL_UP_TO_REFRESH;
}
public final boolean showingRefreshUI() {
return mCurrentMode != 0;
}
// ===========================================================
// Methods for/from SuperClass/Interfaces
// ===========================================================
@Override
public final boolean onTouchEvent(MotionEvent event) {
if (!mIsPullToRefreshEnabled) {
return false;
}
if (isRefreshing() && mDisableScrollingWhileRefreshing) {
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();
this.pullEvent();
return true;
}
break;
}
case MotionEvent.ACTION_DOWN: {
if (isReadyForPull()) {
mLastMotionY = mInitialMotionY = event.getY();
return true;
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
if (mIsBeingDragged) {
mIsBeingDragged = false;
if (mState == RELEASE_TO_REFRESH && null != mOnRefreshListener) {
setRefreshingInternal(true);
mOnRefreshListener.onRefresh();
} else {
smoothScrollTo(0);
}
return true;
}
break;
}
}
return false;
}
@Override
public final boolean onInterceptTouchEvent(MotionEvent event) {
if (!mIsPullToRefreshEnabled) {
return false;
}
if (isRefreshing() && mDisableScrollingWhileRefreshing) {
return true;
}
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 (isReadyForPull()) {
final float y = event.getY();
final float dy = y - mLastMotionY;
final float yDiff = Math.abs(dy);
final float xDiff = Math.abs(event.getX() - mLastMotionX);
if (yDiff > mTouchSlop && yDiff > xDiff) {
if ((mMode == MODE_PULL_DOWN_TO_REFRESH || mMode == MODE_BOTH)
&& dy >= 0.0001f && isReadyForPullDown()) {
mLastMotionY = y;
mIsBeingDragged = true;
if (mMode == MODE_BOTH) {
mCurrentMode = MODE_PULL_DOWN_TO_REFRESH;
}
} else if ((mMode == MODE_PULL_UP_TO_REFRESH || mMode == MODE_BOTH)
&& dy <= 0.0001f && isReadyForPullUp()) {
mLastMotionY = y;
mIsBeingDragged = true;
if (mMode == MODE_BOTH) {
mCurrentMode = MODE_PULL_UP_TO_REFRESH;
}
}
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
if (isReadyForPull()) {
mLastMotionY = mInitialMotionY = event.getY();
mLastMotionX = event.getX();
mIsBeingDragged = false;
}
break;
}
}
return mIsBeingDragged;
}
void addRefreshableView(Context context, T refreshableView) {
addView(refreshableView, new LinearLayout.LayoutParams(
LayoutParams.FILL_PARENT, 0, 1.0f));
}
/**
* 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
* @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);
final int getCurrentMode() {
return mCurrentMode;
}
final LoadingLayout getFooterLayout() {
return mFooterLayout;
}
final LoadingLayout getHeaderLayout() {
return mHeaderLayout;
}
final int getHeaderHeight() {
return mHeaderHeight;
}
final int getMode() {
return mMode;
}
/**
* Implemented by derived class to return whether the View is in a state
* where the user can Pull to Refresh by scrolling down.
*
* @return true if the View is currently the correct state (for example, top
* of a ListView)
*/
protected abstract boolean isReadyForPullDown();
/**
* Implemented by derived class to return whether the View is in a state
* where the user can Pull to Refresh by scrolling up.
*
* @return true if the View is currently in the correct state (for example,
* bottom of a ListView)
*/
protected abstract boolean isReadyForPullUp();
// ===========================================================
// Methods
// ===========================================================
void resetHeader() {
mState = PULL_TO_REFRESH;
mIsBeingDragged = false;
if (null != mHeaderLayout) {
mHeaderLayout.reset();
}
if (null != mFooterLayout) {
mFooterLayout.reset();
}
smoothScrollTo(0);
}
void setRefreshingInternal(boolean doScroll) {
mState = REFRESHING;
if (null != mHeaderLayout) {
mHeaderLayout.refreshing();
}
if (null != mFooterLayout) {
mFooterLayout.refreshing();
}
if (doScroll) {
smoothScrollTo(mCurrentMode == MODE_PULL_DOWN_TO_REFRESH ? -mHeaderHeight
: mHeaderHeight);
}
}
final void setHeaderScroll(int y) {
scrollTo(0, y);
}
final void smoothScrollTo(int y) {
if (null != mCurrentSmoothScrollRunnable) {
mCurrentSmoothScrollRunnable.stop();
}
if (this.getScrollY() != y) {
this.mCurrentSmoothScrollRunnable = new SmoothScrollRunnable(
mHandler, getScrollY(), y);
mHandler.post(mCurrentSmoothScrollRunnable);
}
}
private void init(Context context, AttributeSet attrs) {
setOrientation(LinearLayout.VERTICAL);
mTouchSlop = ViewConfiguration.getTouchSlop();
// Styleables from XML
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.PullToRefresh);
if (a.hasValue(R.styleable.PullToRefresh_mode)) {
mMode = a.getInteger(R.styleable.PullToRefresh_mode,
MODE_PULL_DOWN_TO_REFRESH);
}
// Refreshable View
// By passing the attrs, we can add ListView/GridView params via XML
mRefreshableView = this.createRefreshableView(context, attrs);
this.addRefreshableView(context, mRefreshableView);
// Loading View Strings
String pullLabel = context
.getString(R.string.pull_to_refresh_pull_label);
String refreshingLabel = context
.getString(R.string.pull_to_refresh_refreshing_label);
String releaseLabel = context
.getString(R.string.pull_to_refresh_release_label);
// Add Loading Views
if (mMode == MODE_PULL_DOWN_TO_REFRESH || mMode == MODE_BOTH) {
mHeaderLayout = new LoadingLayout(context,
MODE_PULL_DOWN_TO_REFRESH, releaseLabel, pullLabel,
refreshingLabel);
addView(mHeaderLayout, 0, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
measureView(mHeaderLayout);
mHeaderHeight = mHeaderLayout.getMeasuredHeight();
}
if (mMode == MODE_PULL_UP_TO_REFRESH || mMode == MODE_BOTH) {
mFooterLayout = new LoadingLayout(context, MODE_PULL_UP_TO_REFRESH,
releaseLabel, pullLabel, refreshingLabel);
addView(mFooterLayout, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
measureView(mFooterLayout);
mHeaderHeight = mFooterLayout.getMeasuredHeight();
}
// Styleables from XML
if (a.hasValue(R.styleable.PullToRefresh_headerTextColor)) {
final int color = a.getColor(
R.styleable.PullToRefresh_headerTextColor, Color.BLACK);
if (null != mHeaderLayout) {
mHeaderLayout.setTextColor(color);
}
if (null != mFooterLayout) {
mFooterLayout.setTextColor(color);
}
}
if (a.hasValue(R.styleable.PullToRefresh_headerBackground)) {
this.setBackgroundResource(a.getResourceId(
R.styleable.PullToRefresh_headerBackground, Color.WHITE));
}
if (a.hasValue(R.styleable.PullToRefresh_adapterViewBackground)) {
mRefreshableView.setBackgroundResource(a.getResourceId(
R.styleable.PullToRefresh_adapterViewBackground,
Color.WHITE));
}
a.recycle();
// Hide Loading Views
switch (mMode) {
case MODE_BOTH:
setPadding(0, -mHeaderHeight, 0, -mHeaderHeight);
break;
case MODE_PULL_UP_TO_REFRESH:
setPadding(0, 0, 0, -mHeaderHeight);
break;
case MODE_PULL_DOWN_TO_REFRESH:
default:
setPadding(0, -mHeaderHeight, 0, 0);
break;
}
// If we're not using MODE_BOTH, then just set currentMode to current
// mode
if (mMode != MODE_BOTH) {
mCurrentMode = mMode;
}
}
private static void measureView(View child) {
ViewGroup.LayoutParams p = child.getLayoutParams();
if (p == null) {
p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_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 (mCurrentMode) {
case MODE_PULL_UP_TO_REFRESH:
newHeight = Math.round(Math.max(mInitialMotionY - mLastMotionY, 0)
/ FRICTION);
break;
case MODE_PULL_DOWN_TO_REFRESH:
default:
newHeight = Math.round(Math.min(mInitialMotionY - mLastMotionY, 0)
/ FRICTION);
break;
}
setHeaderScroll(newHeight);
if (newHeight != 0) {
if (mState == PULL_TO_REFRESH
&& mHeaderHeight < Math.abs(newHeight)) {
mState = RELEASE_TO_REFRESH;
switch (mCurrentMode) {
case MODE_PULL_UP_TO_REFRESH:
mFooterLayout.releaseToRefresh();
break;
case MODE_PULL_DOWN_TO_REFRESH:
mHeaderLayout.releaseToRefresh();
break;
}
return true;
} else if (mState == RELEASE_TO_REFRESH
&& mHeaderHeight >= Math.abs(newHeight)) {
mState = PULL_TO_REFRESH;
switch (mCurrentMode) {
case MODE_PULL_UP_TO_REFRESH:
mFooterLayout.pullToRefresh();
break;
case MODE_PULL_DOWN_TO_REFRESH:
mHeaderLayout.pullToRefresh();
break;
}
return true;
}
}
return oldHeight != newHeight;
}
private boolean isReadyForPull() {
switch (mMode) {
case MODE_PULL_DOWN_TO_REFRESH:
return isReadyForPullDown();
case MODE_PULL_UP_TO_REFRESH:
return isReadyForPullUp();
case MODE_BOTH:
return isReadyForPullUp() || isReadyForPullDown();
}
return false;
}
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
public static interface OnRefreshListener {
public void onRefresh();
}
public static interface OnLastItemVisibleListener {
public void onLastItemVisible();
}
@Override
public void setLongClickable(boolean longClickable) {
getRefreshableView().setLongClickable(longClickable);
}
}