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 es 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:
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 bee difficult if you think about it.
My current solution has the following steps when transitioning from DIV1 to DIV2:
- position
top
of DIV2 to the currentscrollTop
of the window - animate DIV1's and DIV2's
left
property so that DIV2 fills the screen - move DIV2's
top
to0
once the animation has finished so that the user can't scroll back further than the top of this screen - Move the window's scrollTop to 0
- 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 plex 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.
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 es 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:
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 bee difficult if you think about it.
My current solution has the following steps when transitioning from DIV1 to DIV2:
- position
top
of DIV2 to the currentscrollTop
of the window - animate DIV1's and DIV2's
left
property so that DIV2 fills the screen - move DIV2's
top
to0
once the animation has finished so that the user can't scroll back further than the top of this screen - Move the window's scrollTop to 0
- 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 plex 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.
Share Improve this question edited May 23, 2017 at 11:46 CommunityBot 11 silver badge asked Aug 12, 2014 at 18:57 gphilipgphilip 70615 silver badges33 bronze badges 9- 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 Commented Aug 15, 2014 at 16:22
- 1 Maybe going fullscreen solved your problem. Then you could use the first method you mentioned. – dieortin Commented Aug 16, 2014 at 22:02
- @MattDerrick not true see github./TNT-RoX/android-swipe-shim – tnt-rox Commented Aug 18, 2014 at 7: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 Commented Aug 18, 2014 at 11:43
- 1 @gphilip what a great site: vanilla-js.. thanks for mentioning it. i will have to show it to some people who are always wondering why their jquery is so slow ;) – dreamlab Commented Aug 21, 2014 at 19:39
5 Answers
Reset to default 2 +100your 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:
- save the current scroll vertical position
- change the div's to
position: fixed
- change the div's
scrollTop
to0 - scrollPosition
- start horizontal transition
after the transition:
- change the window's scroll position with
scrollTo()
- 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 userequestAnimationFrame()
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.
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/articles/how-to-native-iphone-ipad-apps-in-javascript/
and for safari
https://developer.apple./library/safari/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html
Hope it helps you somehow!!
Using window.scrollTo(0,1);
you can make the navigation bar disappear. It's a hack, but it works.
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./M
Visit on your mobile device.
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 pletely 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.