react-native-extended-stylesheet icon indicating copy to clipboard operation
react-native-extended-stylesheet copied to clipboard

Ideas for orientation change support

Open vitalets opened this issue 9 years ago • 51 comments

After implementing media queries in #5 I'm thinking about supporting orientation change somehow. As it will allow to get full power of media queries.

Technically I see these steps:

  1. catch orientation change
  2. re-calculate styles
  3. re-render components

I quickly searched for that topic but did not found ready to use concept how it can be implemented in RN. If you have some ideas or experience, please feel free to share it here and we will think how to add it.

Thanks!

vitalets avatar Apr 26 '16 08:04 vitalets

Found refs: https://github.com/facebook/react-native/issues/25 https://github.com/facebook/react-native/issues/3219 https://github.com/walmartreact/react-native-orientation-listener https://github.com/yamill/react-native-orientation https://github.com/kkjdaniel/react-native-device-display http://kouky.org/blog/2015/02/16/ios-adaptive-layouts-in-code.html

vitalets avatar Apr 28 '16 15:04 vitalets

@vitalets, shall it actually be not a stylesheet business to watch for the possible orientation change and even command the components rendering?

I am playing with the extended stylesheets at the moment and while full automation of orientation handling wouldn't hurt, on demand calculation could be not that bad either.

For example, EStyleSheet.create() or a similar API point could return not a finalized sheet, but a singletone-like function that guarantees to have all the media queries caclulated when returns and behind the scenes caches in a memoize()-like way or even precaches well in-advance (as orientation change is quite an expected event). For example,

var getStyles = EStyleSheet.createRotationReadyStyle({
    debugObj: {
      backgroundColor: 'green',
    },
    '@media (orientation: landscape)': {
      debugObj: {
        backgroundColor: 'yellow',
        fontSize: 32
      }
    },
  });
  ...
  <Text style={getStyles().debugObj}>Hello</Text>
  ...
  // getStyles() is guaranteed to return a stylesheet valid for "right now"
  // caching is the implementation detail, yet in practice styles are 
  // probably calculated just once per orientation
  //
  // very efficient createRotationReadyStyle() probably even precalculated
  // portrait and landscape versions before getStyles() was ever called

amarchen avatar May 23 '16 22:05 amarchen

hi @amarchen ! thanks for your ideas! I believe that while having percentage values and media-queries supported, the library should keep this values in sync with screen size itself. So developer should only declaratively describe styles and no code for listening orientation.

Currently I've got working proto with the same approach as you described here https://github.com/vitalets/react-native-extended-stylesheet/issues/13 - using View with onLayout subscription. I like this because it does not require any native code and works with both platforms.

Of course, it should automatically memoize two calculated stylesheets (portrait and landscape), so developer should not care about it.

I'm going to make orientation support as option:

  • automatic
  • manual: you may call something like EStyleSheet.update(layoutWidth, layoutHeight)
  • no support (as current)

vitalets avatar May 31 '16 10:05 vitalets

Hi, any update on this?

sampurcell93 avatar Jul 13 '16 15:07 sampurcell93

Hi @sampurcell93

trying to find time to finalize that. But still it is not ready to be published.

vitalets avatar Jul 22 '16 13:07 vitalets

Is there any way I can help? This orientation change feature would be huge for our company.

Thanks for the hard work!

sampurcell93 avatar Jul 22 '16 17:07 sampurcell93

@abdallahm That looks great! @vitalets What do you think of abdallhm's commit?

yvbeek avatar Sep 19 '16 18:09 yvbeek

@Zyphrax

I'll repost here my answer from pr:

I've pushed my drafts on this feature to separate branch orientation-support, lets move our work there.

I suggest creating layout module that will do all job for processing layout changes (not in utils as in your pr) Here it is in more or less ready state: https://github.com/vitalets/react-native-extended-stylesheet/blob/orientation-support/src/layout.js

Also I suggest to provide <AppContainer> component. Those developers who want to listen orientation change, should just wrap application into it.

AppContainer itself: https://github.com/vitalets/react-native-extended-stylesheet/blob/orientation-support/src/components/app-container.js

Example of wrapping app: https://github.com/vitalets/react-native-extended-stylesheet/blob/orientation-support/examples/orientation-change/app.js

The problem is to automatically update react components after orientation change. For that my thought was to register each component in tracker that will push updates after orientation change: https://github.com/vitalets/react-native-extended-stylesheet/blob/orientation-support/src/components/tracker.js This approach was not finally ready. So it woulb be great if play with stuff in orientation-change branch and make PR if got it working..

