React-native-reanimated-bottom-sheet: Callback on close

Created on 12 Jun 2019  Â·  12Comments  Â·  Source: osdnk/react-native-reanimated-bottom-sheet

Hey!
I want to get a callback when the user closes the bottom sheet, something like onClosed.
I don't know how to use callbackNode, could you please explain this to me?
Thank you

Most helpful comment

@songxiaoliang @jcatink It's probably even a little simpler in a class cause you don't need the ref's. I think something like this would work.

import Animated from "react-native-reanimated"
import BottomSheet from "reanimated-bottom-sheet"
import React from "react"

import Header from "somewhere"
import Content from "somewhere"

const {
  Extrapolate,
  block,
  call,
  cond,
  greaterOrEq,
  interpolate,
  lessOrEq,
  onChange,
} = Animated

class MyComponent extends React.Component {
  constructor() {
    super()
    this.drawerCallbackNode = new Animated.Value(0)
    this.clampedDrawerCallbackNode = interpolate(this.drawerCallbackNode, {
      extrapolate: Extrapolate.CLAMP,
      inputRange: [0, 1],
      outputRange: [0, 1],
    })
  }

  render() {
    return (
      <>
        <Animated.Code
          exec={onChange(
            this.clampedDrawerCallbackNode,
            block([
              cond(
                greaterOrEq(this.clampedDrawerCallbackNode, 1),
                call([], () => {
                  console.log(`[${new Date()}] Drawer closed`)
                })
              ),
              cond(
                lessOrEq(this.drawerCallbackNode, 0),
                call([], () => {
                  console.log(`[${new Date()}] Drawer opened`)
                })
              ),
            ])
          )}
        />
        <BottomSheet
          callbackNode={this.drawerCallbackNode}
          renderContent={() => <Content />}
          renderHeader={() => <Header />}
          snapPoints={[230, 0]}
        />
      </>
    )
  }
}

Let me explain this a little. At the top we're importing reanimated and destructuring the required nodes so we can call them directly later on. In the constructor we're creating an Animated Value that we can pass to the BottomSheet as a callback node. This value will always contain the current relative position of the drawer with 0 being fully open and 1 being fully closed. On the next line we create another value that references the first but we clamp the value between 0 and 1 so that the overshoot animation doesn't take the value outside of this range. With this current code it may not be necessary but I was getting some weirdness when I was first playing with this.

Finally in the render method we're creating an Animated node that will execute certain callbacks based on the outcome of the conditionals on our clamped value. Normally this node would need to be provided to an animated view component to be used within its style. The view will then(in native code) evaluate the value and update the style attribute with the nodes return value. This would in our case evaluate the conditional nodes we configure. However, as we're not passing this to a component we need to use Animated.Code to arbitrarily execute this node and get the desired callback effect.

I haven't tried this code so it may not be 100% correct but I hope that helps get you going😄

If you need to customise the callback behaviour further it may be worth looking at the reanimated docs directly.

All 12 comments

I'm still becoming familiar with reanimated and this library but I've managed to get something like this working.

const drawerCallbackNode = useRef(new Animated.Value(0)).current
const clampedDrawerCallbackNode = useRef(
  interpolate(drawerCallbackNode, {
    extrapolate: Extrapolate.CLAMP,
    inputRange: [0, 1],
    outputRange: [0, 1],
  })
).current

useCode(
  onChange(
    clampedDrawerCallbackNode,
    block([
      cond(
        greaterOrEq(clampedDrawerCallbackNode, 1),
        call([], () => {
          console.log(`[${new Date()}] Drawer closed`)
        })
      ),
      cond(
        lessOrEq(drawerCallbackNode, 0),
        call([], () => {
          console.log(`[${new Date()}] Drawer opened`)
        })
      ),
    ])
  )
)

I'm using functional components hence the use of hooks so you may need to adapt this slightly. Essentially I'm creating and animated value and later on passing that to bottom sheet. This value updates with the relative snap position on every frame including when it overshoots. By calling interpolate on the next line I'm able to clamp this value between 0 and 1. Then inside useCode I'm combining some nodes to say when that value changes check if it's either 0 or 1 and call the appropriate callback.

I only have 2 snap points so can pretty easily deal with 0 and 1. You would obviously need more conditional nodes if you wanted to detect more snap points.

@njdancer Hello, can you give it a way without a hook? Thank you

@njdancer Looking forward to your reply

Thanks @njdancer this works great with me.

@paranoia5 怎么解决的呢?

@songxiaoliang exactly like @njdancer solution

import BottomSheet from 'reanimated-bottom-sheet';
import Animated from 'react-native-reanimated';

const {useCode, onChange, block, cond, greaterOrEq, lessOrEq, call} = Animated;

