Skip to content

Commit

Permalink
Add pagingEnabled to HorizontalScrollView
Browse files Browse the repository at this point in the history
Summary:
This adds support for pagingEnabled to the HorizontalScrollView.

This is an initial implementation.

Because Android doesn't provide great details about what is happening with a scroll view after you are done touching it, we have some post touch handling.  This is kicked off either by touch up or a fling call.
Once we are doing that handling, we start a runnable that basically checks if we are still scrolling.  If we are, we just schedule that runnable again and check a frame later.  If we are done scrolling (no onScrollChanged since we last fired), we could be in one of two states, the fling is done or we are done snapping to the page boundary.  If we are in the fling done case, we then check if we need to scroll to a page boundary.  If so, we call smoothScrollTo and schedule ourself to check onScroll events again until done with that scroll.  If we are done with both (either we only did momentum scroll or we did that and then snapped to page), we can then fire the final event and stop checking.  This logic is all in handlePostTouchScrolling.

Because of the decision to only do page scrolling after momentum ends, we do allow you to scroll through with momentum a number of pages and the transition can be a little strange where it stops a sec and then slides to be page aligned.  As a follow up, we can probably smooth that up by changing the value we pass to super.fling() that would adjust it to be let momentum carry it to the page boundary.

Reviewed By: weicool

Differential Revision: D3207608

fb-gh-sync-id: 02f62970ed9a5e3a5f9c0d959402756bc4b3699e
fbshipit-source-id: 02f62970ed9a5e3a5f9c0d959402756bc4b3699e
  • Loading branch information
