Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[View] Keep view's aspect ratio based on new prop #1538

Closed
wants to merge 2 commits into from

Conversation

paramaggarwal
Copy link
Contributor

Continues from #858.

Current behaviour:

  1. When a static image is configured with <Image source=require("image_name.jpg") />, the packager adds the width and height into the source prop. Hence there is no need of setting dimensions in style.
  2. But for remote images, this width and height is forcefully set on the style prop, which means if one uses only alignSelf: 'stretch' to layout the image, the height would not change in the same ratio as the width inherited from the parent.

This pull request fixes this. I believe the basic problem that this attempts to solve is very genuine. It includes a measure function on NetworkImageView to maintain aspect ratio of image when dimensions are known beforehand.

Now just like automatic sizing and layout of text, the same would work for images that actually take the entire width of the device, and the height would be configured automatically based on the calculated aspect ratio of the image:

<View style={{flex: 1, paddingTop: 20}}>
  <Image
    style={{margin: 5}}
    source={{
      uri: "http://placehold.it/800x100",
      height: 100,
      width: 800
    }}
  />
  <Image
    style={{margin: 5}}
    source={{
      uri: "http://placehold.it/600x300",
      height: 300,
      width: 600
    }}
  />
  <Image
    style={{margin: 5}}
    source={{
      uri: "http://placehold.it/400x400",
      height: 400,
      width: 400
    }}
  />
</View>

Screenshots comparing the output of above code on devices with different widths.

Open to suggestions on how to improve this pull request.

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 6, 2015
@brentvatne
Copy link
Collaborator

@paramaggarwal - this is a really interesting idea! cc @vjeux

An initial bit of feedback:

  • I'm not sure that this is best suited as an addition to Image, but rather as a separate component.
  • It feels awkward to specify the width and height in the style when that will not be respected - it would make more sense to include this in the source prop sorry I read that wrong 😄

@ide
Copy link
Contributor

ide commented Jun 7, 2015

