109

I am styling an Image component with flexbox to be in the center of the screen which works pretty well. Now I want a second Image component to be displayed directly on the top of the first one. The second image is using absolute positioning. Currently I'm just guessing pixels so that it fits, but of course this is not accurate and way too much maintainability effort.

I am pretty much looking for the React Native equivalent of jQuery's .offset(). Is there such a thing and if there isn't what's the best way to achieve this?

jolyonruss
  • 1,770
  • 2
  • 19
  • 34
Johannes Stein
  • 1,951
  • 4
  • 18
  • 13

8 Answers8

132

React Native provides a .measure(...) method which takes a callback and calls it with the offsets and width/height of a component:

myComponent.measure( (fx, fy, width, height, px, py) => {

    console.log('Component width is: ' + width)
    console.log('Component height is: ' + height)
    console.log('X offset to frame: ' + fx)
    console.log('Y offset to frame: ' + fy)
    console.log('X offset to page: ' + px)
    console.log('Y offset to page: ' + py)
})

Example...

The following calculates the layout of a custom component after it is rendered:

class MyComponent extends React.Component {
    render() {
        return <View ref={view => { this.myComponent = view; }} />
    }
    componentDidMount() {
        // Print component dimensions to console
        this.myComponent.measure( (fx, fy, width, height, px, py) => {
            console.log('Component width is: ' + width)
            console.log('Component height is: ' + height)
            console.log('X offset to frame: ' + fx)
            console.log('Y offset to frame: ' + fy)
            console.log('X offset to page: ' + px)
            console.log('Y offset to page: ' + py)
        })        
    }
}

Bug notes

  • Note that sometimes the component does not finish rendering before componentDidMount() is called. If you are getting zeros as a result from measure(...), then wrapping it in a setTimeout should solve the problem, i.e.:

    setTimeout( myComponent.measure(...), 0 )
    
Mark Amery
  • 143,130
  • 81
  • 406
  • 459
tohster
  • 6,973
  • 5
  • 38
  • 55
  • 5
    This way of invocation of `measure` didn't work for me. Had to pass element handle: `const handle = findNodeHandle(this.refs.dropdown); UIManager.measure(handle, (x, y, ...) ...)` – Brock Nov 03 '16 at 14:17
  • setImmediate(...) now exists to replace setTimeout(..., 0) . It has the same behavior but provides some semantics around your call timing. – JesusTheHun May 29 '18 at 15:44
  • @tohster First time scroll moving into correct position. But after that, Y offset to page value is not correct. Do you know why its happening? – Balasubramanian Jun 01 '18 at 08:17
  • onLayout is preferred way to do this – hakazvaka Nov 19 '18 at 20:21
  • Does this work on android? I have tried to get the relative-to-parent position (fy) and getting 0. On ios working good. – Victor Molina Jul 29 '20 at 12:23
  • and using hooks? – Uéslei Suptitz Jun 24 '22 at 18:45
  • you can also just measure in the `onLayout` callback of an element. The benefit of it is that there is no need to fix faulty behavior with a `setTimeout` + no need for a `useEffect`, which I also kinda like. – Maxim Zubarev Aug 21 '22 at 13:32
102

You can use onLayout to get the width, height, and relative-to-parent position of a component at the earliest moment that they're available:

<View
  onLayout={event => {
    const layout = event.nativeEvent.layout;
    console.log('height:', layout.height);
    console.log('width:', layout.width);
    console.log('x:', layout.x);
    console.log('y:', layout.y);
  }}
>

Compared to using .measure() as shown in the accepted answer, this has the advantage that you'll never have to fiddle around deferring your .measure() calls with setTimeout to make sure that the measurements are available, but the disadvantage that it doesn't give you offsets relative to the entire page, only ones relative to the element's parent.

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
37

I had a similar problem and solved it by combining the answers above

class FeedPost extends React.Component {
  constructor(props) {
    ...
    this.handleLayoutChange = this.handleLayoutChange.bind(this);
  }


handleLayoutChange() {
    this.feedPost.measure( (fx, fy, width, height, px, py) => {
      console.log('Component width is: ' + width)
      console.log('Component height is: ' + height)
      console.log('X offset to page: ' + px)
      console.log('Y offset to page: ' + py)
    })
  }

