13

I'm build a single page web application for mobile phones. The application should implement transitions between "screens" (like any other mobile app e.g. Facebook, Twitter) and these transitions should be animated (slide left-right). Each screen has to preserve its scroll position between transitions.

One obvious solution that comes in mind is this:

Viewport
+----------+ 
|+--------+| +--------+ +--------+ +--------+
|| DIV1   || | DIV2   | | DIV3   | | DIV4   |
||        || |        | |        | |        |
||        || |        | |        | |        |
||        || |        | |        | |        |
||        || |        | |        | |        |
|+--------+| +--------+ +--------+ +--------+
+----------+

The different screens are put into containers (DIV1, DIV2, ...) which are styled to fit the screen (position: absolute; width: 100%; height: 100%; top: 0) and have overflow-x: scroll. The containers are positioned next to each other and the transition is as easy as animating their left property.

Easy so far.

The problem is the following: in this implementation the address bar doesn't disappear in the mobile browser when the user scrolls down.

I'm talking about this feature: enter image description here

It's because mobile browsers do this only if the user scrolls the body - not a container in the body. There are several suggestions for solution but they don't work in all targeted platforms (Android Chrome and native browser, iPhone Safari) and are quite hacky. I'd like to preserve the original behavior of the browser as it is.

For that reason - apparently - need to leave the scrolling on the body. This means that containers have to be "full-length" (and not overflow-scroll), still positioned next to each other. This is where transitions become difficult if you think about it.

