React-native-keyboard-aware-scroll-view: Scroll Into View

Created on 20 Jan 2018  路  15Comments  路  Source: APSL/react-native-keyboard-aware-scroll-view

Recently I needed a scroll into view functionality just like it works in the web. Sadly, I had to implement it myself as I use this library and it does not implement such method.

I now have a working prototype of the method that receives a ref into the view that you want to scroll and magically scrolls to it and I would really like if this library implemented such method but I'm not sure if such functionality is out of scope here.

So, what do you think?

Most helpful comment

This is what I've got:

import React, { Component } from 'react'
import { UIManager, findNodeHandle } from 'react-native'
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'

export default class CustomScrollView extends Component {
  _container = null
  _contentOffset = 0

  scrollIntoView = async (view) => {
    if (!this._container || !view) {
      return
    }

    const [
      parentLayout,
      childLayout
    ] = await Promise.all([
      this._measureElement(this._container),
      this._measureElement(view)
    ])

    const top = childLayout.y - parentLayout.y + this._contentOffset

    this._container.scrollToPosition(0, Math.max(0, top))
  }

  _measureElement = (element) => {
    const node = findNodeHandle(element)

    return new Promise((resolve) => {
      UIManager.measureInWindow(node, (x, y, width, height) => {
        resolve({ x, y, width, height })
      })
    })
  }

  _onRef = (view) => {
    this._container = view
  }

  _onScroll = (event) => {
    this._contentOffset = event.nativeEvent.contentOffset.y

    if (this.props.onScroll) {
      this.props.onScroll(event)
    }
  }

  render () {
    return (
      <KeyboardAwareScrollView
        {...this.props}
        ref={this._onRef}
        onScroll={this._onScroll}
        scrollEventThrottle={1}
     />
    )
  }
}

And to use this method:

import React, { Component } from 'react'
import  CustomScrollView from '...'
import { Button, StyleSheet, View } from 'react-native'

const styles = StyleSheet.create({
  block: {
    alignSelf: 'stretch',
    height: 600
  },
  red: {
    backgroundColor: 'red'
  },
  blue: {
    backgroundColor: 'blue'
  }
})

export default class TestBehaviour extends Component {
  _redView = null
  _blueView = null
  _scroll = null

  _onRef = (property, view) => {
    this[property] = view
  }

  _scrollIntoView = (ref) => {
    if (this._scroll) this._scroll.scrollIntoView(ref)
  }

  render () {
    return (
      <CustomScrollView ref={scroll => this._onRef('_scroll', scroll)}>
        <View
          ref={view => this._onRef('_redView', view)}
          style={[styles.block, styles.red]}
        />
        <View
          ref={view => this._onRef('_blueView', view)}
          style={[styles.block, styles.blue]}
        />
        <Button
          title='Scroll to Red'
          onPress={() => this._scrollIntoView(this._redView)}
        />
        <Button
          title='Scroll to Blue'
          onPress={() => this._scrollIntoView(this._blueView)}
        />
      </CustomScrollView>
    )
  }
}

I haven't tested all of this but I believe it is correct and the CustomScrollView is copy-pasted from what we have in production. We are using React v16.2.0 and the React Native that comes with Expo v24.

Inspired by this method from the DOM: https://developer.mozilla.org/es/docs/Web/API/Element/scrollIntoView

All 15 comments

Hello @sebasgarcep!

So if I understand correctly, do you want to being able to scroll directly into a passed view? Can you post a snippet of how your solution looks like in the actual source code?

This is what I've got:

import React, { Component } from 'react'
import { UIManager, findNodeHandle } from 'react-native'
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'

export default class CustomScrollView extends Component {
  _container = null
  _contentOffset = 0

  scrollIntoView = async (view) => {
    if (!this._container || !view) {
      return
    }

    const [
      parentLayout,
      childLayout
    ] = await Promise.all([
      this._measureElement(this._container),
      this._measureElement(view)
    ])

    const top = childLayout.y - parentLayout.y + this._contentOffset

    this._container.scrollToPosition(0, Math.max(0, top))
  }

  _measureElement = (element) => {
    const node = findNodeHandle(element)

    return new Promise((resolve) => {
      UIManager.measureInWindow(node, (x, y, width, height) => {
        resolve({ x, y, width, height })
      })
    })
  }

  _onRef = (view) => {
    this._container = view
  }

  _onScroll = (event) => {
    this._contentOffset = event.nativeEvent.contentOffset.y

    if (this.props.onScroll) {
      this.props.onScroll(event)
    }
  }

  render () {
    return (
      <KeyboardAwareScrollView
        {...this.props}
        ref={this._onRef}
        onScroll={this._onScroll}
        scrollEventThrottle={1}
     />
    )
  }
}

And to use this method:

import React, { Component } from 'react'
import  CustomScrollView from '...'
import { Button, StyleSheet, View } from 'react-native'

const styles = StyleSheet.create({
  block: {
    alignSelf: 'stretch',
    height: 600
  },
  red: {
    backgroundColor: 'red'
  },
  blue: {
    backgroundColor: 'blue'
  }
})

export default class TestBehaviour extends Component {
  _redView = null
  _blueView = null
  _scroll = null

  _onRef = (property, view) => {
    this[property] = view
  }

  _scrollIntoView = (ref) => {
    if (this._scroll) this._scroll.scrollIntoView(ref)
  }

  render () {
    return (
      <CustomScrollView ref={scroll => this._onRef('_scroll', scroll)}>
        <View
          ref={view => this._onRef('_redView', view)}
          style={[styles.block, styles.red]}
        />
        <View
          ref={view => this._onRef('_blueView', view)}
          style={[styles.block, styles.blue]}
        />
        <Button
          title='Scroll to Red'
          onPress={() => this._scrollIntoView(this._redView)}
        />
        <Button
          title='Scroll to Blue'
          onPress={() => this._scrollIntoView(this._blueView)}
        />
      </CustomScrollView>
    )
  }
}