vitalets avatar Sep 23 '16 14:09 vitalets

Hey, Uranium supports orientation changes :)

tuckerconnelly avatar Nov 14 '16 02:11 tuckerconnelly

hi @tuckerconnelly

could you share with us which approach you are using to detect orientation change?

vitalets avatar Nov 14 '16 08:11 vitalets

Hi @vitalets , I believe it leverages https://github.com/walmartlabs/react-native-orientation-listener to do that.

tlvenn avatar Nov 21 '16 13:11 tlvenn

Oh sorry, how did I miss that notification? Yeah it's that ^, and then on top of that https://github.com/tuckerconnelly/react-native-match-media

I've thought about this quite a bit--you can also poll Dimensions if you don't want a third-party dependency. The React Native team also has better support for orientation on the roadmap, if you want to wait.

tuckerconnelly avatar Nov 21 '16 13:11 tuckerconnelly

What's the current way of listening to orientation changes? Wrapping the component as described by @vitalets ?

fermuch avatar Dec 02 '16 13:12 fermuch

I'm using this for my orientation change stuff. It works pretty well so far.

https://gist.github.com/jimthedev/edcfe00d3f27da2a248b97559ce6e29f

jimthedev avatar Feb 08 '17 05:02 jimthedev

@jimthedev thanks for sharing! Actually as I see you are using subscription on onLayout event.

A user MUST bind this function explicitly as an onLayout

vitalets avatar Feb 10 '17 13:02 vitalets

Correct

jimthedev avatar Feb 10 '17 14:02 jimthedev

I took a look at #21. Not a fan of the approach so far.

Instead of a very manual API ...

var updatedStyles = EStyleSheet.orientationUpdate(event, styles); 

... how about a more automatic API using property accessors?

Take styles.card. This should be a (memoized) property accessor that returns the style value that is correct according to the current orientation. That's literally all we need to do in this package.

class example extends React.Component {
  render() {
    return (
      <View style={styles.card}>
          {/* ... */}
      </View>
    );
  }
}

A user interested in responding to orientation would then need to re-render their views on orientation change.


