Skip to content

Commit

Permalink
Swipe handler (software-mansion#145)
Browse files Browse the repository at this point in the history
* Add basic support for swipe/fling

* Add project files

* Bump gradle version
Add number of Touches handling
Remove useless code from iOS pack
Add android Fling Gesture Handler

* Update react-native 🎉

* Rethink max duration on Android

* Naname prop to 'numberOfTouches'

* Update RN 🎉

* Rename comment

* Fix issues

* Rethink directions on ios

* Add android multi-directional fling handling

* Remove useless code and style issue

* Update gradle, add yarn.lock

* Shadow useless YellowBox

* Rename issue and remove ios Example iOS file changes revert

* Update README.md

* Update README.md
  • Loading branch information
kmagiera authored Apr 17, 2018
1 parent ad34f22 commit 2a4703a
Show file tree
Hide file tree
Showing 15 changed files with 351 additions and 2 deletions.
5 changes: 5 additions & 0 deletions Example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import PanAndScroll from './panAndScroll';
import PanResponder from './panResponder';
import Bouncing from './bouncing';
import HorizontalDrawer from './horizontalDrawer';
import Fling from './fling/index';
import ChatHeads from './chatHeads';
import { ComboWithGHScroll, ComboWithRNScroll } from './combo';

Expand Down Expand Up @@ -41,6 +42,10 @@ const SCREENS = {
screen: PanAndScroll,
title: 'Horizontal pan or tap in ScrollView',
},
Fling: {
screen: Fling,
title: 'Flinghandler',
},
PanResponder: { screen: PanResponder },
Bouncing: { screen: Bouncing, title: 'Twist & bounce back animation' },
// ChatHeads: {
Expand Down
104 changes: 104 additions & 0 deletions Example/fling/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { Component } from 'react';
import { Animated, Dimensions, StyleSheet, Text, View } from 'react-native';
import {
FlingGestureHandler,
Directions,
State,
} from 'react-native-gesture-handler';

import { USE_NATIVE_DRIVER } from '../config';

const windowWidth = Dimensions.get('window').width;
const circleRadius = 30;

class Fling extends Component {
constructor(props) {
super(props);
this._touchX = new Animated.Value(windowWidth / 2 - circleRadius);
this._translateX = Animated.add(
this._touchX,
new Animated.Value(-circleRadius)
);
this._translateY = new Animated.Value(0);
}

_onVerticalFlingHandlerStateChange = ({ nativeEvent }, offset) => {
if (nativeEvent.oldState === State.ACTIVE) {
Animated.spring(this._touchX, {
toValue: this._touchX._value + offset,
useNativeDriver: USE_NATIVE_DRIVER,
}).start();
}
};

_onHorizontalFlingHandlerStateChange = ({ nativeEvent }) => {
if (nativeEvent.oldState === State.ACTIVE) {
Animated.spring(this._translateY, {
toValue: this._translateY._value + 10,
useNativeDriver: USE_NATIVE_DRIVER,
}).start();
}
};

render() {
return (
<FlingGestureHandler
direction={Directions.UP}
numberOfPointers={2}
onHandlerStateChange={this._onHorizontalFlingHandlerStateChange}>
<FlingGestureHandler
direction={Directions.RIGHT | Directions.LEFT}
onHandlerStateChange={ev =>
this._onVerticalFlingHandlerStateChange(ev, -10)
}>
<View style={styles.horizontalPan}>
<Animated.View
style={[
styles.circle,
{
transform: [
{
translateX: this._translateX,
},
{
translateY: this._translateY,
},
],
},
]}
/>
</View>
</FlingGestureHandler>
</FlingGestureHandler>
);
}
}

export default class Example extends Component {
render() {
return (
<View>
<Fling />
<Text>
Move up (with two fingers) or right/left (with one finger) and watch
magic happens
</Text>
</View>
);
}
}

const styles = StyleSheet.create({
horizontalPan: {
backgroundColor: '#f76f41',
height: 300,
justifyContent: 'center',
marginVertical: 10,
},
circle: {
backgroundColor: '#42a5f5',
borderRadius: circleRadius,
height: circleRadius * 2,
width: circleRadius * 2,
},
});
14 changes: 14 additions & 0 deletions GestureHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ ReactNativeBridgeEventPlugin.processEventTypes({

const State = RNGestureHandlerModule.State;

const Directions = RNGestureHandlerModule.Direction;

let handlerTag = 1;
const handlerIDToTag = {};

Expand Down Expand Up @@ -305,6 +307,16 @@ const TapGestureHandler = createHandler(
},
{}
);

const FlingGestureHandler = createHandler(
'FlingGestureHandler',
{
numberOfPointers: PropTypes.number,
direction: PropTypes.number,
},
{}
);

const LongPressGestureHandler = createHandler(
'LongPressGestureHandler',
{
Expand Down Expand Up @@ -597,6 +609,7 @@ export {
WrappedWebView as WebView,
NativeViewGestureHandler,
TapGestureHandler,
FlingGestureHandler,
LongPressGestureHandler,
PanGestureHandler,
PinchGestureHandler,
Expand All @@ -612,4 +625,5 @@ export {
gestureHandlerRootHOC,
Swipeable,
DrawerLayout,
Directions,
};
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Here are a gesture recognizers currently available in the package:
- `PanGestureHandler`
- `PinchGestureHandler`
- `RotationGestureHandler`
- `FlingGestureHandler`

Whenever you use a native component that should handle touch events you can either wrap it with `NativeViewGestureHandler` or import wrapper component exported by the library instead of importing it from `react-native` package. Here is the list of available components:
- `ScrollView`
Expand Down Expand Up @@ -162,6 +163,11 @@ Library exports a `State` object that provides a number of constants used to exp

#### `RotationGestureHandler`

#### `FlingGestureHandler` extra properties

- `direction`
- `numberOfPointers`

## Buttons

Gesture handler library provides native components that can act as buttons. These can be treated as a replacement to `TouchableHighlight` or `TouchableOpacity` from RN core. Gesture handler's buttons recognize touches in native which makes the recognition process deterministic, allows for rendering ripples on Android in highly performant way (`TouchableNativeFeedback` requires that touch event does a roundtrip to JS before we can update ripple effect, which makes ripples lag a bit on older phones), and provides native and platform default interaction for buttons that are placed in a scrollable container (in which case the interaction is slightly delayed to prevent button from highlighting when you fling).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.swmansion.gesturehandler;

import android.os.Handler;
import android.view.MotionEvent;

public class FlingGestureHandler extends GestureHandler<FlingGestureHandler> {
private static final long DEFAULT_MAX_DURATION_MS = 800;
private static final long DEFAULT_MIN_ACCEPTABLE_DELTA = 160;
private static final int DEFAULT_DIRECTION = DIRECTION_RIGHT;
private static final int DEFAULT_NUMBER_OF_TOUCHES_REQUIRED = 1;

private long mMaxDurationMs = DEFAULT_MAX_DURATION_MS;
private long mMinAcceptableDelta = DEFAULT_MIN_ACCEPTABLE_DELTA;
private int mDirection = DEFAULT_DIRECTION;
private int mNumberOfPointersRequired = DEFAULT_NUMBER_OF_TOUCHES_REQUIRED;
private float mStartX, mStartY;

private Handler mHandler;
private int mMaxNumberOfPointersSimultaneously;

private final Runnable mFailDelayed = new Runnable() {
@Override
public void run() {
fail();
}
};

public void setNumberOfPointersRequired(int numberOfPointersRequired) {
mNumberOfPointersRequired = numberOfPointersRequired;
}

public void setDirection(int direction) {
mDirection = direction;
}

private void startFling(MotionEvent event) {
mStartX = event.getRawX();
mStartY = event.getRawY();
begin();
mMaxNumberOfPointersSimultaneously = 1;
if (mHandler == null) {
mHandler = new Handler();
} else {
mHandler.removeCallbacksAndMessages(null);
}
mHandler.postDelayed(mFailDelayed, mMaxDurationMs);
}

private boolean tryEndFling(MotionEvent event) {
if (mMaxNumberOfPointersSimultaneously == mNumberOfPointersRequired &&
(((mDirection & DIRECTION_RIGHT) != 0 &&
event.getRawX() - mStartX > mMinAcceptableDelta) ||
((mDirection & DIRECTION_LEFT) !=0 &&
mStartX - event.getRawX() > mMinAcceptableDelta) ||
((mDirection & DIRECTION_UP) !=0 &&
mStartY - event.getRawY() > mMinAcceptableDelta) ||
((mDirection & DIRECTION_DOWN) !=0 &&
event.getRawY() - mStartY > mMinAcceptableDelta))) {
mHandler.removeCallbacksAndMessages(null);
activate();
end();
return true;
} else {
return false;
}
}

private void endFling(MotionEvent event) {
if (!tryEndFling(event)) {
fail();
}

}

@Override
protected void onHandle(MotionEvent event) {
int state = getState();

if (state == STATE_UNDETERMINED) {
startFling(event);
}


if (state == STATE_BEGAN) {
tryEndFling(event);
if (event.getPointerCount() > mMaxNumberOfPointersSimultaneously) {
mMaxNumberOfPointersSimultaneously = event.getPointerCount();
}

int action = event.getActionMasked();
if (action == MotionEvent.ACTION_UP) {
endFling(event);
}
}
}

@Override
protected void onCancel() {
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
}

@Override
protected void onReset() {
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ public class GestureHandler<T extends GestureHandler> {
private static final int HIT_SLOP_WIDTH_IDX = 4;
private static final int HIT_SLOP_HEIGHT_IDX = 5;

public static final int DIRECTION_RIGHT = 1;
public static final int DIRECTION_LEFT = 2;
public static final int DIRECTION_UP = 4;
public static final int DIRECTION_DOWN = 8;

private int mTag;
private View mView;
private int mState = STATE_UNDETERMINED;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.swmansion.gesturehandler.FlingGestureHandler;
import com.swmansion.gesturehandler.GestureHandler;
import com.swmansion.gesturehandler.LongPressGestureHandler;
import com.swmansion.gesturehandler.NativeViewGestureHandler;
Expand Down Expand Up @@ -70,6 +71,8 @@ public class RNGestureHandlerModule extends ReactContextBaseJavaModule {
private static final String KEY_PAN_MIN_POINTERS = "minPointers";
private static final String KEY_PAN_MAX_POINTERS = "maxPointers";
private static final String KEY_PAN_AVG_TOUCHES = "avgTouches";
private static final String KEY_NUMBER_OF_POINTERS = "numberOfPointers";
private static final String KEY_DIRECTION= "direction";

private abstract static class HandlerFactory<T extends GestureHandler>
implements RNGestureHandlerEventDataExtractor<T> {
Expand Down Expand Up @@ -316,6 +319,34 @@ public void extractEventData(PinchGestureHandler handler, WritableMap eventData)
}
}

private static class FlingGestureHandlerFactory extends HandlerFactory<FlingGestureHandler> {
@Override
public Class<FlingGestureHandler> getType() {
return FlingGestureHandler.class;
}

@Override
public String getName() {
return "FlingGestureHandler";
}

@Override
public FlingGestureHandler create(Context context) {
return new FlingGestureHandler();
}

@Override
public void configure(FlingGestureHandler handler, ReadableMap config) {
super.configure(handler, config);
if (config.hasKey(KEY_NUMBER_OF_POINTERS)) {
handler.setNumberOfPointersRequired(config.getInt(KEY_NUMBER_OF_POINTERS));
}
if (config.hasKey(KEY_DIRECTION)) {
handler.setDirection(config.getInt(KEY_DIRECTION));
}
}
}

private static class RotationGestureHandlerFactory extends HandlerFactory<RotationGestureHandler> {
@Override
public Class<RotationGestureHandler> getType() {
Expand All @@ -334,6 +365,7 @@ public RotationGestureHandler create(Context context) {

@Override
public void extractEventData(RotationGestureHandler handler, WritableMap eventData) {
eventData.putDouble("rotation", handler.getRotation());
eventData.putDouble("rotation", handler.getRotation());
eventData.putDouble("anchorX", PixelUtil.toDIPFromPixel(handler.getAnchorX()));
eventData.putDouble("anchorY", PixelUtil.toDIPFromPixel(handler.getAnchorY()));
Expand All @@ -359,7 +391,8 @@ public void onStateChange(GestureHandler handler, int newState, int oldState) {
new LongPressGestureHandlerFactory(),
new PanGestureHandlerFactory(),
new PinchGestureHandlerFactory(),
new RotationGestureHandlerFactory()
new RotationGestureHandlerFactory(),
new FlingGestureHandlerFactory()
};
private final RNGestureHandlerRegistry mRegistry = new RNGestureHandlerRegistry();

Expand Down Expand Up @@ -450,6 +483,11 @@ public void handleClearJSResponder() {
"CANCELLED", GestureHandler.STATE_CANCELLED,
"FAILED", GestureHandler.STATE_FAILED,
"END", GestureHandler.STATE_END
), "Direction", MapBuilder.of(
"RIGHT", GestureHandler.DIRECTION_RIGHT,
"LEFT", GestureHandler.DIRECTION_LEFT,
"UP", GestureHandler.DIRECTION_UP,
"DOWN", GestureHandler.DIRECTION_DOWN
));
}

Expand Down
4 changes: 4 additions & 0 deletions ios/Handlers/RNFlingHandler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#import "RNGestureHandler.h"

@interface RNFlingGestureHandler : RNGestureHandler
@end
Loading

0 comments on commit 2a4703a

Please sign in to comment.