React-native-interactable: MapPanel.js example with ScrollView (similar to Material Bottom sheets)

Created on 18 Apr 2017  路  17Comments  路  Source: wix/react-native-interactable

First, sorry if this is very related to #35, just wanted to make sure.

Basically, I was trying to mimic Bottom sheets from Material spec using this example MapPanel.js.

But, with one addition, changing content view from the panel here to ScrollView. Then, you cannot interact with it anymore (only internal scrolling).

You can see this is an issue in the real example, on Apple Maps-Style Panel, if you change your device orientation (I try with Android) to landscape, because there is no internal scroll, you cannot see the whole view.

Desired result would be something like:

ezgif-3-fa4a3bc778

Most helpful comment

Hey any success on implementing this? I'm also trying to do it.

All 17 comments

Hey any success on implementing this? I'm also trying to do it.

I'm also trying to implement this - has anyone had any luck?

Hey @abeddow91 I did a bit of a workaround. I've put a bar that's at the top of interactable view that is not scrollable. Below it have a scrollview which normally handles it's events.

This is the code that does it:
screen shot 2017-05-26 at 03 36 14

And this is the render of ShoppingListView
screen shot 2017-05-26 at 03 37 19

This <HeaderDisplay /> is the bar at the top of interactable view and behaves like a normal interactable view.
This <List /> inside of <Interactable.InterceptionBlocker /> is a wrapped FlatList (so ScrollView).

Important part is that you add canCancelContentTouches={true} as a prop to your scrollview component. If you don't it won't receive touch events. You can find an example by @talkol in one the issues that's similar to this one.

Also important: <Interactable.InterceptionBlocker /> is needed only for android and you can find it in a separate branch in this repo. There's a really long thread in one of the issues and maybe you can find some ideas there.

Right now I can snap the interactable view only by interacting with a top bar and that's not bad.

I had an idea however that I could remove the top bar and put it inside the scrollview. Then I would listen for onScroll events and when the would be negative enough. (let's say -30-40px) I would manually call .snapTo on the interactable view and close it down. It wouldn't be as smooth as on the video in this issue but it would behave similarly to "now playing" view in Apple music.

Hope I helped.

@urbancvek that is good workaround but still it is not as good as interact only with one view as you have to release your finger to do either scroll or closing the view. But i know @talkol wanna address this on #35. I feel that is almost only missing piece of this lib :D

Yup this approach I'm using is far from perfect but it's what this lib can currently do and yeah since it can do almost everything else I had to use it 馃槃. This is the thread I mentioned where I got a lot of inspiration to work around the problem. It will be awesome once it's implemented but as far as I understand it's a hard problem to solve.

Any updates on this?

@urbancvek @abeddow91 @ferrannp any update?

I ended up with just a ScrollView above all the content, I wanted to make the top of the scrollview pass trough touch events, I could not get there with just the normal react-native features. I looked and saw a nice library which does just that. https://github.com/rome2rio/react-native-touch-through-view.

Works like a charm!

import React, { PureComponent } from 'react'
import PT from 'prop-types'

import {
  TouchThroughView,
  TouchThroughWrapper,
} from 'react-native-touch-through-view'
import {
  View,
  Dimensions,
  ScrollView,
  TouchableWithoutFeedback,
} from 'react-native'

import keyboardHOC from '../logic/hocs/keyboardHOC'
import styles from './styles/BottomSheet.style'
import binder from '../logic/shared/helpers/binder'
import c from '../logic/shared/constants'

const getSize = () => ({
  width: Dimensions.get('window').width,
  height: Dimensions.get('window').height,
})

class BottomSheet extends PureComponent {
  constructor(props) {
    super(props)
    binder(this, ['_handleScroll', '_close'])
    this.state = {
      opened: false,
    }
    this.y = 0
  }

  _close() {
    this.setState(
      {
        opened: false,
      },
      () => this._scrollView.scrollTo({ y: 0, animated: true })
    )
  }
  _handleScroll(event) {
    if (event.nativeEvent.contentOffset.y === 0 && this.y !== 0) {
      if (this.state.opened) {
        this.setState({
          opened: false,
        })
      }
      this.props.collapsed()
    }

    if (this.y > 0) {
      if (!this.state.opened) {
        this.setState({
          opened: true,
        })
      }
      this.props.opened()
    }
  }

  render() {
    const { height } = getSize()

    const offsetY =
      this.y > this.props.keyboardHeight ? this.y : this.props.keyboardHeight

    const BackDrop = this.state.opened ? View : TouchThroughView

    return (
      <TouchThroughWrapper style={styles.wrapper}>
        <ScrollView
          onScroll={this._handleScroll}
          ref={component => {
            this._scrollView = component
            this.props.scrollViewRef(component)
          }}
          scrollEventThrottle={16}
          alwaysBounceVertical={false}
          bounces={false}
          keyboardDismissMode={'on-drag'}
          contentOffset={{
            y: offsetY,
          }}
        >
          <TouchableWithoutFeedback onPress={this._close}>
            <BackDrop
              style={{
                height: height - c.panelHeight,
              }}
            />
          </TouchableWithoutFeedback>

          <View
            style={{
              minHeight: height,
              backgroundColor: '#fff',
            }}
          >
            {this.props.children}
          </View>
        </ScrollView>
      </TouchThroughWrapper>
    )
  }
}

BottomSheet.propTypes = {
  children: PT.oneOfType([PT.arrayOf(PT.node), PT.node]).isRequired,
  keyboardHeight: PT.number,
  collapsed: PT.func,
  opened: PT.func,
  scrollViewRef: PT.func,
}
BottomSheet.defaultProps = {
  keyboardHeight: 0,
  count: 0,
  collapsed: () => {},
  opened: () => {},
  scrollViewRef: () => {},
}

export default keyboardHOC(BottomSheet)

BottomSheet.style.js

import { StyleSheet } from 'react-native'

export default StyleSheet.create({
  wrapper: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
  },
})

