Skip to content

Commit

Permalink
fix: add renderScrollComponent for improved scrolling with pressabl…
Browse files Browse the repository at this point in the history
…es (#4)

* feat: define custom render scroll

* fix: forward ref issue

* feat: support proper tap trigger

* perf: memoize tap gesture

* chore: release 0.1.9-beta.0

* refactor: enabled behavior handling

* docs: readme

* fix: handle tap concurrency

* docs: info about the delayed tap trigger

* docs: rename section
  • Loading branch information
enzomanuelmangano authored Oct 8, 2024
1 parent 73e87d3 commit 2a360f7
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 66 deletions.
64 changes: 50 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ function BasicPressablesExample() {
return (
<View style={styles.container}>
<PressableScale style={styles.box} onPress={() => console.log('scale')} />
<PressableOpacity style={styles.box} onPress={() => console.log('opacity')} />
<PressableOpacity
style={styles.box}
onPress={() => console.log('opacity')}
/>
</View>
);
}

```

### Create a custom Pressable with createAnimatedPressable
Expand All @@ -49,15 +51,16 @@ function BasicPressablesExample() {
import { createAnimatedPressable } from 'pressto';

const PressableRotate = createAnimatedPressable((progress) => ({
transform: [
{ rotate: `${progress.value * Math.PI / 4}rad` },
],
transform: [{ rotate: `${(progress.value * Math.PI) / 4}rad` }],
}));

function CustomPressableExample() {
return (
<View style={styles.container}>
<PressableRotate style={styles.box} onPress={() => console.log('rotate')} />
<PressableRotate
style={styles.box}
onPress={() => console.log('rotate')}
/>
</View>
);
}
Expand All @@ -73,22 +76,32 @@ import { PressablesConfig } from 'pressto';
function App() {
return (
<View style={styles.container}>
<PressableRotate style={styles.box} onPress={() => console.log('rotate')} />
<PressableRotate
style={styles.box}
onPress={() => console.log('rotate')}
/>
<PressableScale style={styles.box} onPress={() => console.log('scale')} />
<PressableOpacity style={styles.box} onPress={() => console.log('opacity')} />
<PressableOpacity
style={styles.box}
onPress={() => console.log('opacity')}
/>
</View>
);
}

export default () => (
<PressablesConfig animationType="spring" config={{ mass: 2 }} globalHandlers={{
onPress: () => {
console.log('you can call haptics here');
}
}}>
<PressablesConfig
animationType="spring"
config={{ mass: 2 }}
globalHandlers={{
onPress: () => {
console.log('you can call haptics here');
},
}}
>
<App />
</PressablesConfig>
)
);
```

## API
Expand All @@ -109,6 +122,29 @@ A function to create custom animated pressables. It takes a worklet function tha

A component to configure global settings for all pressable components within its children.

## Use with ScrollView and FlatList/FlashList

`pressto` provides an optional custom scroll render component that enhances the scrolling experience when used with pressable components.

```jsx
import { renderScrollComponent } from 'pressto';
import { FlatList } from 'react-native';

function App() {
return (
// This works with all the lists that support the renderScrollComponent prop
<FlatList
renderScrollComponent={renderScrollComponent}
data={data}
renderItem={({ item }) => <PressableRotate style={styles.box} />}
/>
);
}
```

The `renderScrollComponent` function wraps the scroll view with additional functionality in order to allow smoother interactions between scrolling and pressable components, preventing unwanted activations during scroll gestures.
Applying the renderScrollComponent from `pressto` means that the tap gesture will be delayed for a short amount of time to understand if the tap gesture is a scroll or a tap gesture.

## Contributing

Contributions are welcome! Please see our [contributing guide](CONTRIBUTING.md) for more details.
Expand Down
50 changes: 23 additions & 27 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {
createAnimatedPressable,
PressableOpacity,
PressableScale,
PressablesConfig,
renderScrollComponent,
} from 'pressto';
import { StyleSheet, View } from 'react-native';
import { FlatList, StyleSheet, View } from 'react-native';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import { interpolate, interpolateColor } from 'react-native-reanimated';

Expand All @@ -18,7 +17,7 @@ const PressableRotate = createAnimatedPressable((progress) => {
backgroundColor: interpolateColor(
progress.value,
[0, 1],
['#e4e4e4', '#ffffff']
['#d1d1d1', '#000000']
),
shadowColor: '#ffffff',
shadowOffset: {
Expand All @@ -33,22 +32,12 @@ const PressableRotate = createAnimatedPressable((progress) => {
function App() {
return (
<View style={styles.container}>
<PressableRotate
style={styles.box}
onPress={() => {
console.log('tap rotate :)');
}}
/>
<PressableScale
style={styles.box}
onPress={() => {
console.log('tap scale :)');
}}
/>
<PressableOpacity
style={styles.box}
onPress={() => {
console.log('tap opacity :)');
<FlatList
contentContainerStyle={styles.container}
data={new Array(10).fill(0)}
renderScrollComponent={renderScrollComponent}
renderItem={() => {
return <PressableRotate style={styles.box} />;
}}
/>
</View>
Expand All @@ -57,18 +46,25 @@ function App() {

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#000000',
gap: 25,
paddingTop: 25,
backgroundColor: '#fff',
gap: 10,
},
box: {
width: 120,
width: '95%',
height: 120,
backgroundColor: '#cbcbcb',
elevation: 5,
shadowColor: '#000000',
shadowOffset: {
width: 0,
height: 0,
},
shadowOpacity: 0.5,
backgroundColor: 'red',
shadowRadius: 10,
borderRadius: 35,
borderCurve: 'continuous',
alignSelf: 'center',
},
});

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pressto",
"version": "0.1.8",
"version": "0.1.9-beta.0",
"description": "Some custom react native touchables",
"source": "./src/index.tsx",
"main": "./lib/commonjs/index.js",
Expand Down
100 changes: 76 additions & 24 deletions src/pressables/base.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import type { ViewProps, ViewStyle } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import type { AnimateProps, SharedValue } from 'react-native-reanimated';
import Animated, {
runOnJS,
useAnimatedReaction,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
Expand All @@ -12,6 +13,10 @@ import Animated, {
} from 'react-native-reanimated';
import { usePressablesConfig } from '../provider';
import type { PressableContextType } from '../provider/context';
import {
scrollableInfoShared,
useIsInInternalScrollContext,
} from './render-scroll';
import { unwrapSharedValue } from './utils';

type AnimatedViewProps = AnimateProps<ViewProps>;
Expand Down Expand Up @@ -80,29 +85,76 @@ const BasePressable: React.FC<BasePressableProps> = ({
return unwrapSharedValue(enabledProp);
}, [enabledProp]);

const gesture = Gesture.Tap()
.maxDuration(4000)
.onTouchesDown(() => {
if (!enabled.value) return;
active.value = true;
if (onPressInProvider != null) runOnJS(onPressInProvider)();
if (onPressIn != null) runOnJS(onPressIn)();
})
.onTouchesUp(() => {
if (!enabled.value) return;
if (onPressProvider != null) runOnJS(onPressProvider)();
if (onPress != null) runOnJS(onPress)();
})
.onFinalize(() => {
if (!enabled.value) return;
active.value = false;
if (onPressOutProvider != null) runOnJS(onPressOutProvider)();
if (onPressOut != null) runOnJS(onPressOut)();
});

if (typeof enabledProp === 'boolean') {
gesture.enabled(enabledProp);
}
const isInScrollContext = useIsInInternalScrollContext();
const isTapped = useSharedValue(false);

const onBegin = useCallback(() => {
'worklet';
if (!enabled.value) return;

active.value = true;
if (onPressInProvider != null) runOnJS(onPressInProvider)();
if (onPressIn != null) runOnJS(onPressIn)();
}, [active, enabled.value, onPressIn, onPressInProvider]);

useAnimatedReaction(
() => {
if (!isInScrollContext) {
return false;
}

return (
!scrollableInfoShared.value.isScrolling &&
isTapped.value &&
scrollableInfoShared.value.activatedTap
);
},
(activated, prevActivated) => {
if (activated && !prevActivated) {
return onBegin();
}
}
);

const gesture = useMemo(() => {
const tapGesture = Gesture.Tap()
.maxDuration(4000)
// check if enabledProp is a boolean
// if it's a boolean, use it to enable/disable the gesture
// if it's not a boolean, use the value of the enabled shared value (in each callback)
.enabled(typeof enabledProp === 'boolean' ? enabledProp : true)
.onTouchesDown(() => {
isTapped.value = true;
if (!isInScrollContext) {
return onBegin();
}
})
.onTouchesUp(() => {
if (!enabled.value || !active.value) return;
if (onPressProvider != null) runOnJS(onPressProvider)();
if (onPress != null) runOnJS(onPress)();
})
.onFinalize(() => {
isTapped.value = false;
if (!enabled.value || !active.value) return;
active.value = false;
if (onPressOutProvider != null) runOnJS(onPressOutProvider)();
if (onPressOut != null) runOnJS(onPressOut)();
});

return tapGesture;
}, [
active,
enabled.value,
enabledProp,
isInScrollContext,
isTapped,
onBegin,
onPress,
onPressOut,
onPressOutProvider,
onPressProvider,
]);

const rAnimatedStyle = useAnimatedStyle(() => {
return animatedStyle ? animatedStyle(progress) : {};
Expand Down
1 change: 1 addition & 0 deletions src/pressables/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './custom';
export * from './hoc';
export * from './render-scroll';
Loading

0 comments on commit 2a360f7

Please sign in to comment.