Dave Miller authored and Facebook Github Bot 7 committed May 5, 2016
1 parent a26afd2 commit a3146e4
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 24 deletions.
1 change: 0 additions & 1 deletion Libraries/Components/ScrollView/ScrollView.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ var ScrollView = React.createClass({
* When true, the scroll view stops on multiples of the scroll view's size
* when scrolling. This can be used for horizontal pagination. The default
* value is false.
* @platform ios
*/
pagingEnabled: PropTypes.bool,
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import javax.annotation.Nullable;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
Expand All @@ -35,10 +36,11 @@ public class ReactHorizontalScrollView extends HorizontalScrollView implements

private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();

private boolean mActivelyScrolling;
private @Nullable Rect mClippingRect;
private boolean mDoneFlinging;
private boolean mDragging;
private boolean mFlinging;
private boolean mPagingEnabled = false;
private @Nullable Runnable mPostTouchRunnable;
private boolean mRemoveClippedSubviews;
private boolean mScrollEnabled = true;
private boolean mSendMomentumEvents;
Expand Down Expand Up @@ -71,6 +73,10 @@ public void setScrollEnabled(boolean scrollEnabled) {
mScrollEnabled = scrollEnabled;
}

public void setPagingEnabled(boolean pagingEnabled) {
mPagingEnabled = pagingEnabled;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);
Expand All @@ -95,9 +101,7 @@ protected void onScrollChanged(int x, int y, int oldX, int oldY) {
updateClippingRect();
}

if (mFlinging) {
mDoneFlinging = false;
}
mActivelyScrolling = true;

ReactScrollViewHelper.emitScrollEvent(this);
}
Expand Down Expand Up @@ -129,33 +133,23 @@ public boolean onTouchEvent(MotionEvent ev) {
if (action == MotionEvent.ACTION_UP && mDragging) {
ReactScrollViewHelper.emitScrollEndDragEvent(this);
mDragging = false;
// After the touch finishes, we may need to do some scrolling afterwards either as a result
// of a fling or because we need to page align the content
handlePostTouchScrolling();
}
return super.onTouchEvent(ev);
}

@Override
public void fling(int velocityX) {
super.fling(velocityX);
if (mSendMomentumEvents) {
mFlinging = true;
ReactScrollViewHelper.emitScrollMomentumBeginEvent(this);
Runnable r = new Runnable() {
@Override
public void run() {
if (mDoneFlinging) {
mFlinging = false;
ReactScrollViewHelper.emitScrollMomentumEndEvent(ReactHorizontalScrollView.this);
} else {
mDoneFlinging = true;
ReactHorizontalScrollView.this.postOnAnimationDelayed(this, ReactScrollViewHelper.MOMENTUM_DELAY);
}
}
};
postOnAnimationDelayed(r, ReactScrollViewHelper.MOMENTUM_DELAY);
if (mPagingEnabled) {
smoothScrollToPage(velocityX);
} else {
super.fling(velocityX);
}
handlePostTouchScrolling();
}


@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Expand Down Expand Up @@ -210,4 +204,79 @@ public void draw(Canvas canvas) {
}
super.draw(canvas);
}

/**
* This handles any sort of scrolling that may occur after a touch is finished. This may be
* momentum scrolling (fling) or because you have pagingEnabled on the scroll view. Because we
* don't get any events from Android about this lifecycle, we do all our detection by creating a
* runnable that checks if we scrolled in the last frame and if so assumes we are still scrolling.
*/
@TargetApi(16)
private void handlePostTouchScrolling() {
// If we aren't going to do anything (send events or snap to page), we can early out.
if (!mSendMomentumEvents && !mPagingEnabled) {
return;
}

// Check if we are already handling this which may occur if this is called by both the touch up
// and a fling call
if (mPostTouchRunnable != null) {
return;
}

if (mSendMomentumEvents) {
ReactScrollViewHelper.emitScrollMomentumBeginEvent(this);
}

mActivelyScrolling = false;
mPostTouchRunnable = new Runnable() {

private boolean mSnappingToPage = false;

@Override
public void run() {
if (mActivelyScrolling) {
// We are still scrolling so we just post to check again a frame later
mActivelyScrolling = false;
ReactHorizontalScrollView.this.postOnAnimationDelayed(this, ReactScrollViewHelper.MOMENTUM_DELAY);
} else {
boolean doneWithAllScrolling = true;
if (mPagingEnabled && !mSnappingToPage) {
// Only if we have pagingEnabled and we have not snapped to the page do we
// need to continue checking for the scroll. And we cause that scroll by asking for it
mSnappingToPage = true;
smoothScrollToPage(0);
doneWithAllScrolling = false;
}
if (doneWithAllScrolling) {
if (mSendMomentumEvents) {
ReactScrollViewHelper.emitScrollMomentumEndEvent(ReactHorizontalScrollView.this);
}
ReactHorizontalScrollView.this.mPostTouchRunnable = null;
} else {
ReactHorizontalScrollView.this.postOnAnimationDelayed(this, ReactScrollViewHelper.MOMENTUM_DELAY);
}
}
}
};
postOnAnimationDelayed(mPostTouchRunnable, ReactScrollViewHelper.MOMENTUM_DELAY);
}

/**
* This will smooth scroll us to the nearest page boundary
* It currently just looks at where the content is relative to the page and slides to the nearest
* page. It is intended to be run after we are done scrolling, and handling any momentum
* scrolling.
*/
private void smoothScrollToPage(int velocity) {
int width = getWidth();
int currentX = getScrollX();
// TODO (t11123799) - Should we do anything beyond linear accounting of the velocity
int predictedX = currentX + velocity;
int page = currentX / width;
if (predictedX > page * width + width / 2) {
page = page + 1;
}
smoothScrollTo(page * width, getScrollY());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ public void setSendMomentumEvents(ReactHorizontalScrollView view, boolean sendMo
view.setSendMomentumEvents(sendMomentumEvents);
}

@ReactProp(name = "pagingEnabled")
public void setPagingEnabled(ReactHorizontalScrollView view, boolean pagingEnabled) {
view.setPagingEnabled(pagingEnabled);
}

@Override
public void receiveCommand(
ReactHorizontalScrollView scrollView,
Expand Down

2 comments on commit a3146e4

@stereodenis
Copy link
Contributor

@stereodenis stereodenis commented on a3146e4 May 26, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmmiller what do you think about this issue? #7780

@dmmiller
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented on the issue. Short answer, we didn't add vertical support. We only needed horizontal internally at FB so that's all we did. You are welcome to put up a PR and I'm happy to review it.

Please sign in to comment.