Merge "Separate scroll and swipe views in SwipeRefreshLayout" into klp-ub-dev
diff --git a/v4/java/android/support/v4/widget/SwipeProgressBar.java b/v4/java/android/support/v4/widget/SwipeProgressBar.java
index b3b04cd..ac1e42c 100644
--- a/v4/java/android/support/v4/widget/SwipeProgressBar.java
+++ b/v4/java/android/support/v4/widget/SwipeProgressBar.java
@@ -137,7 +137,7 @@
         final int width = mBounds.width();
         final int height = mBounds.height();
         final int cx = width / 2;
-        final int cy = height / 2;
+        final int cy = mBounds.top + height / 2;
         boolean drawTriggerWhileFinishing = false;
         int restoreCount = canvas.save();
         canvas.clipRect(mBounds);
@@ -270,4 +270,4 @@
         mBounds.right = right;
         mBounds.bottom = bottom;
     }
-}
\ No newline at end of file
+}
diff --git a/v4/java/android/support/v4/widget/SwipeRefreshLayout.java b/v4/java/android/support/v4/widget/SwipeRefreshLayout.java
index 725a418..f789918 100644
--- a/v4/java/android/support/v4/widget/SwipeRefreshLayout.java
+++ b/v4/java/android/support/v4/widget/SwipeRefreshLayout.java
@@ -51,8 +51,12 @@
  * animation, call setEnabled(false) on the view.
  *
  * <p> This layout should be made the parent of the view that will be refreshed as a
- * result of the gesture and can only support one direct child. This view will
- * also be made the target of the gesture and will be forced to match both the
+ * result of the gesture and can only support one direct child. This single direct
+ * child can, however, specify a specific sub-child that will be the view that does
+ * the actual swipe gesture and move with the swipe motion. This specific sub-child
+ * is specified using {@link #setTarget(int)}. If no specific sub-child is
+ * specified, the single direct child will be used for the swipe view as well. This
+ * view will be made the target of the gesture and will be forced to match both the
  * width and the height supplied in this layout. The SwipeRefreshLayout does not
  * provide accessibility events; instead, a menu item must be provided to allow
  * refresh of the content wherever this gesture is used.</p>
@@ -68,8 +72,9 @@
     private static final int REFRESH_TRIGGER_DISTANCE = 120;
     private static final int INVALID_POINTER = -1;
 
-    private SwipeProgressBar mProgressBar; //the thing that shows progress is going
-    private View mTarget; //the content that gets pulled down
+    private SwipeProgressBar mProgressBar; // The thing that shows progress is going
+    private View mScrollTarget; // The scrolling single child
+    private View mTarget; // The content that gets pulled down
     private int mOriginalOffsetTop;
     private OnRefreshListener mListener;
     private int mFrom;
@@ -82,6 +87,7 @@
     private int mProgressBarHeight;
     private int mCurrentTargetOffsetTop;
 
+    private float mInitialTargetY;
     private float mInitialMotionY;
     private float mLastMotionY;
     private boolean mIsBeingDragged;
@@ -141,8 +147,7 @@
         @Override
         public void run() {
             mReturningToStart = true;
-            animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(),
-                    mReturnToStartPositionListener);
+            animateOffsetToStartPosition();
         }
 
     };
@@ -163,8 +168,7 @@
                 mShrinkTrigger.setInterpolator(mDecelerateInterpolator);
                 startAnimation(mShrinkTrigger);
             }
-            animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(),
-                    mReturnToStartPositionListener);
+            animateOffsetToStartPosition();
         }
 
     };
@@ -216,11 +220,11 @@
         removeCallbacks(mCancel);
     }
 
-    private void animateOffsetToStartPosition(int from, AnimationListener listener) {
-        mFrom = from;
+    private void animateOffsetToStartPosition() {
+        mFrom = mCurrentTargetOffsetTop + getPaddingTop();
         mAnimateToStartPosition.reset();
         mAnimateToStartPosition.setDuration(mMediumAnimationDuration);
-        mAnimateToStartPosition.setAnimationListener(listener);
+        mAnimateToStartPosition.setAnimationListener(mReturnToStartPositionListener);
         mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
         mTarget.startAnimation(mAnimateToStartPosition);
     }
@@ -233,6 +237,27 @@
         mListener = listener;
     }
 
