/* * Copyright 2013 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 uk.co.senab.actionbarpulltorefresh.library; import android.app.ActionBar; import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.graphics.PixelFormat; import android.graphics.Rect; import android.os.Build; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowManager; import java.util.WeakHashMap; import uk.co.senab.actionbarpulltorefresh.library.listeners.HeaderViewListener; import uk.co.senab.actionbarpulltorefresh.library.listeners.OnRefreshListener; import uk.co.senab.actionbarpulltorefresh.library.viewdelegates.ViewDelegate; public class PullToRefreshAttacher { private static final boolean DEBUG = false; private static final String LOG_TAG = "PullToRefreshAttacher"; /* Member Variables */ private EnvironmentDelegate mEnvironmentDelegate; private HeaderTransformer mHeaderTransformer; private OnRefreshListener mOnRefreshListener; private Activity mActivity; private View mHeaderView; private HeaderViewListener mHeaderViewListener; private final int mTouchSlop; private final float mRefreshScrollDistance; private float mInitialMotionY, mLastMotionY, mPullBeginY; private float mInitialMotionX; private boolean mIsBeingDragged, mIsRefreshing, mHandlingTouchEventFromDown; private View mViewBeingDragged; private final WeakHashMap<View, ViewDelegate> mRefreshableViews; private final boolean mRefreshOnUp; private final int mRefreshMinimizeDelay; private final boolean mRefreshMinimize; private boolean mIsDestroyed = false; private final int[] mViewLocationResult = new int[2]; private final Rect mRect = new Rect(); protected PullToRefreshAttacher(Activity activity, Options options) { if (activity == null) { throw new IllegalArgumentException("activity cannot be null"); } if (options == null) { Log.i(LOG_TAG, "Given null options so using default options."); options = new Options(); } mActivity = activity; mRefreshableViews = new WeakHashMap<View, ViewDelegate>(); // Copy necessary values from options mRefreshScrollDistance = options.refreshScrollDistance; mRefreshOnUp = options.refreshOnUp; mRefreshMinimizeDelay = options.refreshMinimizeDelay; mRefreshMinimize = options.refreshMinimize; // EnvironmentDelegate mEnvironmentDelegate = options.environmentDelegate != null ? options.environmentDelegate : createDefaultEnvironmentDelegate(); // Header Transformer mHeaderTransformer = options.headerTransformer != null ? options.headerTransformer : createDefaultHeaderTransformer(); // Get touch slop for use later mTouchSlop = ViewConfiguration.get(activity).getScaledTouchSlop(); // Get Window Decor View final ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); // Create Header view and then add to Decor View mHeaderView = LayoutInflater.from( mEnvironmentDelegate.getContextForInflater(activity)).inflate( options.headerLayout, decorView, false); if (mHeaderView == null) { throw new IllegalArgumentException("Must supply valid layout id for header."); } // Make Header View invisible so it still gets a layout pass mHeaderView.setVisibility(View.INVISIBLE); // Notify transformer mHeaderTransformer.onViewCreated(activity, mHeaderView); // Now HeaderView to Activity decorView.post(new Runnable() { @Override public void run() { if (decorView.getWindowToken() != null) { // The Decor View has a Window Token, so we can add the HeaderView! addHeaderViewToActivity(mHeaderView); } else { // The Decor View doesn't have a Window Token yet, post ourselves again... decorView.post(this); } } }); } /** * Add a view which will be used to initiate refresh requests. * * @param view View which will be used to initiate refresh requests. */ void addRefreshableView(View view, ViewDelegate viewDelegate) { if (isDestroyed()) return; // Check to see if view is null if (view == null) { Log.i(LOG_TAG, "Refreshable View is null."); return; } // ViewDelegate if (viewDelegate == null) { viewDelegate = InstanceCreationUtils.getBuiltInViewDelegate(view); } // View to detect refreshes for mRefreshableViews.put(view, viewDelegate); } void useViewDelegate(Class<?> viewClass, ViewDelegate delegate) { for (View view : mRefreshableViews.keySet()) { if (viewClass.isInstance(view)) { mRefreshableViews.put(view, delegate); } } } /** * Clear all views which were previously used to initiate refresh requests. */ void clearRefreshableViews() { mRefreshableViews.clear(); } /** * This method should be called by your Activity's or Fragment's * onConfigurationChanged method. * * @param newConfig The new configuration */ public void onConfigurationChanged(Configuration newConfig) { mHeaderTransformer.onConfigurationChanged(mActivity, newConfig); } /** * Manually set this Attacher's refreshing state. The header will be * displayed or hidden as requested. * * @param refreshing * - Whether the attacher should be in a refreshing state, */ final void setRefreshing(boolean refreshing) { setRefreshingInt(null, refreshing, false); } /** * @return true if this Attacher is currently in a refreshing state. */ final boolean isRefreshing() { return mIsRefreshing; } /** * Call this when your refresh is complete and this view should reset itself * (header view will be hidden). * * This is the equivalent of calling <code>setRefreshing(false)</code>. */ final void setRefreshComplete() { setRefreshingInt(null, false, false); } /** * Set the Listener to be called when a refresh is initiated. */ void setOnRefreshListener(OnRefreshListener listener) { mOnRefreshListener = listener; } void destroy() { if (mIsDestroyed) return; // We've already been destroyed // Remove the Header View from the Activity removeHeaderViewFromActivity(mHeaderView); // Lets clear out all of our internal state clearRefreshableViews(); mActivity = null; mHeaderView = null; mHeaderViewListener = null; mEnvironmentDelegate = null; mHeaderTransformer = null; mIsDestroyed = true; } /** * Set a {@link HeaderViewListener} which is called when the visibility * state of the Header View has changed. */ final void setHeaderViewListener(HeaderViewListener listener) { mHeaderViewListener = listener; } /** * @return The Header View which is displayed when the user is pulling, or * we are refreshing. */ final View getHeaderView() { return mHeaderView; } /** * @return The HeaderTransformer currently used by this Attacher. */ HeaderTransformer getHeaderTransformer() { return mHeaderTransformer; } final boolean onInterceptTouchEvent(MotionEvent event) { if (DEBUG) { Log.d(LOG_TAG, "onInterceptTouchEvent: " + event.toString()); } // If we're not enabled or currently refreshing don't handle any touch // events if (isRefreshing()) { return false; } final float x = event.getX(), y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_MOVE: { // We're not currently being dragged so check to see if the user has // scrolled enough if (!mIsBeingDragged && mInitialMotionY > 0f) { final float yDiff = y - mInitialMotionY; final float xDiff = x - mInitialMotionX; if (yDiff > xDiff && yDiff > mTouchSlop) { mIsBeingDragged = true; onPullStarted(y); } else if (yDiff < -mTouchSlop) { resetTouch(); } } break; } case MotionEvent.ACTION_DOWN: { // If we're already refreshing, ignore if (canRefresh(true)) { for (View view : mRefreshableViews.keySet()) { if (isViewBeingDragged(view, event)) { mInitialMotionX = x; mInitialMotionY = y; mViewBeingDragged = view; } } } break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: { resetTouch(); break; } } if (DEBUG) Log.d(LOG_TAG, "onInterceptTouchEvent. Returning " + mIsBeingDragged); return mIsBeingDragged; } final boolean isViewBeingDragged(View view, MotionEvent event) { if (view.isShown() && mRefreshableViews.containsKey(view)) { // First we need to set the rect to the view's screen co-ordinates view.getLocationOnScreen(mViewLocationResult); final int viewLeft = mViewLocationResult[0], viewTop = mViewLocationResult[1]; mRect.set(viewLeft, viewTop, viewLeft + view.getWidth(), viewTop + view.getHeight()); if (DEBUG) Log.d(LOG_TAG, "isViewBeingDragged. View Rect: " + mRect.toString()); final int rawX = (int) event.getRawX(), rawY = (int) event.getRawY(); if (mRect.contains(rawX, rawY)) { // The Touch Event is within the View's display Rect ViewDelegate delegate = mRefreshableViews.get(view); if (delegate != null) { // Now call the delegate, converting the X/Y into the View's co-ordinate system return delegate.isReadyForPull(view, rawX - mRect.left, rawY - mRect.top); } } } return false; } final boolean onTouchEvent(MotionEvent event) { if (DEBUG) { Log.d(LOG_TAG, "onTouchEvent: " + event.toString()); } // Record whether our handling is started from ACTION_DOWN if (event.getAction() == MotionEvent.ACTION_DOWN) { mHandlingTouchEventFromDown = true; } // If we're being called from ACTION_DOWN then we must call through to // onInterceptTouchEvent until it sets mIsBeingDragged if (mHandlingTouchEventFromDown && !mIsBeingDragged) { onInterceptTouchEvent(event); return true; } if (mViewBeingDragged == null) { return false; } switch (event.getAction()) { case MotionEvent.ACTION_MOVE: { // If we're already refreshing ignore it if (isRefreshing()) { return false; } final float y = event.getY(); if (mIsBeingDragged && y != mLastMotionY) { final float yDx = y - mLastMotionY; /** * Check to see if the user is scrolling the right direction * (down). We allow a small scroll up which is the check against * negative touch slop. */ if (yDx >= -mTouchSlop) { onPull(mViewBeingDragged, y); // Only record the y motion if the user has scrolled down. if (yDx > 0f) { mLastMotionY = y; } } else { onPullEnded(); resetTouch(); } } break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: { checkScrollForRefresh(mViewBeingDragged); if (mIsBeingDragged) { onPullEnded(); } resetTouch(); break; } } return true; } void minimizeHeader() { if (isDestroyed()) return; mHeaderTransformer.onRefreshMinimized(); if (mHeaderViewListener != null) { mHeaderViewListener.onStateChanged(mHeaderView, HeaderViewListener.STATE_MINIMIZED); } } void resetTouch() { mIsBeingDragged = false; mHandlingTouchEventFromDown = false; mInitialMotionY = mLastMotionY = mPullBeginY = -1f; } void onPullStarted(float y) { if (DEBUG) { Log.d(LOG_TAG, "onPullStarted"); } showHeaderView(); mPullBeginY = y; } void onPull(View view, float y) { if (DEBUG) { Log.d(LOG_TAG, "onPull"); } final float pxScrollForRefresh = getScrollNeededForRefresh(view); final float scrollLength = y - mPullBeginY; if (scrollLength < pxScrollForRefresh) { mHeaderTransformer.onPulled(scrollLength / pxScrollForRefresh); } else { if (mRefreshOnUp) { mHeaderTransformer.onReleaseToRefresh(); } else { setRefreshingInt(view, true, true); } } } void onPullEnded() { if (DEBUG) { Log.d(LOG_TAG, "onPullEnded"); } if (!mIsRefreshing) { reset(true); } } void showHeaderView() { updateHeaderViewPosition(mHeaderView); if (mHeaderTransformer.showHeaderView()) { if (mHeaderViewListener != null) { mHeaderViewListener.onStateChanged(mHeaderView, HeaderViewListener.STATE_VISIBLE); } } } void hideHeaderView() { if (mHeaderTransformer.hideHeaderView()) { if (mHeaderViewListener != null) { mHeaderViewListener.onStateChanged(mHeaderView, HeaderViewListener.STATE_HIDDEN); } } } protected final Activity getAttachedActivity() { return mActivity; } protected EnvironmentDelegate createDefaultEnvironmentDelegate() { return new EnvironmentDelegate() { @Override public Context getContextForInflater(Activity activity) { Context context = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { ActionBar ab = activity.getActionBar(); if (ab != null) { context = ab.getThemedContext(); } } if (context == null) { context = activity; } return context; } }; } protected HeaderTransformer createDefaultHeaderTransformer() { return new DefaultHeaderTransformer(); } private boolean checkScrollForRefresh(View view) { if (mIsBeingDragged && mRefreshOnUp && view != null) { if (mLastMotionY - mPullBeginY >= getScrollNeededForRefresh(view)) { setRefreshingInt(view, true, true); return true; } } return false; } private void setRefreshingInt(View view, boolean refreshing, boolean fromTouch) { if (isDestroyed()) return; if (DEBUG) Log.d(LOG_TAG, "setRefreshingInt: " + refreshing); // Check to see if we need to do anything if (mIsRefreshing == refreshing) { return; } resetTouch(); if (refreshing && canRefresh(fromTouch)) { startRefresh(view, fromTouch); } else { reset(fromTouch); } } /** * @param fromTouch Whether this is being invoked from a touch event * @return true if we're currently in a state where a refresh can be * started. */ private boolean canRefresh(boolean fromTouch) { return !mIsRefreshing && (!fromTouch || mOnRefreshListener != null); } private float getScrollNeededForRefresh(View view) { return view.getHeight() * mRefreshScrollDistance; } private void reset(boolean fromTouch) { // Update isRefreshing state mIsRefreshing = false; // Remove any minimize callbacks if (mRefreshMinimize) { getHeaderView().removeCallbacks(mRefreshMinimizeRunnable); } // Hide Header View hideHeaderView(); } private void startRefresh(View view, boolean fromTouch) { // Update isRefreshing state mIsRefreshing = true; // Call OnRefreshListener if this call has originated from a touch event if (fromTouch) { if (mOnRefreshListener != null) { mOnRefreshListener.onRefreshStarted(view); } } // Call Transformer mHeaderTransformer.onRefreshStarted(); // Show Header View showHeaderView(); // Post a runnable to minimize the refresh header if (mRefreshMinimize) { if (mRefreshMinimizeDelay > 0) { getHeaderView().postDelayed(mRefreshMinimizeRunnable, mRefreshMinimizeDelay); } else { getHeaderView().post(mRefreshMinimizeRunnable); } } } private boolean isDestroyed() { if (mIsDestroyed) { Log.i(LOG_TAG, "PullToRefreshAttacher is destroyed."); } return mIsDestroyed; } protected void addHeaderViewToActivity(View headerView) { // Get the Display Rect of the Decor View mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(mRect); // Honour the requested layout params int width = WindowManager.LayoutParams.MATCH_PARENT; int height = WindowManager.LayoutParams.WRAP_CONTENT; ViewGroup.LayoutParams requestedLp = headerView.getLayoutParams(); if (requestedLp != null) { width = requestedLp.width; height = requestedLp.height; } // Create LayoutParams for adding the View as a panel WindowManager.LayoutParams wlp = new WindowManager.LayoutParams(width, height, WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT); wlp.x = 0; wlp.y = mRect.top; wlp.gravity = Gravity.TOP; // Workaround for Issue #182 headerView.setTag(wlp); mActivity.getWindowManager().addView(headerView, wlp); } protected void updateHeaderViewPosition(View headerView) { // Refresh the Display Rect of the Decor View mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(mRect); WindowManager.LayoutParams wlp = null; if (headerView.getLayoutParams() instanceof WindowManager.LayoutParams) { wlp = (WindowManager.LayoutParams) headerView.getLayoutParams(); } else if (headerView.getTag() instanceof WindowManager.LayoutParams) { wlp = (WindowManager.LayoutParams) headerView.getTag(); } if (wlp != null && wlp.y != mRect.top) { wlp.y = mRect.top; mActivity.getWindowManager().updateViewLayout(headerView, wlp); } } protected void removeHeaderViewFromActivity(View headerView) { if (headerView.getWindowToken() != null) { mActivity.getWindowManager().removeViewImmediate(headerView); } } private final Runnable mRefreshMinimizeRunnable = new Runnable() { @Override public void run() { minimizeHeader(); } }; }