cesardeazevedo / react-native-bottom-sheet-behavior

react-native wrapper for android BottomSheetBehavior
MIT License
1.16k stars 114 forks source link

Any Plans to Support iOS? #6

Closed joncursi closed 7 years ago

joncursi commented 8 years ago

The Google Maps iOS app, and many other material design apps, use this same bottom sheet behavior on both iOS and Android. Are there any plans to make this library cross-compatible with iOS as well?

cesardeazevedo commented 8 years ago

Thanks for the feedback, I wasn't aware of the new bottom sheet maps on iOS 10, and indeed it's very similar, i had no plans to support iOS when i was designing it, i, but now looking on the new iOS 10 maps it might make sense, but on android is just a wrapper on the android BottomSheetBehavior, and it's easier because i don't have to deal with animations at all, it's already on the component, it's reliable and well tested, i just need to expose it to react-native, but iOS maps is a closed source and there's no such thing (as far i know) on iOS standard library, so i need to reimplement and "fake" it to looks like iOS maps, which depending the implementation can disappoint some users, because the implementation will never be the same.

Searching a little while, i found this stackoverflow issue and a project answer https://github.com/AhmedElassuty/IOS-BottomSheet, and the implementation looks similar, but after playing along side with iOS maps the experience isn't the same thing.

There's also STPopup that provides a bottom sheet approach which could be more interesting.

If you know any approach to achieve that or external library that we can rely on it, would be great, but i've no plans for now.

YusufBesim commented 8 years ago

STPopup is amazing anyone there (someone pro) for implement for react native ?

cesardeazevedo commented 7 years ago

Hey guys, it's been a while, but i just want to share a very cool google maps implementation that works well on ios, it's actually physics based library by wix, check it out the react-native-interactable

Yes, i've tested myself, and it feels native and uses a declarative API relied on UIKit Dynamics, so it's very reliable and easy to implement, i will also use on my ios version, and remain this library on android.

I am going to close this, once i'm taking this library to a next step, and adding any other API will be highly incompatible and confused.

RichardLindhout commented 7 years ago

@cesardeazevedo The react-native-interactable can't handle a scrollview inside the panel. https://github.com/wix/react-native-interactable/issues/62 https://github.com/wix/react-native-interactable/issues/35

cesardeazevedo commented 7 years ago

@RichardLindhout nested scrolls is actually a very difficult problem, the way that bottom-sheet-behavior works with nested-scroll-view is because it's backed by a coordinatorlayout on the native side, so didn't had to do much thing in order to make it work, but the react-native-interactable is a completely different propose, they will have to implement a different approach, so there's not much thing that i can do about it, sorry.

RichardLindhout commented 7 years ago

I'll maybe try to port https://github.com/AhmedElassuty/BottomSheetController/tree/develop to react-native

RichardLindhout commented 7 years ago

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, it should also work on Android!

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>