+    /**
+     * Specify a separate {@link android.view.View} that should become the
+     * piece of content that the user is able to drag in order to trigger the
+     * refresh. This {@link android.view.View} must be a child of the
+     * {@link android.support.v4.widget.SwipeRefreshLayout}. If not specified,
+     * it will default to being the same single, direct child of the overall
+     * {@link android.support.v4.widget.SwipeRefreshLayout}.
+     *
+     * @param resId Resource ID of the {@link android.view.View} that should
+     *              become the new draggable section.
+     */
+    public void setTarget(int resId) {
+        View potentialChild = findViewById(resId);
+        // Only update if it's a valid child
+        if (potentialChild != null) {
+            mTarget = potentialChild;
+        } else {
+            Log.w(LOG_TAG, "View provided is not a valid child of SwipeRefreshLayout.");
+        }
+    }
+
     private void setTriggerPercentage(float percent) {
         if (percent == 0f) {
             // No-op. A null trigger means it's uninitialized, and setting it to zero-percent
@@ -303,24 +328,34 @@
 
     private void ensureTarget() {
         // Don't bother getting the parent height if the parent hasn't been laid out yet.
+
+        // Scroll target is always the single child.
+        if (mScrollTarget == null) {
+            mScrollTarget = getSingleChild();
+        }
+
+        // Swipeable target is either already defined or defaults to the single child.
         if (mTarget == null) {
-            if (getChildCount() > 1 && !isInEditMode()) {
-                throw new IllegalStateException(
-                        "SwipeRefreshLayout can host only one direct child");
-            }
-            mTarget = getChildAt(0);
-            mOriginalOffsetTop = mTarget.getTop() + getPaddingTop();
+            mTarget = getSingleChild();
         }
         if (mDistanceToTriggerSync == -1) {
             if (getParent() != null && ((View)getParent()).getHeight() > 0) {
                 final DisplayMetrics metrics = getResources().getDisplayMetrics();
                 mDistanceToTriggerSync = (int) Math.min(
                         ((View) getParent()) .getHeight() * MAX_SWIPE_DISTANCE_FACTOR,
-                                REFRESH_TRIGGER_DISTANCE * metrics.density);
+                        REFRESH_TRIGGER_DISTANCE * metrics.density);
             }
         }
     }
 
+    private View getSingleChild() {
+        if (getChildCount() > 1 && !isInEditMode()) {
+            throw new IllegalStateException(
+                    "SwipeRefreshLayout can host only one direct child");
+        }
+        return getChildAt(0);
+    }
+
     @Override
     public void draw(Canvas canvas) {
         super.draw(canvas);
@@ -331,16 +366,20 @@
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         final int width =  getMeasuredWidth();
         final int height = getMeasuredHeight();
-        mProgressBar.setBounds(0, 0, width, mProgressBarHeight);
         if (getChildCount() == 0) {
             return;
         }
         final View child = getChildAt(0);
         final int childLeft = getPaddingLeft();
-        final int childTop = mCurrentTargetOffsetTop + getPaddingTop();
+        final int childTop = getPaddingTop();
         final int childWidth = width - getPaddingLeft() - getPaddingRight();
         final int childHeight = height - getPaddingTop() - getPaddingBottom();
         child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
+
+        // Setting these initial value needs to happen after mTarget has received its size
+        mOriginalOffsetTop = mTarget.getTop() + getPaddingTop();
+        mProgressBar.setBounds(0, mOriginalOffsetTop, width,
+                mOriginalOffsetTop + mProgressBarHeight);
     }
 
     @Override
@@ -366,16 +405,16 @@
      */
     public boolean canChildScrollUp() {
         if (android.os.Build.VERSION.SDK_INT < 14) {
-            if (mTarget instanceof AbsListView) {
-                final AbsListView absListView = (AbsListView) mTarget;
+            if (mScrollTarget instanceof AbsListView) {
+                final AbsListView absListView = (AbsListView) mScrollTarget;
                 return absListView.getChildCount() > 0
                         && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
-                                .getTop() < absListView.getPaddingTop());
+                        .getTop() < absListView.getPaddingTop());
             } else {
-                return mTarget.getScrollY() > 0;
+                return mScrollTarget.getScrollY() > 0;
             }
         } else {
-            return ViewCompat.canScrollVertically(mTarget, -1);
+            return ViewCompat.canScrollVertically(mScrollTarget, -1);
         }
     }
 