const ScreenX = ():React.ReactElement<any> => {

const bottomSheetRef = React.useRef<BottomSheet>().current;
const drawerCallbackNode = React.useRef<any>(new Animated.Value(0)).current;
const clampedDrawerCallbackNode = React.useRef<Animated.Adaptable<any>>(
    Animated.interpolate(drawerCallbackNode, {
      extrapolate: Animated.Extrapolate.CLAMP,
      inputRange: [0, 1],
      outputRange: [0, 1],
    })
  ).current;

 useCode(
    onChange(
      clampedDrawerCallbackNode,
      block([
        cond(
          greaterOrEq(clampedDrawerCallbackNode, 1),
          call([], () => {
            console.log("Sheet is closed")
          })
        ),
      ])
    ), 
   null
  );

return (
    <View style={styles.container}>
      <BottomSheet
        ref={e => (bottomSheetRef = e)}
        snapPoints={[
          Screen.height - (Screen.statusBar + Screen.tabBarHeight * 2),
          PANEL_MINIMUM_HEIGHT,
        ]}
        callbackNode={drawerCallbackNode}
        renderContent={renderInner}
        initialSnap={1}
      />
   </View>
  )
}

That solution is not without Hooks @paranoia5, it fails with hooks warning inside my component. Any ideas?

@songxiaoliang @jcatink It's probably even a little simpler in a class cause you don't need the ref's. I think something like this would work.

import Animated from "react-native-reanimated"
import BottomSheet from "reanimated-bottom-sheet"
import React from "react"

import Header from "somewhere"
import Content from "somewhere"

const {
  Extrapolate,
  block,
  call,
  cond,
  greaterOrEq,
  interpolate,
  lessOrEq,
  onChange,
} = Animated

class MyComponent extends React.Component {
  constructor() {
    super()
    this.drawerCallbackNode = new Animated.Value(0)
    this.clampedDrawerCallbackNode = interpolate(this.drawerCallbackNode, {
      extrapolate: Extrapolate.CLAMP,
      inputRange: [0, 1],
      outputRange: [0, 1],
    })
  }

  render() {
    return (
      <>
        <Animated.Code
          exec={onChange(
            this.clampedDrawerCallbackNode,
            block([
              cond(
                greaterOrEq(this.clampedDrawerCallbackNode, 1),
                call([], () => {
                  console.log(`[${new Date()}] Drawer closed`)
                })
              ),
              cond(
                lessOrEq(this.drawerCallbackNode, 0),
                call([], () => {
                  console.log(`[${new Date()}] Drawer opened`)
                })
              ),
            ])
          )}
        />
        <BottomSheet
          callbackNode={this.drawerCallbackNode}
          renderContent={() => <Content />}
          renderHeader={() => <Header />}
          snapPoints={[230, 0]}
        />
      </>
    )
  }
}

Let me explain this a little. At the top we're importing reanimated and destructuring the required nodes so we can call them directly later on. In the constructor we're creating an Animated Value that we can pass to the BottomSheet as a callback node. This value will always contain the current relative position of the drawer with 0 being fully open and 1 being fully closed. On the next line we create another value that references the first but we clamp the value between 0 and 1 so that the overshoot animation doesn't take the value outside of this range. With this current code it may not be necessary but I was getting some weirdness when I was first playing with this.

Finally in the render method we're creating an Animated node that will execute certain callbacks based on the outcome of the conditionals on our clamped value. Normally this node would need to be provided to an animated view component to be used within its style. The view will then(in native code) evaluate the value and update the style attribute with the nodes return value. This would in our case evaluate the conditional nodes we configure. However, as we're not passing this to a component we need to use Animated.Code to arbitrarily execute this node and get the desired callback effect.

I haven't tried this code so it may not be 100% correct but I hope that helps get you going😄

If you need to customise the callback behaviour further it may be worth looking at the reanimated docs directly.

Hello! Great library, but I have the same requirement.

Is it possible to add onSnapTo event callback?

@njdancer When I put the bottomsheet into Modal, iOS can slide normally, Android can't slide。
<Modal
onRequestClose={() => this.close()}
supportedOrientations={['landscape', 'portrait']}
transparent
visible={visible}
>
<>
<Animated.Code
exec={onChange(
this.clampedDrawerCallbackNode,
block([
cond(
greaterOrEq(this.clampedDrawerCallbackNode, 1),
call([], () => {
console.log([${new Date()}] Drawer closed)
})
),
cond(
lessOrEq(this.drawerCallbackNode, 0),
call([], () => {
console.log([${new Date()}] Drawer opened)
})
),
])
)}
/>
<BottomSheet
callbackNode={this.drawerCallbackNode}
renderContent={() => <Content />}
renderHeader={() => <Header />}
snapPoints={[230, 0]}
/>
</>

What is the purpose of clampedDrawerCallbackNode? Seems like it works by using only drawerCallbackNode

@alamothe when the drawer animation overshoots so does the callback node. When I was initially fiddling with this I think I was having some issues because of that and clamping the value to between 0 and 1 helped. As the example sits though it probably isn’t required. The same interpolation technique could be used to convert the relative callback node value to the actual number of points of that position.

Sent with GitHawk

Was this page helpful?
0 / 5 - 0 ratings