/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.react.views.swiperefresh; import android.support.v4.widget.SwipeRefreshLayout; import android.view.MotionEvent; import android.view.ViewConfiguration; import com.facebook.react.bridge.ReactContext; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.events.NativeGestureUtil; /** * Basic extension of {@link SwipeRefreshLayout} with ReactNative-specific functionality. */ public class ReactSwipeRefreshLayout extends SwipeRefreshLayout { private static final float DEFAULT_CIRCLE_TARGET = 64; private boolean mDidLayout = false; private boolean mRefreshing = false; private float mProgressViewOffset = 0; private int mTouchSlop; private float mPrevTouchX; private boolean mIntercepted; public ReactSwipeRefreshLayout(ReactContext reactContext) { super(reactContext); mTouchSlop = ViewConfiguration.get(reactContext).getScaledTouchSlop(); } @Override public void setRefreshing(boolean refreshing) { mRefreshing = refreshing; // `setRefreshing` must be called after the initial layout otherwise it // doesn't work when mounting the component with `refreshing = true`. // Known Android issue: https://code.google.com/p/android/issues/detail?id=77712 if (mDidLayout) { super.setRefreshing(refreshing); } } public void setProgressViewOffset(float offset) { mProgressViewOffset = offset; // The view must be measured before calling `getProgressCircleDiameter` so // don't do it before the initial layout. if (mDidLayout) { int diameter = getProgressCircleDiameter(); int start = Math.round(PixelUtil.toPixelFromDIP(offset)) - diameter; int end = Math.round(PixelUtil.toPixelFromDIP(offset + DEFAULT_CIRCLE_TARGET) - diameter); setProgressViewOffset(false, start, end); } } @Override public void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (!mDidLayout) { mDidLayout = true; // Update values that must be set after initial layout. setProgressViewOffset(mProgressViewOffset); setRefreshing(mRefreshing); } } /** * {@link SwipeRefreshLayout} overrides {@link ViewGroup#requestDisallowInterceptTouchEvent} and * swallows it. This means that any component underneath SwipeRefreshLayout will now interact * incorrectly with Views that are above SwipeRefreshLayout. We fix that by transmitting the call * to this View's parents. */ @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (getParent() != null) { getParent().requestDisallowInterceptTouchEvent(disallowIntercept); } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (shouldInterceptTouchEvent(ev) && super.onInterceptTouchEvent(ev)) { NativeGestureUtil.notifyNativeGestureStarted(this, ev); return true; } return false; } /** * {@link SwipeRefreshLayout} completely bypasses ViewGroup's "disallowIntercept" by overriding * {@link ViewGroup#onInterceptTouchEvent} and never calling super.onInterceptTouchEvent(). * This means that horizontal scrolls will always be intercepted, even though they shouldn't, so * we have to check for that manually here. */ private boolean shouldInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mPrevTouchX = ev.getX(); mIntercepted = false; break; case MotionEvent.ACTION_MOVE: final float eventX = ev.getX(); final float xDiff = Math.abs(eventX - mPrevTouchX); if (mIntercepted || xDiff > mTouchSlop) { mIntercepted = true; return false; } } return true; } }