I think we want this in core since it achieves parity between static and network images and it's a fundamental that applies to all images. To be clear, @paramaggarwal's proposal maintains the current invariant of needing to specify the image size up front (before we've downloaded it from the network). The sole difference is that it's the intrinsic image size, not the displayed size (in the style prop) that needs to be specified. This provides a very consistent API for users that I could explain to people:

  • You should specify an image's intrinsic dimensions in the <Image source> prop
  • The intrinsic dimensions, which tell us the intrinsic aspect ratio, will be used to help the layout engine calculate the image's displayed dimensions (e.g. the flex example in this discussion)
  • If you don't specify intrinsic dimensions then you must specify the image's displayed dimensions via width and height in the style prop.
  • The packager automatically specifies the intrinsic dimensions when it replaces require('image!icon.png')
  • Missing intrinsic dimensions default to zero

@paramaggarwal: might want to rename original width/height to intrinsic width/height to match w3c docs.

@brentvatne
Copy link
Collaborator

Great points @ide - what are your thoughts on this @vjeux? I know you've looked into this kind of thing previously.

@paramaggarwal
Copy link
Contributor Author

Thanks @brentvatne and @ide. I have renamed originalHeight/originalWidth to intrinsicHeight/intrinsicWidth. I have also updated the calculated dimensions to be 0x0 when intrinsic dimensions are not provided instead of the previous assumption of a default aspectRatio of 4:3.

For the benefit of others, here is the flow of how the final dimensions of a network image are calculated in order of priority:

  1. Absolute height and width specified in style.
  2. Flex calculated height and width coming from style.
  3. Intrinsic height and width used from the dimensions passed to source.

In all of these scenarios, if it is not able to calculate the height from the given info, it will attempt to use the aspect ratio computed from the dimensions passed to source.

Travis test is failing because of this failure on TEST_TYPE=build_website over here, and not in the obj-c, js tests.

{ [Error: socket hang up] code: 'ECONNRESET' }

Also, should I extend this change to static images also? So that even static images would have the same behaviour in terms of calculating their height themselves given the intrinsic dimentions in source.

var style = flattenStyle([{width, height}, styles.base, this.props.style]);
if (isNetwork) {
// NetworkImageView uses a measure method instead to set original width and height
style = flattenStyle([styles.base, this.props.style]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're throwing away the work done in flattenStyle above - it can be quite expensive, so only do one or the other.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one, I fixed this.

@sahrens
Copy link
Contributor

sahrens commented Jun 13, 2015

This is cool, but we have some changes planned with the source width/height that may need to coordinate here (although I think this is a better solution to the problems we've been seeing than we were planning). Assigning to @frantic who has been working on that image stuff.

@sahrens
Copy link
Contributor

sahrens commented Jun 13, 2015

Also, can you rebase? It might also give a passing test run.

@paramaggarwal
Copy link
Contributor Author

@sahrens Squashed commits into one, rebased to fix merge conflicts, removed redundant flattenStyle and tests now passing!

@paramaggarwal
Copy link
Contributor Author

@frantic please share your thoughts.

Also, I noticed while using this, that setting a measure function on css_layout will not cause it to look for measurements based on child elements. So if you have an image with children, and you would like the image to get its dimension based on children, then this is a no-go.

Currently I do this:

- (void)fillCSSNode:(css_node_t *)node
{
  [super fillCSSNode:node];
  if (_intrinsicHeight && _intrinsicWidth) {
    node->measure = RCTMeasure;
  }
}

So this works well. It works like an opt-in switch. Passing in source.width and source.height will mean that you want the image to size itself based on its aspect ratio than its children. The problem is that if an image element was first configured without this, and then later on gets the source.width and source.height parameters, this aspect ratio behaviour would not get turned on. Thoughts?

Pinging @vjeux for thoughts regarding the use of css-layout measure here.

@sahrens
Copy link
Contributor

sahrens commented Jun 16, 2015

The problem is that if an image element was first configured without this, and then later on gets the source.width and source.height parameters, this aspect ratio behaviour would not get turned on.

In general, it's really important that the order of property changes not matter, just the final state - can you fix that?

@paramaggarwal
Copy link
Contributor Author

@sahrens, I thought of a way to do it conditionally - and updated the PR. Now only if an intrinsicHeight or intrinsicWidth are passed in, it will attempt to measure layout dynamically. So this is taken care of.

@frantic @vjeux please do share your thoughts.

@vjeux
Copy link
Contributor

vjeux commented Jun 17, 2015

Sorry for the super long delay in my response.

Can we polyfill this on web somehow? Two ways would be padding-bottom: (aspectRatio * 100)% or using with a data uri that's a transparent image of the intrisic size.

If we can't then I'm going to push back on this. Otherwise this seems like a fine change. Very much opt-in.

For your measure question, this is indeed an issue, we probably need to change the caching logic to let a component defined function also tell if it should invalidate caching or not (based on intrisic width)

@vjeux
Copy link
Contributor

vjeux commented Jun 17, 2015

Btw image themselves may not be the only one that require an aspect ratio. If you want to do a responsive square grid, you need to maintain 1:1 aspect ratio on a View. Out of the scope of this pull request but would be nice to unify the concept of aspect ratio constraint

@paramaggarwal
Copy link
Contributor Author

Isn't this the default behaviour on the web already? When an <img> is used on the web, the browser fetches the image, calculates the width and height and maintains the aspect ratio of the image automatically. See this JSBin, try resizing the output panel and note that when the width of the image is changing, the height is also changing. Hence the aspect ratio of the image is automatically maintained by the browser.

Now in React Native, we do not load images and calculate dimensions, we expect the dimensions to be passed before-hand as part of style. But then we lose the capability of changing height of image in accordance with the width of the image.

This PR, helps us give the original intrinsicHeight and intrinsicWidth of the image (which a browser finds out after actually loading the image, while we provide beforehand) - and based on this, using the measure method of css-layout, we are able to maintain the aspect ratio of the image when it's width changes.

In much this way, the src attribute on the web, maps to the source attribute in React Native. Only that, in addition to the uri, we also need to give the width and height of the image - completely describing the remote resource.

<Image source={{
  uri: 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/57/React.js_logo.svg/600px-React.js_logo.svg.png',
  width: 600,
  height: 600
}} />

@frantic
Copy link
Contributor

frantic commented Jun 18, 2015

Hey @paramaggarwal, great work!

I'm wondering if we could use the same logic for both static and network images? Or am I missing something?

For more context about the image work we are doing internally. The idea is to deprecate require('image!...') usage in favor of relative require styles (require('./logo.png')). There are lots of benefits to this system: no need to manually add images in Xcode, no need to recompile, images live alongside the code, etc. You can follow it here #1043

Implementation wise we will be serving the images over network in development mode and will bundle them with the app when submitting to AppStore, so the same image will be rendered using NetworkImage and StaticImage depending on environment. That means they have to behave the same way to avoid surprises.

As part of that, we made packager add width and height to image metadata, so the developer won't need to duplicate information that the system can figure out. The only problem with this approach is when the developer wants to use something like flex to size the image - width and height attributes we set get priority. One solution could be flattening style and checking if flex or alignSelf are set, and only if not - applying known width and height.

@ide
Copy link
Contributor

ide commented Jun 19, 2015

I'm wondering if we could use the same logic for both static and network images?

Yes, this should absolutely happen.

  • For truly remote images (http://example.com/logo.png) the programmer has to specify {width, height, uri} in the image source
  • For images that are locally available but are served remotely by the packager (./logo.png), the packager should read the intrinsic size of the image and replace require('./logo.png') with {width, height, uri}
  • For images that are locally available and are statically bundled, the packager should read the intrinsic size of the image and replace require('./logo.png') with {width, height, uri, isStatic}

Then, both the native implementations for static and remote images should take the provided intrinsic dimensions into account when computing layout.

The only problem with this approach is when the developer wants to use something like flex to size the image - width and height attributes we set get priority.

The width and height properties of the image source should not get priority. They are intrinsic dimensions, not the rendered dimensions.

Just like the web, they should be the defaults that the layout engine uses as a last resort. For example, if the programmer uses flex and we know the rendered width of the image should be 375px wide but don't know its rendered height, then the layout engine should calculate rendered_height = rendered_width * intrinsic_height / intrinsic_width. If the programmer specifies style={{width: 200}} but leaves off the height, then the layout engine should perform a similar calculation using the intrinsic aspect ratio to compute the rendered height. If the programmers doesn't specify any rendered dimensions (no flex, no width nor height in the style), then the image falls back to its intrinsic dimensions.

@paramaggarwal
Copy link
Contributor Author

Thanks @frantic, the behaviour of static and remote images is now exactly the same after my latest changes (which I have squashed into a single commit). I've published an example to https://github.com/paramaggarwal/flex-image-demo to demonstrate this.

Thanks @ide, the behaviour you describe makes perfect sense, please try out the example I have published. Here is the index.ios.js file of the demo.

@paramaggarwal paramaggarwal changed the title [Image] Maintaining aspect ratio on network images. [Image] Maintaining aspect ratio on images with flex layout. Jun 19, 2015
@paramaggarwal
Copy link
Contributor Author

I am trying the test cases described by @ide above.

If the programmer uses flex and we know the rendered width of the image should be 375px wide but don't know its rendered height, then the layout engine should calculate rendered_height = rendered_width * intrinsic_height / intrinsic_width.

This works, but without having to explicitly use flex. More details below.

If the programmer specifies style={{width: 200}} but leaves off the height, then the layout engine should perform a similar calculation using the intrinsic aspect ratio to compute the rendered height.

This works as expected.

If the programmers doesn't specify any rendered dimensions (no flex, no width nor height in the style), then the image falls back to its intrinsic dimensions.

This does not work as expected. When no style dimensions are specified, it is picking width as the width of the parent.

I attempted to fix this by trying these modifications in my code:

  • If the width of the image is smaller than the width of parent, then return the width of the image as is, with the height calculated in proportion.

But this still didn't work because css-layout uses only the height from the returned dimensions and not the width. It continues to use the width of the parent itself. The relevant snippet of code is here.

  • If there was a way to check if flex is being used on the node, then return intrinsic dimensions as is.
    Doing this is hard from outside of css-layout, as detecting whether flex is being used can only be determined from inside the css-layout code. This is because flex could be cascading down the children.

@vjeux please help with your thoughts here. Should this kind of layout be a method on css-layout instead? For example right now, we specify a measure function for css-layout, but instead if we could pass the intrinsic dimensions of the node to css-layout itself? That way this could be applied to any node, not just images. Please do share thoughts.

@paramaggarwal
Copy link
Contributor Author

Folks, please review! Also, if there is anything that should be done differently here, please share!

@paramaggarwal
Copy link
Contributor Author

Please review/share feedback!

@hayeah
Copy link
Contributor

hayeah commented Aug 18, 2015

I've been using aspectRatio from @paramaggarwal's branch, and have not run into any problem. Absolutely essential! 👍

@paramaggarwal
Copy link
Contributor Author

Folks, please review!

@hayeah
Copy link
Contributor

hayeah commented Aug 22, 2015

ping @vjeux @ide @sahrens. Could aspectRatio make it into 0.10?

@ide
Copy link
Contributor

ide commented Aug 22, 2015

It won't go into 0.10 because we've cut the RC and this isn't an urgent bugfix. I don't think it will go into 0.11 either because that release will focus on stability, but it could make it into 0.12-rc.

For me to feel good about this commit I'd want aspectRatio to work in both dimensions. E.g. if you have an image with aspect ratio 4:3 and give it height: 300, it should be rendered at 400x300. Likewise it should be rendered at 400x300 if you specify width: 400 too.

@paramaggarwal
Copy link
Contributor Author

@ide Good point! That needs a change in css-layout - will leave this open till we get around to supporting it there.

@paramaggarwal
Copy link
Contributor Author

Till then, could someone please pull #1776? It was imported but never showed up on master. With that we can use this component separately as a library. It's merged upstream also: facebook/yoga#84.

@paramaggarwal
Copy link
Contributor Author

Anybody would like to help out on facebook/yoga#118? The idea is to make measure more generic and be more like sizeThatFits from iOS. (ref: sizeThatFits)

@hayeah
Copy link
Contributor

hayeah commented Sep 11, 2015

@paramaggarwal is there another issue I should watch? Would really like to see this happen...

@paramaggarwal
Copy link
Contributor Author

@hayeah Long story short, this PR depended on measure from css-layout to override layout when needed - for example aspect ratio. But measure was originally meant for only text layout, which means it only works to calculate height, given a width. I tried my hand at adding support to measure to also work the other way - calculate the width, given a height.

Basically the goal was to make measure a more generic way to override layout whenever a component needs it. Just like sizeThatFits: from iOS UIView. But I tried hard (facebook/yoga#118) and failed to make css-layout work that way. I don't have much knowledge of how css-layout works.

Currently I am making do with having a fork of RN with #1776 merged in and then just having a small custom component called RigidBox to maintain aspect ratio of my images.

@bakso
Copy link

bakso commented Sep 16, 2015

Very well done! But where is the doc?

@wootwoot1234
Copy link

Any update on this? I could really use this in my project right now. :)

@paramaggarwal
Copy link
Contributor Author

@iostalk @wootwoot1234 The changes in this PR are able to support aspect ratio only in one dimension - for it to make it into core, it needs to support both vertical and horizontal dimensions. Unfortunately that needs serious rework on the css-layout repo.

@alfrednerstu
Copy link

Also needing this badly, what are the latest developments? Is there a way I can get this to work @paramaggarwal ?

@paramaggarwal
Copy link
Contributor Author

@alfrednerstu I should probably publish what I have as an OSS component. Haven't gotten around to doing it...

@paramaggarwal
Copy link
Contributor Author

Hey folks, here is the generic component I have been using to make an arbitrary container who's height is always in a particular ratio to its width. This width is equal to the width of the parent. Great for use with images where you want them to stick to a particular aspect ratio.

Here is the code with the usage as a comment: https://gist.github.com/paramaggarwal/b9baa00fd7bee9a2948f

Feel free to use the code anywhere and also make it into an OSS component.

@paramaggarwal
Copy link
Contributor Author

The changes needed in css-layout are now in: facebook/yoga#154 - but I'll need to integrate the Android version also. @obipawan has implemented it internally. Will have to also add an example in UIExplorer.

Should I make the change only for Image component, or generically available on View?

@polarathene
Copy link

@paramaggarwal It would be nice as generic. I have a use case with react-native-video component where I'm wanting to use flex:1 for the width but maintain aspect ratio without having to provide a fixed height.

I'm curious if this feature might help towards implementing flex-grow/shrink features?

@mschipperheyn
Copy link

This feels a bit like css's: background-size:cover. Ideally, you would be able to specify how to position the image in the defined window. Normally, a centered approach would be best

@facebook-github-bot
Copy link
Contributor

@paramaggarwal updated the pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.
Projects
None yet
Development

Successfully merging this pull request may close these issues.