My current solution has the following steps when transitioning from DIV1 to DIV2:

  1. position top of DIV2 to the current scrollTop of the window
  2. animate DIV1's and DIV2's left property so that DIV2 fills the screen
  3. move DIV2's top to 0 once the animation has finished so that the user can't scroll back further than the top of this screen
  4. Move the window's scrollTop to 0
  5. Hide DIV1 (in case it's longer than DIV2)

Transitioning back to DIV1 is similar, in reverse. This actually works quite nice (although it's insanely complex and uses transition event listeners) but unfortunately there's a really ugly flickering effect between step 3 and 4 under iOS Safari because it renders the page right after step 3 without waiting for step 4.

I am looking for a framework-independent (vanilla JS) solution.

Community
  • 1
  • 1
gphilip
  • 1,114
  • 15
  • 33
  • It's simply not possible to keep the native browser behaviour without the scrolling body. I've spent a lot of time looking at this in the exact same use case (transitioning pages in a single page app). I'd love to know what you end up doing! – Matt Derrick Aug 15 '14 at 16:22
  • 1
    Maybe going fullscreen solved your problem. Then you could use the first method you mentioned. – dieortin Aug 16 '14 at 22:02
  • @MattDerrick not true see https://github.com/TNT-RoX/android-swipe-shim – tnt-rox Aug 18 '14 at 07:36
  • 1
    @tnt I'm not sure how that link helps at all...? The question is how to scroll away the address bar without having to use the body scroll and not just on Android either (not that that link seems to solve this problem anyway). – Matt Derrick Aug 18 '14 at 11:43
  • @MattDerrick Thx for clearing that up. I had no idea what this user is trying to achieve. Maybe it's the title! – tnt-rox Aug 18 '14 at 12:55
  • I think there's no problem with scrolling with the body but this has other negative implications (see in question). – gphilip Aug 18 '14 at 15:58
  • Yea, those negative implications in our use case were far worse than than the benefit of the native browser behaviour we wanted to keep. We just simply couldn't make the app we desired with full body scroll. Good luck! – Matt Derrick Aug 19 '14 at 09:04
  • 1
    @gphilip what a great site: http://vanilla-js.com/. thanks for mentioning it. i will have to show it to some people who are always wondering why their jquery is so slow ;) – dreamlab Aug 21 '14 at 19:39
  • Thanks @dreamlab ;-) I'm just so tired of people assuming `jQuery === javascript` and posting jQuery answers to a JavaScript question. jQuery is a library that proved to be useful in a transitional period of the web as a platform but is becoming less and less relevant. Let it go! – gphilip Aug 21 '14 at 19:57

5 Answers5

2

You can do something like this if you jquery is loaded

$(document).ready(function() {
if (navigator.userAgent.match(/Android/i)) {
window.scrollTo(0,0); // reset in case prev not scrolled  
var nPageH = $(document).height();
var nViewH = window.outerHeight;
if (nViewH > nPageH) {
  nViewH -= 250;
  $('BODY').css('height',nViewH + 'px');
}
window.scrollTo(0,1);
}

});

For Iphone you have to do something like mentioned in below link

http://matt.might.net/articles/how-to-native-iphone-ipad-apps-in-javascript/

and for safari

https://developer.apple.com/library/safari/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html

Hope it helps you somehow!!

StackUser
  • 141
  • 1
  • 1
  • 8
  • 2
    Its, written the as "The problem is the following: in this implementation the address bar doesn't disappear in the mobile browser when the user scrolls down.". So seems very clear that you want to make the address bar disappears and that is what i offered you.Please correct, if something else is required. – StackUser Aug 20 '14 at 01:38
  • Yes, apparently you read only that single sentence. Please read the whole question and you'll see why I cannot accept your answer. – gphilip Aug 20 '14 at 15:48
2

your approach was quite right. you probably get the flickering due to the scroll change position. the trick is to change the div's to position: fixed when scrolling and, than change them back afterwards.

the steps are:

  1. save the current scroll vertical position
  2. change the div's to position: fixed
  3. change the div's scrollTop to 0 - scrollPosition
  4. start horizontal transition

after the transition:

  1. change the window's scroll position with scrollTo()
  2. revert position: fixed on the div's so the natural browser behavior works.

here is a plain vanilla javascript example (also as fiddle):

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
        <title></title>

        <style type="text/css">
            body {
                margin: 0;
                padding: 0;
            }

            .container {
                position: absolute;
                overflow: hidden;
                width: 320px;
                height: 5000px;
            }

            .screen {
                position: absolute;
                width: 320px;
                height: 5000px;
                transition: left 0.5s;
            }

            #screen1 {
                background: linear-gradient(red, yellow);
            }

            #screen2 {
                left: 320px;
                background: linear-gradient(green, blue);
            }

            #button {
                position: fixed;
                left: 20px;
                top: 20px;
                width: 100px;
                height: 50px;
                background-color: white;
                color: black;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <div id="screen1" class="screen"></div>
            <div id="screen2" class="screen"></div>
        </div>

        <div id="button">transition</div>


        <script type="text/javascript">
            var screenActive = 1;
            var screen1 = document.getElementById('screen1');
            var screen2 = document.getElementById('screen2');
            var screen1ScrollTop = 0;
            var screen2ScrollTop = 0;

            function onClick()
            {
                console.log('getting the event');

                if ( screenActive === 1 ) {
                    // will show screen 2
                    screen1ScrollTop = document.body.scrollTop;

                    screen1.style.position = 'fixed';
                    screen2.style.position = 'fixed';
                    screen1.style.top = (0 - screen1ScrollTop) + 'px';
                    screen2.style.top = (0 - screen2ScrollTop) + 'px';

                    screenActive = 2;
                    screen1.style.left = '-320px';
                    screen2.style.left = '0px';
                }
                else {
                    // will show screen 1
                    screen2ScrollTop = document.body.scrollTop;

                    screen1.style.position = 'fixed';
                    screen2.style.position = 'fixed';
                    screen1.style.top = (0 - screen1ScrollTop) + 'px';
                    screen2.style.top = (0 - screen2ScrollTop) + 'px';

                    screenActive = 1;
                    screen1.style.left = '0px';
                    screen2.style.left = '320px';
                }
            }

            function onTransitionEnd(event)
            {
                if ( screenActive === 1 ) {
                    window.scrollTo(0, screen1ScrollTop);
                }
                else {
                    window.scrollTo(0, screen2ScrollTop);
                }

                screen1.style.position = 'absolute';
                screen1.style.top = '0px';

                screen2.style.position = 'absolute';
                screen2.style.top = '0px';
            }

            screen1.addEventListener('webkitTransitionEnd', onTransitionEnd);
            document.getElementById('button').addEventListener('click', onClick);
        </script>

    </body>
</html>

in this example i used the transitionEnd event. have in mind that if you have this event on both animating div's the event will fire twice. solutions to this are:

  • if the timings are identical just use the event on one div (used in the example)
  • use the event an all div's but just do changes respective to the event's div
  • animate a container with all the div's inside. so you will just need one event.
  • if you can not use the transitionEnd event use requestAnimationFrame() and animate manually via js

i also use a fixed height container for the transitions in this example. if you have div's with different height's you will have to change the containers height after the transition. ideally before reverting from position: fixed.

have in mind that changing a div to position: fixed will show it even if it is in a container with overflow: hidden. in the case of a mobile webapp this will not be an issue because the div's are outside of the screen. on a pc you might have to put another div over the other to hide the one transitioning in.