After looking at existing solutions for this.. I feel we should make a slight change to EStyleSheet.build and allow the user to provide a function that returns the current orientation (this function would be called when styles.card is accessed.

Here is how I would use this.

import {observable} from "mobx";
import Orientation from "react-native-orientation";

class OrientationState {
  @observable _current = Orientation.getInitialOrientation();
  
  constructor() {
    Orientation.addOrientationListener((orientation) => {
      this._current = orientation;
    });
  }

  get = () => { 
    return this._current; 
  }
}

// ----

import EStyleSheet from "react-native-extended-stylesheet";

EStyleSheet.build({
  orientation: OrientationState.get,
});

And that's it. Any components that care about reacting to the orientation would just need to be marked @observer as styles._ is accessing OrientationState._current.

mehcode avatar Mar 23 '17 11:03 mehcode

I agree with you and moreover I think developer should not care about passing orientation fn to EStyleSheet. It should be detected inside lib as well as media queries in desktop browsers work - we don't pass orientation to it, we just declare rules how everything should be rendered..

vitalets avatar Mar 23 '17 15:03 vitalets

I think developer should not care about passing orientation fn to EStyleSheet.

Oh, I agree. The passing in EStyleSheet.build was more to override what otherwise would be detected so I could tap into the orientation changing via mobx. Looking back at what I wrote.. it is a touch wordy for something that should be as transparent as possible.

The key problem is in pure components. It'd be nice to just re-render the root on a layout change but that isn't possible if children are pure. Some more ideas:

  • Custom HoC like observesOrientation
  • Provide EComponent that is to be used instead of Component

mehcode avatar Mar 23 '17 19:03 mehcode

It'd be nice to just re-render the root on a layout change but that isn't possible if children are pure.

(RN beginner here). Can you explain why that isn't possible?

raarts avatar Jul 06 '17 09:07 raarts

(RN beginner here). Can you explain why that isn't possible?

Pure react components will only re-render if props or state changes. Unless Orientation is passed through as props all the way down your tree you couldn't easily re-render the whole tree based on orientation.


For what it's worth, I dropped this package and am using a scaled viewport. I don't need this scale to change if the orientation changes. Flexbox already handles resizing and repositioning everything when the orientation changes. Really I think that the idea of "I need to re-layout on orientation change" is a misnomer as I can't think of many reasons why you really want to.

My app is a huge business app and I have a manual orientation listener in one place (a video player) that detects when you rotate the phone to simulate full screen by disabling things like the notification bar.

By a scaled viewport, I mean I define all my sizes as if the only phone in existence was the iPhone 6 and then multiply the size by a ratio of the difference in size of the actual phone to the iPhone 6.

import {Dimensions} from "react-native";

const d = Dimensions.get("window");

const width = 375;
const height = 667;

const vw = (d.width / 100);
const vh = (d.height / 100);

const vmin = Math.min(vw, vh);

function vu(size) {
  return ((size / width) * vmin) * 100;
}

All of my sizes are defined like width: vu(30).

mehcode avatar Jul 06 '17 09:07 mehcode

Thanks. Yes, this is one of such cases where you decide early, and changing it afterwards turns out a huge amount of work.

I also think if you really need to re-layout on orientation change, you will pass down the state.

raarts avatar Jul 06 '17 10:07 raarts

@mehcode thanks for sharing the idea! Do you have different layouts on different orientations or you always only stretch?

vitalets avatar Jul 07 '17 10:07 vitalets

Mmm, do I understand this correctly? For iPhone 6:

const d = Dimensions.get("window");  // d = { width: 375, height: 667 } on iPhone 6

const width = 375;
const height = 667;

const vw = (d.width / 100);  // vw = 3.75 on iPhone 6
const vh = (d.height / 100); // vh = 6.67 on iPhone 6

const vmin = Math.min(vw, vh); // 3.75 on iPhone 6

function vu(size) {
  return ((size / width) * vmin) * 100;
}

// vu(30) would be ((30/375) *  3.75) * 100 = 30 on iPhone 6 (which is 8% of the width)

Now on an iPad Air:

const d = Dimensions.get("window");  // d = { width: 1536, height: 2018 } on iPad Air

const width = 375;
const height = 667;

const vw = (d.width / 100);  // vw = 15.36 on iPad Air
const vh = (d.height / 100); // vh = 20.18 on iPad Air

const vmin = Math.min(vw, vh); // 15.36 on iPad Air

function vu(size) {
  return ((size / width) * vmin) * 100;
}

// vu(30) would be ((30/375) *  15.36) * 100 = 122.88 on iPad Air (also 8%)

So it works, but for people who start now wouldn't it be easier to use percentages for everything, since that's supported by React now?

raarts avatar Jul 07 '17 10:07 raarts

Oh. Percents would work exactly the same way. I just find it far harder to mentally translate a design (that is usually dictated in pixels on the designers computer or relative to something they have like an iPhone) to percents.

Put another way. It's easy to ask a design team to define layout relative to an iPhone 6 (all mock programs I've seen support that) and then your development team can just use the sizes designers give, verbatim. Percent width (vw, rem, other various forms of solving this problem by engineers) all suffer from the same translation problem (that turns into a communication barrier between teams).


Do you have different layouts on different orientations or you always only stretch?

Nearly always scale only. The video player in my app has a different layout in landscape orientation but that's the only thing. Go play with some big, successful apps on your phone.

  • Slack — Scale only
  • Inbox (by Google) — Scale only with some max widths defined
  • Play Store — Scale plus it looks like the tab bar has a minor tweak in landscape
  • Twitter — Scale only
  • Facebook — Scale only
  • Messenger — Scale plus a minor tweak on the bottom nav

Etc. A separate layout for landscape is a waste of development time unless your app really does call for it.

mehcode avatar Jul 07 '17 17:07 mehcode

Put another way. It's easy to ask a design team to define layout relative to an iPhone 6 (all mock programs I've seen support that) and then your development team can just use the sizes designers give, verbatim

THAT is a very good point. Thanks!

raarts avatar Jul 07 '17 18:07 raarts

@mehcode: how do you handle heights?

raarts avatar Jul 07 '17 20:07 raarts

Same function. It works by applying a ratio.

Width of your device is exactly vu(375) and the height is vu(667).

mehcode avatar Jul 07 '17 21:07 mehcode

Thanks @mehcode ! I think we can go the same scale-only way.

vitalets avatar Jul 08 '17 11:07 vitalets

@mehcode : what's the reason for using the 100-factor? I think it can be done like this as well:

// iPhone 6 sizes
const width = 375;
const height = 667;

const screen = Dimensions.get("screen");
const vmin = Math.min(screen.width, screen.height); // compensate for landscape mode
const scale = vmin / width;

function vu(size) {
  return size * scale;
}

BTW I changed 'window' into 'screen' because they differ on Android, and screen seems to be the most appropriate

raarts avatar Jul 10 '17 08:07 raarts