keyboardHOC

import React, { Component } from 'react'
import { Keyboard, Platform, LayoutAnimation } from 'react-native'

import binder from '../shared/helpers/binder'

export default WrappedComponent => {
  class HOC extends Component {
    constructor(props) {
      super(props)

      this.state = {
        keyboardHeight: 0,
      }

      binder(this, [
        '_keyboardDidShow',
        '_keyboardDidHide',
        '_onKeyboardChange',
      ])
    }

    componentWillMount() {
      if (WrappedComponent.componentDidMount)
        WrappedComponent.componentDidMount()

      if (Platform.OS === 'ios') {
        this.keyboardDidChangeListener = Keyboard.addListener(
          'keyboardWillChangeFrame',
          this._onKeyboardChange
        )
      } else {
        this.keyboardDidShowListener = Keyboard.addListener(
          'keyboardDidShow',
          this._keyboardDidShow
        )
      }

      this.keyboardDidHideListener = Keyboard.addListener(
        'keyboardWillHide',
        this._keyboardDidHide
      )
    }
    componentWillUnmount() {
      if (WrappedComponent.componentWillUnmount)
        WrappedComponent.componentWillUnmount()

      if (Platform.OS === 'ios') {
        this.keyboardDidChangeListener.remove()
      } else {
        this.keyboardDidShowListener.remove()
      }
      this.keyboardDidHideListener.remove()
    }

    _onKeyboardChange(event) {
      if (!event) {
        this.setState({ keyboardHeight: 0 })
        return
      }

      const { duration, easing, endCoordinates } = event
      const keyboardHeight = endCoordinates.height

      if (duration && easing) {
        LayoutAnimation.configureNext({
          duration,
          update: {
            duration,
            type: LayoutAnimation.Types[easing] || 'keyboard',
          },
        })
      }
      this.setState({ keyboardHeight })
    }
    _keyboardDidShow(e) {
      const keyboardHeight = e.endCoordinates.height
      if (e && keyboardHeight) {
        this.setState({
          keyboardHeight,
        })
      }
    }
    _keyboardDidHide() {
      this.setState({
        keyboardHeight: 0,
      })
    }

    render() {
      return <WrappedComponent {...this.state} {...this.props} />
    }
  }

  return HOC
}

Use it like this.

<View>
// 
// Put here any content e.g. Google Maps
//
<GoogleMaps />

 <BottomSheet
          scrollViewRef={component => {
            this._bottomSheet = component
          }}
          collapsed={() => {}}
          opened={()=>{}}

        >
          //Bottom sheet children could be anything
   </BottomSheet>
</View>

@RichardLindhout Sweet implementation. I gave it a go, but I'm unable to make the Touch through view work on Android in RN 0.50.1. You able to get it running on the latest version?

@kristfal Thanks! I've only tested it on an iPhone at the moment.

I am closing the issue as a part of an effort to cleanup and revive the project. If this issue persists after v1.0.0, you are more than welcome to reintroduce it.

Thanks for your contribution.

Any updates on this?

@urbancvek Are you able to get the same behavior like Android bottom-sheet? If yes, can you please share your code?
FYI, I want to achieve UI like Uber Lite android App to select Pickup address - https://play.google.com/store/apps/details?id=com.ubercab.uberlite&hl

@neerajemorphis I did use https://github.com/cesardeazevedo/react-native-bottom-sheet-behavior for Android

@RichardLindhout Even I used the same for Android, but I am stuck for iOS. I need the same bottomsheet behavior for iOS as well.

For iOS you could go with the solution I posted above with the touch trough scrollview.

@RichardLindhout thats not working as expected, no methods to expand / collapse the bottomSheet, in touch through view, as well as _collapsed_, _opened_ props doesn't seem to be working. Apart from that, need to restrict the height, of the bottomsheet to some specific height, which should not be crossed, when expanded.
I am looking for the same behavior like Android bottom-sheet - https://github.com/cesardeazevedo/react-native-bottom-sheet-behavior.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

yoavrofe picture yoavrofe  路  4Comments

dmitryusikriseapps picture dmitryusikriseapps  路  3Comments

iremlopsum picture iremlopsum  路  7Comments

brentvatne picture brentvatne  路  6Comments

rexlow picture rexlow  路  6Comments