package com.kth.baasio.helpcenter.ui.pulltorefresh; import com.kth.baasio.helpcenter.R; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; 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; public abstract class PullToRefreshBase<T extends View> extends LinearLayout { final class SmoothScrollRunnable implements Runnable { static final int ANIMATION_DURATION_MS = 400; 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) { mHandler = handler; mScrollFromY = fromY; mScrollToY = toY; mInterpolator = new AccelerateDecelerateInterpolator(); } @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)) / ANIMATION_DURATION_MS; 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) { mHandler.post(this); } } public void stop() { mContinueRunning = false; mHandler.removeCallbacks(this); } } // =========================================================== // Constants // =========================================================== static final boolean DEBUG = false; static final String LOG_TAG = "PullToRefresh"; static final float FRICTION = 2.0f; 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; public static final int MODE_PULL_DOWN_TO_REFRESH = 0x1; public static final int MODE_PULL_UP_TO_REFRESH = 0x2; public static final int MODE_BOTH = 0x3; 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 // =========================================================== protected 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; protected T mRefreshableView; private boolean mPullToRefreshEnabled = true; private boolean mShowViewWhileRefreshing = true; protected boolean mDisableScrollingWhileRefreshing = true; protected boolean mEnableSlopeDragToRefreshing = false; private LoadingLayout mHeaderLayout; private LoadingLayout mFooterLayout; private int mHeaderHeight; private final Handler mHandler = new Handler(); protected OnRefreshListener mOnRefreshListener; protected OnRefreshListener2 mOnRefreshListener2; private SmoothScrollRunnable mCurrentSmoothScrollRunnable; // =========================================================== // Constructors // =========================================================== public PullToRefreshBase(Context context) { super(context); init(context, null); } public PullToRefreshBase(Context context, int mode) { super(context); mMode = mode; init(context, null); } public PullToRefreshBase(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } // =========================================================== // Getter & Setter // =========================================================== /** * 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; } /** * Get whether the 'Refreshing' View should be automatically shown when * refreshing. Returns true by default. * * @return - true if the Refreshing View will be show */ public final boolean getShowViewWhileRefreshing() { return mShowViewWhileRefreshing; } /** * Whether Pull-to-Refresh is enabled * * @return enabled */ public final boolean isPullToRefreshEnabled() { return mPullToRefreshEnabled; } /** * Get the mode that this view is currently in. This is only really useful * when using <code>MODE_BOTH</code>. * * @return int of either <code>MODE_PULL_DOWN_TO_REFRESH</code> or * <code>MODE_PULL_UP_TO_REFRESH</code> */ public final int getCurrentMode() { return mCurrentMode; } /** * Get the mode that this view has been set to. If this returns * <code>MODE_BOTH</code>, you can use <code>getCurrentMode()</code> to * check which mode the view is currently in * * @return int of either <code>MODE_PULL_DOWN_TO_REFRESH</code>, * <code>MODE_PULL_UP_TO_REFRESH</code> or <code>MODE_BOTH</code> */ public final int getMode() { return mMode; } /** * Returns whether the widget has disabled scrolling on the Refreshable View * while refreshing. * * @return true if the widget has disabled scrolling while refreshing */ public final boolean isDisableScrollingWhileRefreshing() { return mDisableScrollingWhileRefreshing; } /** * Returns whether the Widget is currently in the Refreshing mState * * @return true if the Widget is currently refreshing */ public 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 */ public final void setDisableScrollingWhileRefreshing(boolean disableScrollingWhileRefreshing) { 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(); } setTimeLabel(); } // 05.25 유미형님. 시간 보여주지 말자. protected void setTimeLabel() { // if(mHeaderLayout != null){ // mHeaderLayout.setTimeText(); // } // // if(mFooterLayout != null){ // mFooterLayout.setTimeText(); // } } /** * 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; } /** * Set OnRefreshListener for the Widget * * @param listener - Listener to be used when the Widget is set to Refresh */ public final void setOnRefreshListener(OnRefreshListener2 listener) { mOnRefreshListener2 = 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) { mPullToRefreshEnabled = enable; } public final boolean getPullToRefreshEnabled() { return mPullToRefreshEnabled; } /** * A mutator to enable/disable whether the 'Refreshing' View should be * automatically shown when refreshing. * * @param showView */ public final void setShowViewWhileRefreshing(boolean showView) { mShowViewWhileRefreshing = showView; } /** * Set Text to show when the Widget is being pulled, and will refresh when * released * * @param releaseLabel - String to display */ public 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 */ public 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 */ public void setRefreshingLabel(String refreshingLabel) { if (null != mHeaderLayout) { mHeaderLayout.setRefreshingLabel(refreshingLabel); } if (null != mFooterLayout) { mFooterLayout.setRefreshingLabel(refreshingLabel); } } public final void setRefreshing() { setRefreshing(true); } /** * Sets the Widget to be in the refresh mState. 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; } // =========================================================== // Methods for/from SuperClass/Interfaces // =========================================================== @Override public boolean onTouchEvent(MotionEvent event) { if (!mPullToRefreshEnabled) { 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(); 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) { if (null != mOnRefreshListener) { setRefreshingInternal(true); mOnRefreshListener.onRefresh(); return true; } else if (null != mOnRefreshListener2) { setRefreshingInternal(true); if (mCurrentMode == MODE_PULL_DOWN_TO_REFRESH) { mOnRefreshListener2.onPullDownToRefresh(); } else if (mCurrentMode == MODE_PULL_UP_TO_REFRESH) { mOnRefreshListener2.onPullUpToRefresh(); } return true; } return true; } smoothScrollTo(0); return true; } break; } } return false; } @Override public boolean onInterceptTouchEvent(MotionEvent event) { if (!mPullToRefreshEnabled) { 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 (mEnableSlopeDragToRefreshing) { if (yDiff < mTouchSlop || yDiff < xDiff) return false; } if ((mMode == MODE_PULL_DOWN_TO_REFRESH || mMode == MODE_BOTH) && dy >= 0.0001f && isReadyForPullDown()) { mLastMotionY = y; mIsBeingDragged = true; if (null != mCurrentSmoothScrollRunnable) { mCurrentSmoothScrollRunnable.stop(); } 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; } protected 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. 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 mFooterLayout; } protected final LoadingLayout getHeaderLayout() { return mHeaderLayout; } protected final int getHeaderHeight() { return mHeaderHeight; } protected final int getState() { return mState; } /** * 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(); @Override protected Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putInt(STATE_STATE, mState); bundle.putInt(STATE_MODE, mMode); bundle.putInt(STATE_CURRENT_MODE, mCurrentMode); bundle.putBoolean(STATE_DISABLE_SCROLLING_REFRESHING, mDisableScrollingWhileRefreshing); bundle.putBoolean(STATE_SHOW_REFRESHING_VIEW, mShowViewWhileRefreshing); bundle.putParcelable(STATE_SUPER, super.onSaveInstanceState()); return bundle; } @Override protected void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle)state; final int viewState = bundle.getInt(STATE_STATE, PULL_TO_REFRESH); mMode = bundle.getInt(STATE_MODE, MODE_PULL_DOWN_TO_REFRESH); mCurrentMode = bundle.getInt(STATE_CURRENT_MODE, MODE_PULL_DOWN_TO_REFRESH); mDisableScrollingWhileRefreshing = bundle.getBoolean( STATE_DISABLE_SCROLLING_REFRESHING, true); mShowViewWhileRefreshing = bundle.getBoolean(STATE_SHOW_REFRESHING_VIEW, true); // Let super Restore Itself super.onRestoreInstanceState(bundle.getParcelable(STATE_SUPER)); if (viewState == REFRESHING) { setRefreshingInternal(true); mState = viewState; } return; } super.onRestoreInstanceState(state); } // =========================================================== // Methods // =========================================================== protected void resetHeader() { mState = PULL_TO_REFRESH; mIsBeingDragged = false; if (null != mHeaderLayout) { mHeaderLayout.reset(); } if (null != mFooterLayout) { mFooterLayout.reset(); } smoothScrollTo(0); } protected void setRefreshingInternal(boolean doScroll) { mState = REFRESHING; if (null != mHeaderLayout) { mHeaderLayout.refreshing(); } if (null != mFooterLayout) { mFooterLayout.refreshing(); } if (doScroll) { if (mShowViewWhileRefreshing) { smoothScrollTo(mCurrentMode == MODE_PULL_DOWN_TO_REFRESH ? -mHeaderHeight : mHeaderHeight); } else { smoothScrollTo(0); } } } protected final void setHeaderScroll(int y) { scrollTo(0, y); } protected final void smoothScrollTo(int y) { if (null != mCurrentSmoothScrollRunnable) { mCurrentSmoothScrollRunnable.stop(); } if (getScrollY() != y) { 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_Helpcenter_ptrMode)) { mMode = a.getInteger(R.styleable.PullToRefresh_Helpcenter_ptrMode, MODE_PULL_DOWN_TO_REFRESH); } // Refreshable View // By passing the attrs, we can add ListView/GridView params via XML mRefreshableView = createRefreshableView(context, attrs); addRefreshableView(context, mRefreshableView); // Loading View Strings String pullLabel = context.getString(R.string.pull_to_refresh_pull_label); String refreshingLabel = null;// 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, a); 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, a); 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_Helpcenter_ptrHeaderBackground)) { setBackgroundResource(a.getResourceId( R.styleable.PullToRefresh_Helpcenter_ptrHeaderBackground, Color.WHITE)); } if (a.hasValue(R.styleable.PullToRefresh_Helpcenter_ptrAdapterViewBackground)) { mRefreshableView.setBackgroundResource(a.getResourceId( R.styleable.PullToRefresh_Helpcenter_ptrAdapterViewBackground, Color.WHITE)); } a.recycle(); a = null; // 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, set mCurrentMode to mMode, otherwise // set it to pull down mCurrentMode = mMode != MODE_BOTH ? mMode : MODE_PULL_DOWN_TO_REFRESH; } private 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 = 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 // =========================================================== /** * Simple Listener to listen for any callbacks to Refresh. * * @author Chris Banes */ public static interface OnRefreshListener { /** * onRefresh will be called for both Pull Down from top, and Pull Up * from Bottom */ public void onRefresh(); public void onUpdate(); } /** * 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 { /** * onPullDownToRefresh will be called only when the user has Pulled Down * from the top, and released. */ public void onPullDownToRefresh(); /** * onPullUpToRefresh will be called only when the user has Pulled Up * from the bottom, and released. */ public void onPullUpToRefresh(); } public static interface OnLastItemVisibleListener { public void onLastItemVisible(); } public static interface OnDirectionListener { public void onUp(); public void onDown(); } public static interface OnFirstItemVisibleListener { public void onFirstItemVisible(); } @Override public void setLongClickable(boolean longClickable) { getRefreshableView().setLongClickable(longClickable); } }