dreamlab
  • 3,321
  • 20
  • 23
  • thank you @gphilip. hope the solution works well. you might want to implement it in a generic way, so you can use it in all cases – dreamlab Aug 21 '14 at 20:22
  • Thanks for that. I'm still not sure why I need the `setTimeout` before the `window.scrollTo` but it just didn't work without that step, kept flickering in iOS. – gphilip Aug 21 '14 at 20:22
  • Yeah... to be fair my actual implementation is slightly different as the containers don't all slide to the left-right but there's a main container and others just slide over it from the right. When that happens I hide the main controller to fox the scroll height of the window. That might cause some flickering also, although I'm not sure. In Android it worked without the setTimeout, not in iOS though. – gphilip Aug 21 '14 at 20:28
  • in that case you can try to promote the layer to the gpu. i had quite some flickering issues on iOS in a similar scenario. you can apply a `translateZ` to get in on the gpu. in some scenarios i also had issues with `left:` updating. using `translateX` instead worked. – dreamlab Aug 21 '14 at 20:33
  • Thanks for the tip. They are already in the GPU. Also all transitions are done with `translate3d` for exactly that reason. – gphilip Aug 21 '14 at 21:02
  • than maybe try removing them from gpu. sometimes if too many stuff is in there problems arise as well. – dreamlab Aug 21 '14 at 21:10
  • Did you ever get this working, flicker-free across all major mobile platforms? – x0b Feb 02 '15 at 19:47
  • This [HTML5 Rocks article](https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/) explains that it's _much_ faster to animate `transform: translate(320px)` than it is to animate `left: 320px`. I believe [this fiddle](http://jsfiddle.net/ncbzz5zu/1/) is silky smooth everywhere. (Also consider replacing all of the `320px` references with `100%`.) – Dan Fabulich Apr 16 '17 at 05:41
0

Using window.scrollTo(0,1); you can make the navigation bar disappear. It's a hack, but it works.

dieortin
  • 514
  • 5
  • 16
  • 2
    @gphilip, I don't think it fair to -1 dieortin quoting from your question "The problem is the following: in this implementation the address bar doesn't disappear in the mobile browser when the user scrolls down." I don't fully understand your problem either. – tnt-rox Aug 18 '14 at 07:41
  • 1
    Please read this part where I actually refer to others suggesting your solution: It's because mobile browsers do this only if the user scrolls the body - not a container in the body. There are several suggestions for solution but they don't work in all targeted platforms (Android Chrome and native browser, iPhone Safari) and are quite hacky. **I'd like to preserve the original behavior of the browser as it is.** – gphilip Aug 18 '14 at 15:55
  • 1
    This solution might work, when user start to scroll down your inner content you can use scrollTop function to disappear navigation bar and reverse for scroll up, i am not sure about reverse function. – ymutlu Aug 20 '14 at 10:59
  • @gphilip So, the original behavior is presenting a problem, and you don't want the problem to occur, but you want to keep the original behavior. Does that sound right to you? Are you sure you are asking the right question? – pkExec Aug 20 '14 at 18:26
0

Why not:

<body>
     <div id=header>Header</div>
     <div id=content>Scrollable Content</div>
     <div id=footer>Footer</div>
</body>

Then the CSS:

#header,#footer,#content{
    left:0%;
    right:0%;
}
#header,#footer{
    position:fixed;
}
#header{
    top:0%;
    height:30px;
}
#footer{
    bottom:0%;
    height:30px;
}
#content{
    position:absolute;
    top:30px;
    height:1000px; /*Whatever you need it to be*/
}

The touch screen responds to the <body> tag, not the <div>, so setting position:fixed on #header and #footer allow them to maintain position relative to the window, regardless of scroll position, and then when the user scrolls the content, they scroll the <body>

EDIT: I have implemented this as an example:

https://www.museri.com/M

Visit on your mobile device.

Evan Hendler
  • 342
  • 1
  • 12
0

I think I figured it out, it's tricky.

In short: in the question I describe my current solution that flickers in iOS. At point 3 you need to add position: fixed to DIV2. That way it's gonna "stick" and you avoid the flickering at point 4. Then you need to delay point 4 a couple of milliseconds (setTimeout, 500ms worked for me but probably could be smaller) and set position: absolute again to DIV2 right after window.scrollTo. I'm not sure that's the reason you need the delay, but without it the screen still flickers.

If there's interest I can post a PoC later.

As a side note, I found it pretty disappointing that most if the people who answered did not read the question entirely or just completely ignored some criteria (framework-independence, keeping original scroll behavior). Most of them suggested solutions that I already specifically linked in the question as not acceptable. Some of them even reclaim when get downvoted.

EDIT: dreamlab answered just a couple of minutes before I posted my solution. Both solutions use position: fixed. His solution is more detailed too. He deserves the bounty.

gphilip
  • 1,114
  • 15
  • 33
  • you don't need a timeout if you change the scroll position while fixed. i explain it in some detail in my answer – dreamlab Aug 21 '14 at 20:04
  • Sorry, I didn't see your answer because I was typing mine when you posted yours. Let me glance though it quickly. – gphilip Aug 21 '14 at 20:17