Skip to content

Commit

Permalink
OverScrollView for android
Browse files Browse the repository at this point in the history
  • Loading branch information
savelichalex committed Nov 18, 2021
1 parent 8480b95 commit c1c8d32
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 35 deletions.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new ReactOverScrollPackage());

return packages;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package tonlabs.uikit;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.annotation.Nonnull;

public class ReactOverScrollPackage implements ReactPackage {
@Nonnull
@Override
public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
return Collections.emptyList();
}

@Nonnull
@Override
public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
List<ViewManager> modules = new ArrayList<>();
modules.add(new ReactOverScrollViewManager());
return modules;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package tonlabs.uikit;

import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.events.NativeGestureUtil;
import com.facebook.react.views.scroll.ReactScrollView;

import android.content.Context;
import android.graphics.Canvas;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import androidx.core.view.MotionEventCompat;

import com.facebook.react.views.scroll.ReactScrollViewHelper;
import com.facebook.react.views.scroll.ScrollEvent;
import com.facebook.react.views.scroll.ScrollEventType;
import com.mixiaoxiao.overscroll.OverScrollDelegate;
import com.mixiaoxiao.overscroll.OverScrollDelegate.OverScrollable;
import com.mixiaoxiao.overscroll.PathScroller;

import java.lang.reflect.Field;

/**
* https://github.com/Mixiaoxiao/OverScroll-Everywhere
*
* @author Mixiaoxiao 2016-08-31
*/
public class ReactOverScrollView extends ReactScrollView implements OverScrollable {

private OverScrollDelegate mOverScrollDelegate;

private boolean mDragging;

// ===========================================================
// Constructors
// ===========================================================
public ReactOverScrollView(ReactContext context) {
super(context);
createOverScrollDelegate(context);
}

// ===========================================================
// createOverScrollDelegate
// ===========================================================
private void createOverScrollDelegate(Context context) {
mOverScrollDelegate = new OverScrollDelegate(this);
}

// ===========================================================
// Delegate
// ===========================================================
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
if (action == MotionEvent.ACTION_DOWN) {
NativeGestureUtil.notifyNativeGestureStarted(this, ev);
// TODO: it's fired twice for some reason
ReactScrollViewHelper.emitScrollBeginDragEvent(this);
mDragging = true;
}
if (mOverScrollDelegate.onInterceptTouchEvent(ev)) {
return true;
}
return super.onInterceptTouchEvent(ev);
}



@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO: mScrollEnabled from ReactScrollView
final int action = MotionEventCompat.getActionMasked(event);
if (action == MotionEvent.ACTION_UP && mDragging) {
// TODO: velocity
// TODO: it's fired twice for some reason
ReactScrollViewHelper.emitScrollEndDragEvent(this, 0, 0);
mDragging = false;
// return true;
}
int offset = this.superComputeVerticalScrollOffset();
int range = this.superComputeVerticalScrollRange() - this.superComputeVerticalScrollExtent();
Log.d("ReactOverScrollView", String.format("range: %d, offset: %d, canScrollUp: %b, canScrollDown: %b", range, offset, offset > 0, offset < range - 1));
if (mOverScrollDelegate.onTouchEvent(event)) {
try {
// TODO: create a wrapper with getter method
Field mStateField = mOverScrollDelegate.getClass().getDeclaredField("mState"); //NoSuchFieldException
mStateField.setAccessible(true);
int mState = (int) mStateField.get(mOverScrollDelegate);

// TODO: create a wrapper with getter method
Field mOffsetYField = mOverScrollDelegate.getClass().getDeclaredField("mOffsetY"); //NoSuchFieldException
mOffsetYField.setAccessible(true);
float mOffsetY = (float) mOffsetYField.get(mOverScrollDelegate);

if (mState == OverScrollDelegate.OS_DRAG_TOP || mState == OverScrollDelegate.OS_DRAG_BOTTOM) {
ReactContext reactContext = (ReactContext) this.getContext();
int surfaceId = UIManagerHelper.getSurfaceId(reactContext);
UIManagerHelper.getEventDispatcherForReactTag(reactContext, this.getId()).dispatchEvent(ScrollEvent.obtain(surfaceId, this.getId(), ScrollEventType.SCROLL, 0, (int) (-1 * mOffsetY), 0, 0, 0, 0, 0, 0));
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (NullPointerException e) {
e.printStackTrace();
}
return true;
}
return super.onTouchEvent(event);
}

@Override
public void draw(Canvas canvas) {
mOverScrollDelegate.draw(canvas);
}

@Override
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX,
int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
return mOverScrollDelegate.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY,
maxOverScrollX, maxOverScrollY, isTouchEvent);
}

// ===========================================================
// OverScrollable, aim to call view internal methods
// ===========================================================

@Override
public int superComputeVerticalScrollExtent() {
return super.computeVerticalScrollExtent();
}

@Override
public int superComputeVerticalScrollOffset() {
return super.computeVerticalScrollOffset();
}

