Skip to content

Commit c6c0a3e

Browse files
authoredAug 24, 2020
feat: Spring animation, over scroll and Android bug fix (#39)
1 parent 9b5ca93 commit c6c0a3e

File tree

2 files changed

+100
-31
lines changed

2 files changed

+100
-31
lines changed
 

‎README.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,15 @@ This is the list of exclusive props that are meant to be used to customise the b
161161
| `snapPoints` | yes | `Array<string \| number>` | Array of numbers and/or percentages that indicate the different resting positions of the bottom sheet (in dp or %), **starting from the top**. If a percentage is used, that would translate to the relative amount of the total window height. If you want that percentage to be calculated based on the parent available space instead, for example to account for safe areas or navigation bars, use it in combination with `topInset` prop |
162162
| `initialSnapIndex` | yes | `number` | Index that references the initial resting position of the drawer, **starting from the top** |
163163
| `renderHandle` | yes | `() => React.ReactNode` | Render prop for the handle, should return a React Element |
164+
| `animationConfig` | no | `string` | `timing` (default) or `spring` |
164165
| `onSettle` | no | `(index: number) => void` | Callback that is executed right after the bottom sheet settles in one of the snapping points. The new index is provided on the callback |
165166
| `animatedPosition` | no | `Animated.Value<number>` | Animated value that tracks the position of the drawer, being: 0 => closed, 1 => fully opened |
166167
| `animationConfig` | no | `{ duration: number, easing: Animated.EasingFunction }` | Timing configuration for the animation, by default it uses a duration of 250ms and easing fn `Easing.inOut(Easing.linear)` |
167168
| `topInset` | no | `number` | This value is useful to provide an offset (in dp) when applying percentages for snapping points |
168169
| `innerRef` | no | `RefObject` | Ref to the inner scrollable component (ScrollView, FlatList or SectionList), so that you can call its imperative methods. For instance, calling `scrollTo` on a ScrollView. In order to so, you have to use `getNode` as well, since it's wrapped into an _animated_ component: `ref.current.getNode().scrollTo({y: 0, animated: true})` |
169170
| `containerStyle` | no | `StyleProp<ViewStyle>` | Style to be applied to the container (Handle and Content) |
171+
| `friction` | no | `number` | Factor of resistance when the gesture is released. A value of 0 offers maximum * acceleration, whereas 1 acts as the opposite. Defaults to 0.95 |
172+
| `enableOverScroll` | yes | `boolean` | Allow drawer to be dragged beyond lowest snap point |
170173

171174

172175
### Inherited
@@ -193,11 +196,11 @@ import { useDimensions } from '@react-native-community/hooks'
193196

194197
const useOrientation = () => {
195198
const { width, height } = useDimensions().window;
196-
199+
197200
if (height > width) {
198201
return 'portrait'
199202
}
200-
203+
201204
return 'landscape'
202205
}
203206

‎src/index.tsx

+95-29
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import Animated, {
4444
startClock,
4545
stopClock,
4646
sub,
47+
spring,
4748
timing,
4849
Value,
4950
} from 'react-native-reanimated';
@@ -70,9 +71,19 @@ const Easing: typeof EasingDeprecated = EasingNode ?? EasingDeprecated;
7071
const FlatListComponentType = 'FlatList' as const;
7172
const ScrollViewComponentType = 'ScrollView' as const;
7273
const SectionListComponentType = 'SectionList' as const;
74+
const TimingAnimationType = 'timing' as const;
75+
const SpringAnimationType = 'spring' as const;
76+
77+
const DEFAULT_SPRING_PARAMS = {
78+
damping: 50,
79+
mass: 0.3,
80+
stiffness: 121.6,
81+
overshootClamping: true,
82+
restSpeedThreshold: 0.3,
83+
restDisplacementThreshold: 0.3,
84+
};
7385

7486
const { height: windowHeight } = Dimensions.get('window');
75-
const DRAG_TOSS = 0.05;
7687
const IOS_NORMAL_DECELERATION_RATE = 0.998;
7788
const ANDROID_NORMAL_DECELERATION_RATE = 0.985;
7889
const DEFAULT_ANIMATION_DURATION = 250;
@@ -129,6 +140,7 @@ interface TimingParams {
129140
position: Animated.Value<number>;
130141
finished: Animated.Value<number>;
131142
frameTime: Animated.Value<number>;
143+
velocity: Animated.Node<number>;
132144
}
133145

134146
type CommonProps = {
@@ -159,13 +171,6 @@ type CommonProps = {
159171
* 1 => fully opened
160172
*/
161173
animatedPosition?: Animated.Value<number>;
162-
/**
163-
* Configuration for the timing reanimated function
164-
*/
165-
animationConfig?: {
166-
duration?: number;
167-
easing?: Animated.EasingFunction;
168-
};
169174
/**
170175
* This value is useful if you want to take into consideration safe area insets
171176
* when applying percentages for snapping points. We recommend using react-native-safe-area-context
@@ -181,16 +186,46 @@ type CommonProps = {
181186
* Style to be applied to the container.
182187
*/
183188
containerStyle?: Animated.AnimateStyle<ViewStyle>;
189+
/*
190+
* Factor of resistance when the gesture is released. A value of 0 offers maximum
191+
* acceleration, whereas 1 acts as the opposite. Defaults to 0.95
192+
*/
193+
friction: number;
194+
/*
195+
* Allow drawer to be dragged beyond lowest snap point
196+
*/
197+
enableOverScroll: boolean;
198+
};
199+
200+
type TimingAnimationProps = {
201+
animationType: typeof TimingAnimationType;
202+
/**
203+
* Configuration for the timing reanimated function
204+
*/
205+
animationConfig?: Partial<Animated.TimingConfig>;
206+
};
207+
208+
type SpringAnimationProps = {
209+
animationType: typeof SpringAnimationType;
210+
/**
211+
* Configuration for the spring reanimated function
212+
*/
213+
animationConfig?: Partial<Animated.SpringConfig>;
184214
};
185215

186216
type Props<T> = CommonProps &
187-
(FlatListOption<T> | ScrollViewOption | SectionListOption<T>);
217+
(FlatListOption<T> | ScrollViewOption | SectionListOption<T>) &
218+
(TimingAnimationProps | SpringAnimationProps);
188219

189220
export class ScrollBottomSheet<T extends any> extends Component<Props<T>> {
190221
static defaultProps = {
191222
topInset: 0,
223+
friction: 0.95,
224+
animationType: 'timing',
192225
innerRef: React.createRef<AnimatedScrollableComponent>(),
226+
enableOverScroll: false,
193227
};
228+
194229
/**
195230
* Gesture Handler references
196231
*/
@@ -268,17 +303,22 @@ export class ScrollBottomSheet<T extends any> extends Component<Props<T>> {
268303

269304
constructor(props: Props<T>) {
270305
super(props);
271-
const { initialSnapIndex, animationConfig } = props;
306+
const { initialSnapIndex, animationType } = props;
307+
308+
const animationDriver = animationType === 'timing' ? 0 : 1;
272309
const animationDuration =
273-
animationConfig?.duration || DEFAULT_ANIMATION_DURATION;
310+
(props.animationType === 'timing' && props.animationConfig?.duration) ||
311+
DEFAULT_ANIMATION_DURATION;
274312

275313
const ScrollComponent = this.getScrollComponent();
276314
// @ts-ignore
277315
this.scrollComponent = Animated.createAnimatedComponent(ScrollComponent);
278316

279317
const snapPoints = this.getNormalisedSnapPoints();
280318
const openPosition = snapPoints[0];
281-
const closedPosition = snapPoints[snapPoints.length - 1];
319+
const closedPosition = this.props.enableOverScroll
320+
? windowHeight
321+
: snapPoints[snapPoints.length - 1];
282322
const initialSnap = snapPoints[initialSnapIndex];
283323
this.nextSnapIndex = new Value(initialSnapIndex);
284324

@@ -333,7 +373,15 @@ export class ScrollBottomSheet<T extends any> extends Component<Props<T>> {
333373
const isAnimationInterrupted = and(
334374
or(
335375
eq(handleGestureState, GestureState.BEGAN),
336-
eq(drawerGestureState, GestureState.BEGAN)
376+
eq(drawerGestureState, GestureState.BEGAN),
377+
and(
378+
eq(this.isAndroid, 0),
379+
eq(animationDriver, 1),
380+
or(
381+
eq(drawerGestureState, GestureState.ACTIVE),
382+
eq(handleGestureState, GestureState.ACTIVE)
383+
)
384+
)
337385
),
338386
clockRunning(this.animationClock)
339387
);
@@ -417,7 +465,7 @@ export class ScrollBottomSheet<T extends any> extends Component<Props<T>> {
417465
const endOffsetY = add(
418466
this.lastSnap,
419467
this.translationY,
420-
multiply(DRAG_TOSS, this.velocityY)
468+
multiply(1 - props.friction, this.velocityY)
421469
);
422470

423471
this.calculateNextSnapPoint = (i = 0): Animated.Node<number> | number =>
@@ -436,43 +484,55 @@ export class ScrollBottomSheet<T extends any> extends Component<Props<T>> {
436484
this.calculateNextSnapPoint(i + 1)
437485
);
438486

439-
const runTiming = ({
487+
const runAnimation = ({
440488
clock,
441489
from,
442490
to,
443491
position,
444492
finished,
493+
velocity,
445494
frameTime,
446495
}: TimingParams) => {
447496
const state = {
448497
finished,
498+
velocity: new Value(0),
449499
position,
450500
time: new Value(0),
451501
frameTime,
452502
};
453503

454-
const animationParams = {
504+
const timingConfig = {
455505
duration: animationDuration,
456-
easing: animationConfig?.easing || DEFAULT_EASING,
506+
easing:
507+
(props.animationType === 'timing' && props.animationConfig?.easing) ||
508+
DEFAULT_EASING,
509+
toValue: new Value(0),
457510
};
458511

459-
const config = {
512+
const springConfig = {
513+
...DEFAULT_SPRING_PARAMS,
514+
...((props.animationType === 'spring' && props.animationConfig) || {}),
460515
toValue: new Value(0),
461-
...animationParams,
462516
};
463517

464518
return [
465519
cond(and(not(clockRunning(clock)), not(eq(finished, 1))), [
466520
// If the clock isn't running, we reset all the animation params and start the clock
467521
set(state.finished, 0),
522+
set(state.velocity, velocity),
468523
set(state.time, 0),
469524
set(state.position, from),
470525
set(state.frameTime, 0),
471-
set(config.toValue, to),
526+
set(timingConfig.toValue, to),
527+
set(springConfig.toValue, to),
472528
startClock(clock),
473529
]),
474530
// We run the step here that is going to update position
475-
timing(clock, state, config),
531+
cond(
532+
eq(animationDriver, 0),
533+
timing(clock, state, timingConfig),
534+
spring(clock, state, springConfig)
535+
),
476536
cond(
477537
state.finished,
478538
[
@@ -528,6 +588,7 @@ export class ScrollBottomSheet<T extends any> extends Component<Props<T>> {
528588
set(handleOldGestureState, GestureState.END),
529589
// By forcing that frameTime exceeds duration, it has the effect of stopping the animation
530590
set(this.animationFrameTime, add(animationDuration, 1000)),
591+
set(this.velocityY, 0),
531592
stopClock(this.animationClock),
532593
this.prevTranslateYOffset,
533594
],
@@ -538,7 +599,7 @@ export class ScrollBottomSheet<T extends any> extends Component<Props<T>> {
538599
clockRunning(this.animationClock)
539600
),
540601
[
541-
runTiming({
602+
runAnimation({
542603
clock: this.animationClock,
543604
from: cond(
544605
this.isManuallySetValue,
@@ -549,6 +610,7 @@ export class ScrollBottomSheet<T extends any> extends Component<Props<T>> {
549610
position: this.animationPosition,
550611
finished: this.animationFinished,
551612
frameTime: this.animationFrameTime,
613+
velocity: this.velocityY,
552614
}),
553615
],
554616
[
@@ -570,7 +632,7 @@ export class ScrollBottomSheet<T extends any> extends Component<Props<T>> {
570632
);
571633

572634
this.position = interpolate(this.translateY, {
573-
inputRange: [openPosition, closedPosition],
635+
inputRange: [openPosition, snapPoints[snapPoints.length - 1]],
574636
outputRange: [1, 0],
575637
extrapolate: Extrapolate.CLAMP,
576638
});
@@ -715,15 +777,19 @@ export class ScrollBottomSheet<T extends any> extends Component<Props<T>> {
715777
const { method, args } = imperativeScrollOptions[
716778
this.props.componentType
717779
];
780+
// @ts-ignore
781+
const node = this.props.innerRef.current?.getNode();
782+
718783
if (
719-
(this.props.componentType === 'FlatList' &&
784+
node &&
785+
node[method] &&
786+
((this.props.componentType === 'FlatList' &&
720787
(this.props?.data?.length || 0) > 0) ||
721-
(this.props.componentType === 'SectionList' &&
722-
this.props.sections.length > 0) ||
723-
this.props.componentType === 'ScrollView'
788+
(this.props.componentType === 'SectionList' &&
789+
this.props.sections.length > 0) ||
790+
this.props.componentType === 'ScrollView')
724791
) {
725-
// @ts-ignore
726-
this.props.innerRef.current?.getNode()[method](args);
792+
node[method](args);
727793
}
728794
})
729795
),

0 commit comments

Comments
 (0)
Please sign in to comment.