  render {
    return(
      <View onLayout={(event) => {this.handleLayoutChange(event) }} 
      ref={view => { this.feedPost = view; }} >
...

Now I can see the position of my feedPost element in the logs:

08-24 11:15:36.838  3727 27838 I ReactNativeJS: Component width is: 156
08-24 11:15:36.838  3727 27838 I ReactNativeJS: Component height is: 206
08-24 11:15:36.838  3727 27838 I ReactNativeJS: X offset to page: 188
08-24 11:15:36.838  3727 27838 I ReactNativeJS: Y offset to page: 870
Mislavoo7
  • 567
  • 1
  • 6
  • 15
  • 1
    Where does the this.feedPost variable come from? Do you initialize it somewhere? I'm getting undefined is not a function this.feedPost.measure immediately when I run my app – jsparks Jun 29 '19 at 00:47
  • in the render method in the return part I have the root View: ``` {this.handleLayoutChange(event) }} ref={view => { this.feedPost = view; }} > ....``` This main View is this.feedPost. – Mislavoo7 Jun 30 '19 at 14:23
  • @jsparks feedPost is the class name : class FeedPost – Rickard Jul 16 '19 at 03:52
  • I think we don't need to pass event to handleLayoutChange since it does not use it right? – Alejandro Corredor Apr 27 '21 at 15:29
  • Thanks! For functional components you need to use `const viewRef = useRef(null);` at the top and then `ref={viewRef}` on the component – Daniel Centore Oct 17 '21 at 04:55
22

I needed to find the position of an element inside a ListView and used this snippet that works kind of like .offset:

const UIManager = require('NativeModules').UIManager;
const handle = React.findNodeHandle(this.refs.myElement);
UIManager.measureLayoutRelativeToParent(
  handle, 
  (e) => {console.error(e)}, 
  (x, y, w, h) => {
    console.log('offset', x, y, w, h);
  });

This assumes I had a ref='myElement' on my component.

Mr Speaker
  • 3,047
  • 24
  • 17
  • 4
    Note that - like the accepted answer - you may need to wrap the call to `UIManager.measureLayoutRelativeToParent` in a `setTimeout` call to prevent it from erroring out if you're executing this code from a point in the component's lifecycle prior to it having been rendered. Note also that for modern React Native you'll want to do `import { UIManager, findNodeHandler } from 'react-native'` rather than the requires shown here. – Mark Amery Dec 28 '17 at 16:46
  • 1
    `UIManager` and `findNodeHandle` can now be imported directly from "react-native". i.e., `import { UIManager, findNodeHandle} from "react-native"` – HBSKan Oct 26 '19 at 21:12
  • 2
    This method uses APIs which are now deprecated. Please use `event.target.measure`, and for TypeScript users make your ref a `View` type to access the `measure` methods. – Gio Nov 24 '21 at 20:47
17

If you use function components and don't want to use a forwardRef to measure your component's absolute layout, you can get a reference to it from the LayoutChangeEvent in the onLayout callback.

This way, you can get the absolute position of the element:

<MyFunctionComp
  onLayout={(event) => {
    event.target.measure(
      (x, y, width, height, pageX, pageY) => {
        doSomethingWithAbsolutePosition({
          x: x + pageX, 
          y: y + pageY,
        });
      },
    );
  }}
/>

Tested with React Native 0.63.3.

Flo
  • 2,309
  • 20
  • 27
  • 2
    Just tried it on Android and RN 0.63.3 and it gives wrong values (x and y with value 0 for example). – Żabojad Jan 19 '21 at 20:36
  • 1
    @Żabojad try this ```ref.current.measure((width, height, px, py, fx, fy) => {console.log(fx, fy, width, height, px, py)}``` – enestatli Jan 22 '21 at 14:15
6

There is a measureInWindow property on the ref argument object that can be used like so:

const [offset, setOffset] = React.useState();

<View ref={(view) =>
    if(!view) return;
    view.measureInWindow((x, y) => {
        setOffset({ x, y });
    })
}>
</View>
Skyler Knight
  • 131
  • 1
  • 6
5

This seems to have changed in the latest version of React Native when using refs to calculate.

Declare refs this way.

  <View
    ref={(image) => {
    this._image = image
  }}>

And find the value this way.

  _measure = () => {
    this._image._component.measure((width, height, px, py, fx, fy) => {
      const location = {
        fx: fx,
        fy: fy,
        px: px,
        py: py,
        width: width,
        height: height
      }
      console.log(location)
    })
  }
Brian F
  • 1,620
  • 1
  • 19
  • 23
  • 1
    -1; I can't repro the need to use `._component` in React Native 0.51 (current latest release, released 3 days before you posted this answer). Not sure where you're getting that from; for me, the first argument to the `ref` callback *is* a component with a `.measure` method. – Mark Amery Dec 28 '17 at 16:19
  • 2
    @MarkAmery you need it if you're using `Animated.View` instead of `View`, for example. – elzi Jun 22 '18 at 21:55
1

The following worked for me.

  1. Get position on layout.
// ...
const [position, setPosition] = useState({x: 0, y: 0});
// ...
return (
  // ...
  <View
    onLayout={event => {
      event.target.measure((x, y, width, height, pageX, pageY) => {
        setPosition({x: x + pageX, y: y + pageY});
      });
    }}
  />
  // ...
);
  1. Get Position on call.
const ref = useRef(null);
// ...
const [position, setPosition] = useState({x: 0, y: 0});
// ...
return (
  // ...
  <TouchableOpacity
    ref={ref}
    activeOpacity={0.75}
    onPress={() => {
      ref.current.measure((x, y, width, height, pageX, pageY) => {
        setPosition({x: x + pageX, y: y + pageY});
      });
    }}
  />
  // ...
);
Manil Malla
  • 133
  • 8