I haven't tested all of this but I believe it is correct and the CustomScrollView is copy-pasted from what we have in production. We are using React v16.2.0 and the React Native that comes with Expo v24.

Inspired by this method from the DOM: https://developer.mozilla.org/es/docs/Web/API/Element/scrollIntoView

Hi @sebasgarcep!

Can you open us a PR?

Hi, did a PR ever come out of this?

@TSMMark Not really, because this only works for a ScrollView and I don't know if that's okay by @alvaromb. If that's okay I can proceed. It's not much work really.

Don't mind to add this feature, as long as it is properly documented and exposed into the component API.

That's great, I'd love to see this feature. I'm about to implement it manually right now but will be more than happy to refactor to use this in the future. Thanks for the snippets @sebasgarcep

Working on the PR now!

I tried using the sample but I am getting an error while accessing the function
import { KeyboardAwareScrollView, KeyboardAwareHOC, KeyboardAwareListView } from 'react-native-keyboard-aware-scroll-view'

    render() {
  <KeyboardAwareScrollView innerRef={ref => { this.scroll = ref }} keyboardShouldPersistTaps='handled'>
                                    <SelectionView
                                lang="en-US"
                                placeholder={label}
                                minuteInterval={10}
                                ref={ref => this.inspectionPicker = ref}
                                scrollToPicker={(evt) => {
                                    //TODO : Check this
    /* this._scrollToInput(evt.target)
                                    this.scroll.props.scrollIntoView(evt.target)*/
                                }}
                                defaultSelectedValue={this.state.value[this.state.listPosition][name] == '' ? '' : this.state.value[this.state.listPosition][name]}
                                onConfirm={(option) => {

                                }}
                                onSelect={(option) => {
                                    console.log(option.value)
                                    let value = _.cloneDeep(this.state.value)
                                    value[this.state.listPosition][name] = option.value
                                    this.setState({
                                        value
                                    })
                                }}
                                onClear={() => {
                                    let value = _.cloneDeep(this.state.value)
                                    value[this.state.listPosition][name] = ''
                                    this.setState({
                                        value
                                    })
                                }}
                                options={optionTypes}
                            >
                            </SelectionView>

                                </KeyboardAwareScrollView>
}
}

 _scrollToInput(reactNode) {
        // Add a 'scroll' ref to your ScrollView
        this.scroll.scrollToFocusedInput(reactNode)
    }

screen shot 2018-05-22 at 7 32 54 pm
screen shot 2018-05-22 at 7 31 50 pm

@TSMMark @sebasgarcep Could you please guide me which approach should I follow.
I also tried using KeyboardAwareHOC scrollIntoView function but getting an error

@iosfitness I think you need to call this.scroll.scrollIntoView instead of this.scroll.props.scrollIntoView.

Also, if I recall correctly, this method only works on KeyboardAwareScrollView.

Hey, I tried it but still I'm getting the same error. Function not found
```
lang="en-US"
placeholder={label}
minuteInterval={10}
ref={ref => this.inspectionPicker = ref}
scrollToPicker={(evt) => {
const ReactNativeComponentTree = require('ReactNativeComponentTree');
const elem = ReactNativeComponentTree.getInstanceFromNode(evt.target);
this._scrollIntoView(elem)
}}

                            defaultSelectedValue={this.state.value[this.state.listPosition][name] == '' ? '' : this.state.value[this.state.listPosition][name]}
                            onConfirm={(option) => {
                            }}
                            onSelect={(option) => {
                                console.log(option.value)
                                let value = _.cloneDeep(this.state.value)
                                value[this.state.listPosition][name] = option.value
                                this.setState({
                                    value
                                })
                            }}
                            onClear={() => {
                                let value = _.cloneDeep(this.state.value)
                                value[this.state.listPosition][name] = ''
                                this.setState({
                                    value
                                })
                            }}
                            options={optionTypes}
                        >
                        </SelectionView>

_scrollIntoView = (ref) => {
    if (this.scroll) this.scroll.scrollIntoView(ref)
}

```
One observation, when I searched for scrollIntoView in lib folder, I could see this function declared in KeyboardAwareHOC

Hi,

The scrollIntoView function is not passed down to the scroll container, so it is not possible to call it (at least according to the documentation)

This PR fixes the issue: https://github.com/APSL/react-native-keyboard-aware-scroll-view/pull/258

hi @sebasgarcep

I've seen your math implementation and it almost work fine for my usecase:

    const top = childLayout.y - parentLayout.y + this._contentOffset

The problem is that the view you call scrollIntoView with, will be put at the top of the scrollview. IMHO this is not the behavior of web:
1) If view is above the scroll area, then make the view appear at the top of the scrollview (align view top border with scrollview top border)
2) If view is under the scroll area, then make the view appear at the bottom of the scrollview (align view bottom border with scrollview bottom border)

Currently, it seems in all cases I have this behavior: align view top border with scrollview top border.

Do you know if it's possible to add support for the 2nd rule, so that behavior is closer to web behavior?

So I've been able to provide a better default implementation, maybe you can review it @sebasgarcep ?

https://github.com/APSL/react-native-keyboard-aware-scroll-view/pull/260

scrollintoview auto scroll to top in android!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

clfristoe picture clfristoe  路  4Comments

ltankey picture ltankey  路  3Comments

diegorodriguesvieira picture diegorodriguesvieira  路  4Comments

MyGuySi picture MyGuySi  路  3Comments

snksergio picture snksergio  路  3Comments