@@ -389,13 +428,17 @@
             mReturningToStart = false;
         }
 
-        if (!isEnabled() || mReturningToStart || canChildScrollUp()) {
+        if (!isEnabled() || mReturningToStart || canChildScrollUp()
+                || ev.getY() < mTarget.getTop() || ev.getY() > mTarget.getBottom()) {
             // Fail fast if we're not in a state where a swipe is possible
+            // This includes if the swipe is outside mTarget
+            touchEventFailFast();
             return false;
         }
 
         switch (action) {
             case MotionEvent.ACTION_DOWN:
+                mInitialTargetY = mTarget.getTop();
                 mLastMotionY = mInitialMotionY = ev.getY();
                 mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                 mIsBeingDragged = false;
@@ -428,9 +471,7 @@
 
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
-                mIsBeingDragged = false;
-                mCurrPercentage = 0;
-                mActivePointerId = INVALID_POINTER;
+                touchEventFailFast();
                 break;
         }
 
@@ -450,13 +491,17 @@
             mReturningToStart = false;
         }
 
-        if (!isEnabled() || mReturningToStart || canChildScrollUp()) {
+        if (!isEnabled() || mReturningToStart || canChildScrollUp()
+                || ev.getY() < mTarget.getTop() || ev.getY() > mTarget.getBottom()) {
             // Fail fast if we're not in a state where a swipe is possible
+            // This includes if the swipe it outside mTarget
+            touchEventFailFast();
             return false;
         }
 
         switch (action) {
             case MotionEvent.ACTION_DOWN:
+                mInitialTargetY = mTarget.getTop();
                 mLastMotionY = mInitialMotionY = ev.getY();
                 mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                 mIsBeingDragged = false;
@@ -487,7 +532,7 @@
                         setTriggerPercentage(
                                 mAccelerateInterpolator.getInterpolation(
                                         yDiff / mDistanceToTriggerSync));
-                        updateContentOffsetTop((int) (yDiff));
+                        updateContentOffsetTop((int) (mInitialTargetY + yDiff));
                         if (mLastMotionY > y && mTarget.getTop() == getPaddingTop()) {
                             // If the user puts the view back at the top, we
                             // don't need to. This shouldn't be considered
@@ -514,28 +559,34 @@
 
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
-                mIsBeingDragged = false;
-                mCurrPercentage = 0;
-                mActivePointerId = INVALID_POINTER;
+                touchEventFailFast();
                 return false;
         }
 
         return true;
     }
 
+    private void touchEventFailFast() {
+        mIsBeingDragged = false;
+        mCurrPercentage = 0;
+        mActivePointerId = INVALID_POINTER;
+    }
+
     private void startRefresh() {
         removeCallbacks(mCancel);
         mReturnToStartPosition.run();
         setRefreshing(true);
-        mListener.onRefresh();
+        if (mListener != null) {
+            mListener.onRefresh();
+        }
     }
 
     private void updateContentOffsetTop(int targetTop) {
         final int currentTop = mTarget.getTop();
-        if (targetTop > mDistanceToTriggerSync) {
-            targetTop = (int) mDistanceToTriggerSync;
+        if (targetTop > (currentTop + mDistanceToTriggerSync)) {
+            targetTop = currentTop + ((int) mDistanceToTriggerSync);
         } else if (targetTop < 0) {
-            targetTop = 0;
+            targetTop = mTarget.getTop();
         }
         setTargetOffsetTopAndBottom(targetTop - currentTop);
     }