@Override
public int superComputeVerticalScrollRange() {
return super.computeVerticalScrollRange();
}

@Override
public void superOnTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
}

@Override
public void superDraw(Canvas canvas) {
super.draw(canvas);
}

@Override
public boolean superAwakenScrollBars() {
return super.awakenScrollBars();
}

@Override
public boolean superOverScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX,
int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX,
maxOverScrollY, isTouchEvent);
}

@Override
public View getOverScrollableView() {
return this;
}

@Override
public OverScrollDelegate getOverScrollDelegate() {
return mOverScrollDelegate;
}

@Override
public void scrollTo(int x, int y) {
try {
// TODO: create a wrapper with getter method
Field mStateField = mOverScrollDelegate.getClass().getDeclaredField("mState"); //NoSuchFieldException
mStateField.setAccessible(true);
int mState = (int) mStateField.get(mOverScrollDelegate);

// TODO: create a wrapper with getter method
Field mOffsetYField = mOverScrollDelegate.getClass().getDeclaredField("mOffsetY"); //NoSuchFieldException
mOffsetYField.setAccessible(true);

if (mState == OverScrollDelegate.OS_DRAG_TOP || mState == OverScrollDelegate.OS_DRAG_BOTTOM) {
mStateField.set(mOverScrollDelegate, OverScrollDelegate.OS_NONE);
mOffsetYField.set(mOverScrollDelegate, 0.0F);
invalidate();
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (NullPointerException e) {
e.printStackTrace();
}
super.scrollTo(x, y);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package tonlabs.uikit;

import androidx.annotation.NonNull;

import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.views.scroll.ReactScrollView;
import com.facebook.react.views.scroll.ReactScrollViewManager;

import org.jetbrains.annotations.NotNull;


@ReactModule(name = ReactOverScrollViewManager.REACT_CLASS)
public class ReactOverScrollViewManager extends ReactScrollViewManager {
public static final String REACT_CLASS = "RCTScrollView";

@NonNull
@NotNull
@Override
public ReactScrollView createViewInstance(ThemedReactContext reactContext) {
return new ReactOverScrollView(reactContext);
}

@Override
public boolean canOverrideExistingModule() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ export default function (
return;
}

yIsNegative.value = y <= 0;
// yIsNegative.value = y <= 0;

// TODO: probably unneeded
if (ctx.yWithoutRubberBand == null) {
ctx.yWithoutRubberBand = 0;
}

if (parentScrollHandlerActive) {
if (
Expand Down Expand Up @@ -108,7 +113,12 @@ export default function (
// scrollTo reset real y, so we need to count it ourselves
ctx.yWithoutRubberBand -= y;
shift.value = Math.max(shift.value - y, 0 - largeTitleHeight.value);
scrollTo(scrollRef, 0, 0, false);
// 1 here is to trick OverScrollView
// the algorithm is the following: https://github.com/Mixiaoxiao/OverScroll-Everywhere/blob/master/OverScroll/src/com/mixiaoxiao/overscroll/OverScrollDelegate.java#L360-L368
// Basically it tries to understand, is there a room to scroll up and down
// so if we set y to 0 the lib would think that it needs to apply
// overscroll animation, but in reality we don't want it here
scrollTo(scrollRef, 0, 1, false);
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,8 @@ export function wrapScrollableComponent<Props extends ScrollViewProps>(

const { onLayout, onContentSizeChange } = useHasScroll();

const {
ref,
panGestureHandlerRef,
scrollHandler,
gestureHandler,
registerScrollable,
unregisterScrollable,
} = React.useContext(ScrollableContext);
const { ref, scrollHandler, gestureHandler, registerScrollable, unregisterScrollable } =
React.useContext(ScrollableContext);

React.useEffect(() => {
if (registerScrollable) {
Expand All @@ -47,31 +41,15 @@ export function wrapScrollableComponent<Props extends ScrollViewProps>(
});

return (
<PanGestureHandler
ref={panGestureHandlerRef}
shouldCancelWhenOutside={false}
onGestureEvent={gestureHandler}
simultaneousHandlers={nativeGestureRef}
>
<Animated.View style={{ flex: 1 }}>
<NativeViewGestureHandler
ref={nativeGestureRef}
disallowInterruption
shouldCancelWhenOutside={false}
>
{/* @ts-ignore */}
<AnimatedScrollable
{...props}
ref={ref}
overScrollMode="never"
onScrollBeginDrag={scrollHandler}
scrollEventThrottle={16}
onLayout={onLayout}
onContentSizeChange={onContentSizeChange}
/>
</NativeViewGestureHandler>
</Animated.View>
</PanGestureHandler>
<AnimatedScrollable
{...props}
ref={ref}
overScrollMode="never"
onScrollBeginDrag={scrollHandler}
scrollEventThrottle={16}
onLayout={onLayout}
onContentSizeChange={onContentSizeChange}
/>
);
}

Expand Down

0 comments on commit c1c8d32

Please sign in to comment.