facebook/react-native

[Image] Maintaining Aspect Ratio of <Image>

paramaggarwal opened this issue ยท 20 comments

[EDIT] Use aspectRatio style prop since RN 0.40+. Docs: aspectRatio

Starting this discussion thread around how to maintain aspect ratio of an image like it does in a browser. When we use <img src='example.com/image.jpg'> in a browser, it renders a 0x0 block, then it fetches the image, looks at its width and height, then updates the layout with this new width/height bound by width/height set on container. So if the image is 400x300. It effectively gets a style of width: 400 and height: 300. But if we configure styling to be width: 500, then it fills in the other dimension height: 300 and is able to display the image.

Now in React Native, this behaviour does not exist for the right reasons. But we also lose this ability to somehow convey the size of the image. In React Native we are limited to giving width and height as part of the style. But in scenarios where the width actually comes from the Flexbox behaviour - with something like alignSelf: stretch, the height needs to be configured absolutely. So it ends up with the width taking the entire width from parent, but height being fixed. Using cover on the image, we are able to fill this space with the image, but the image gets cropped.

@vjeux mentioned in #494 that he prototyped an implementation where you would pass in something like an aspectRatio for the image, and then React Native would maintain that within the bounds of the configured styling - but he backed off because it is not part of the CSS standard. I have a suggestion on how we can do this (we use this method in our internal screen layout system that also uses css-layout).

In React Native, an image is configured like this: <Image source={{uri: "example.com/image.jpeg"}} />, what we are missing here is information about the size of the image. What if we gave that info here in source, and not as part of CSS style info? So it would look something like this:

<Image source={{
    uri: "example.com/image.jpeg",
    height: 400,
    width: 300
  }} style={{
    alignSelf: 'stretch'
  }} />

In this example, the width of the Image would come from the parent, and it would pick the height based on the ratio inferred from the meta info in source. The logic to this would remain the same as what @vjeux prototyped, but now we have a way to configure it outside CSS as there is nothing of this sort in the CSS spec itself. The source spec is a React Native spec, and hence we can support two new properties width and height inside it to not have to fetch an image, but instead know the height and width of the image beforehand.

ide commented

There's something intuitive and familiar about grouping the image's intrinsic dimensions with its source. I believe the packager now includes the width and height of local images when you do require('image!icon.png') so a lot of existing code would be able to take advantage of this feature as-is.

@paramaggarwal - you could use one of the resizeMode options to adjust the image to maintain the aspect ratio and still fit in whatever container size you have

Hey @brentvatne, in my use-case I would like the entire image to show - hence would like that the height of the image vary so that it fit fully without any cropping nor bands. Now CSS does not support something like an aspectRatio style for something like this. In this issue I suggest an idea that does it out of the context of CSS with a very simple API by tagging the width and height in the image object itself.

@paramaggarwal - would it help if you were given a callback with the width and height of the network image when it finishes loading?

Hmm, I'm unable to explain my use case clearly, I'll give it another shot.

Imagine I wish to display a network image, whose width and height is 200x100. I already know its width and height before fetching it. Now when I do its layout I would like it to keep its aspect ratio. So if it happens to be in a parent that is 300px wide, then I would like this image view to be of 300x150 size. Hence the 150 comes automatically - I cannot say height: 150 in the CSS style because when the parent is 400px wide, I would like the image to be 400x200 - again maintain its aspect ratio. CSS does not provide a way to specify the height in this way for layout. Hence looking for a way outside CSS to be able to give such a relation between the height of the image with respect to its width.

My original post above suggests one such way to configure the Image component.

That makes sense, you could maybe make a wrapper component around it that does that for you

<ImageWithConstraints source={require('..')} 
   width={200} height={100} maintainAspectRatio={true} style={{width: 300}}/>

then you can just calculate out the height and pass that on to an Image that is rendered by ImageWithConstraints. If you wanted to use flex you could hook into onLayout and find what width it is given and then calculate the height from the aspect ratio.

Would that solve your problem?

(It would be great to see this published as community component)

Sounds perfect. One question: If onLayout I set the height of the image, would it trigger a full layout again and hence trigger onLayout again?

@paramaggarwal - yeah, depending on your use case that might not be ideal but this seems pretty easy to implement so it's worth a shot ๐Ÿ˜„

Please post code here afterwards! I'm curious to see

Sure, I'll also look at how I can specify the css_node->measure method that css-layout accepts where I can return a height based on a width parameter. Maybe I'm able to implement my original suggestion here myself. ๐Ÿ˜›

Will share code/learnings here. Thanks @brentvatne!

Alright based on onLayout this is what we have: (results in a flash of re-layout...)

var ImageWithConstraints = React.createClass({
  getInitialState: function () {
    return {
      style: {}
    };
  },

  propTypes: {
    originalWidth: React.PropTypes.number.isRequired,
    originalHeight: React.PropTypes.number.isRequired,
  },

  onImageLayout: function (e) {
    var layout = e.nativeEvent.layout;
    var aspectRatio = this.props.originalWidth / this.props.originalHeight;
    var measuredHeight = layout.width / aspectRatio;
    var currentHeight = layout.height;

    if (measuredHeight != currentHeight) {
      this.setState({
        style: {
          height: measuredHeight
        }
      });
    }
  },

  render: function () {
    return (
      <Image
        {...this.props}
        style={[this.props.style, this.state.style]}
        onLayout={this.onImageLayout}
      />
    );
  }
});

Figured out how to do this with css_node->measure from css-layout. Please refer to pull request #1538. Will need a lot of feedback to bring this upto the standards of the rest of the repo...

For what it's worth @paramaggarwal , it might be possible to do this onLayout without a flash of relayout? Mainly, if object A contains object B, but object A's onLayout sets object B's size (before B gets laid out?), then there should be no flash, right? (I'm very new to React Native, so could totally be misunderstanding the rendering flow.) In your case, you were having an object set it's own size in response to a layout, which I can understand why that'd cause a flash. Here's my slight modification to yours that demonstrates what I think(!) is working for me: https://gist.github.com/mikelambert/12e4496ae813f2b365567b460548360a

Regardless, very much appreciate you listing demo code here, as well as attempting to push a better approach back upstream in your pull request.

I've published to npm react-native-fit-image. It enables you to draw responsive image component.

@originerd as I understand, this will not automatically use the network image's native dimensions?
For example if I load an image that is smaller than the width of the screen, for example: 120x100, it will still stretch the image, I was hoping it would than just show it as 120x100, without having to provide the dimensions as another image might be 250x80 and should be rendered as 250x80.
Or am I missing something?

Kind regards,

ide commented

In master a new style property called aspectRatio was recently added. I haven't tried it yet but believe it could be super useful for images incl. this issue.

I'm currently using the static getSize function on the image to determine the width and the height of the image. In the callback I set the style in the state so it get's updated, this causes a flash of unstyled content but I will probably try to show a spinner until the callback is triggered.

shafy commented

@ide the aspectRatio style property is now available in 0.39 according to the docs, but somehow I still get that it's not a valid style property when I try to run it (on View or Image). Did you have more luck with it?

I believe aspectRatio was only added in 0.40: 5850165

(though thanks for the heads-up!)

shafy commented

Seems like it. But weird that it is mentioned here https://github.com/facebook/react-native/releases/tag/v0.39.0 and also included in the Java and Objective C code with 0.39.

I use this library to get the job done react-native-scalable-image.