单页面Web应用程序屏幕转换并保持滚动位置

13

我正在为移动手机构建单页面Web应用程序。该应用程序应该实现“屏幕”之间的过渡(就像其他任何移动应用程序,例如Facebook,Twitter),并且这些过渡应该是动画效果(从左到右滑动)。每个屏幕在过渡期间都必须保留其滚动位置。

一个显而易见的解决方案是:

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

不同的屏幕被放入容器中(DIV1、DIV2等),这些容器被设计成适合屏幕大小的 (position: absolute; width: 100%; height: 100%; top: 0),并且具有 overflow-x: scroll 属性。容器被相邻地定位,过渡的方式就像动画化它们的 left 属性一样简单。

到目前为止还好。

问题在于:在这种实现中,当用户向下滚动时,移动浏览器中的地址栏不会消失。

我说的是这个功能: enter image description here

这是因为移动浏览器仅在用户滚动 body 时才会这样做,而不是在 body 中的容器中滚动。有一些解决方案(several)(suggestions),但不适用于所有目标平台(Android Chrome 和原生浏览器,iPhone Safari),而且非常 hacky。我想保留浏览器的原始行为。

因此 - 显然 - 需要将滚动留在 body 上。这意味着容器必须是“全长”的(而不是溢出滚动),仍然相邻地定位。这就是过渡变得困难的地方。

当从 DIV1 过渡到 DIV2 时,我的当前解决方案包括以下步骤:

  1. 将 DIV2 的 top 定位到窗口当前的 scrollTop
  2. 动画化 DIV1 和 DIV2 的 left 属性,以便 DIV2 填充屏幕
  3. 一旦动画完成,将 DIV2 的 top 移动到 0,以便用户无法向上滚动超过此屏幕的顶部
  4. 将窗口的 scrollTop 移动到 0
  5. 隐藏 DIV1(以防它比 DIV2 更长)

反向过渡回 DIV1 类似。实际上,这非常好用(尽管非常复杂,并且使用了转换事件监听器),但不幸的是,在 iOS Safari 下,在步骤 3 和 4 之间存在一个非常丑陋的闪烁效果,因为它会在步骤 3 完成后立即呈现页面,而不等待步骤 4。

我正在寻找一个与框架无关的 (vanilla JS) 解决方案。


没有滚动主体就无法保持原生浏览器的行为,这是不可能的。我在完全相同的用例中花了很多时间研究这个问题(在单页应用程序中转换页面)。我很想知道你最终会做什么! - Matt Derrick
1
也许全屏可以解决你的问题。然后你可以使用你提到的第一种方法。 - dieortin
1
@tnt 我不确定那个链接有什么帮助...?问题是如何滚动掉地址栏,而不必使用body滚动,而且不仅限于Android(似乎那个链接也无法解决这个问题)。 - Matt Derrick
1
@gphilip,太棒了,这个网站http://vanilla-js.com/。谢谢你提到它。我得向一些经常纳闷为什么他们的jQuery运行如此缓慢的人展示一下。;) - dreamlab
感谢 @dreamlab ;-) 我已经厌倦了人们假设 jQuery === javascript 并在 JavaScript 问题中发布 jQuery 答案的情况。jQuery 是一个在 web 平台过渡期证明有用的库,但越来越不相关了。放手吧! - gphilip
显示剩余4条评论
5个回答

2
如果您已经加载了jquery,您可以像这样执行某些操作。
$(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);
}

});

对于 iPhone,您需要按照下面链接中提到的步骤进行操作: http://matt.might.net/articles/how-to-native-iphone-ipad-apps-in-javascript/ 对于 Safari 浏览器,可以参考以下链接: https://developer.apple.com/library/safari/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html 希望这些信息能对您有所帮助!

2
它写成了这样:“问题如下:在此实现中,当用户向下滚动时,移动浏览器中的地址栏不会消失。”。因此,似乎非常清楚,您想让地址栏消失,这就是我为您提供的。如果需要其他内容,请指出。 - StackUser
是的,显然您只读了那一个句子。请阅读整个问题,您就会知道为什么我不能接受您的答案。 - gphilip

2
你的方法很正确。由于滚动变化位置,可能导致闪烁。诀窍是在滚动时将 div 的位置更改为 position: fixed,然后在之后将它们改回。
步骤如下:
  1. 保存当前滚动垂直位置
  2. 将 div 的位置更改为 position: fixed
  3. 将 div 的 scrollTop 更改为 0 - scrollPosition
  4. 开始水平过渡
过渡后:
  1. 使用 scrollTo() 更改窗口的滚动位置
  2. 恢复 div 的 position: fixed,使其自然浏览器行为正常。
这里有一个纯 JavaScript 示例(也作为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>

在这个例子中,我使用了transitionEnd事件。请注意,如果两个动画的div都使用了该事件,它将会触发两次。解决方法有:
  • 如果时间相同,请只在一个div上使用事件(在示例中使用)
  • 在所有div上使用事件,但是只针对事件的div进行更改
  • 用所有div放在一个容器内,这样你只需要一个事件
  • 如果不能使用transitionEnd事件,请使用requestAnimationFrame()并通过js手动进行动画处理
在此示例中,我还使用了固定高度的容器进行过渡效果。如果您有不同高度的div,那么您需要在过渡结束后更改容器的高度,最好在还原成position:fixed之前进行操作。
请注意,将div更改为position:fixed会使其显示出来,即使它位于具有overflow:hidden的容器中也是如此。在移动Web应用程序的情况下,这不是问题,因为div在屏幕外。在PC上,您可能需要在另一个div上方添加另一个div以隐藏正在转换中的div。

谢谢@gphilip。希望解决方案能够顺利运行。您可能想以通用的方式实现它,这样您就可以在所有情况下使用它。 - dreamlab
谢谢。我仍然不确定为什么在window.scrollTo之前需要setTimeout,但是如果没有这一步骤,它就无法正常工作,在iOS上会闪烁。 - gphilip
在这种情况下,您可以尝试将该层推广到GPU。我在类似的情况下在iOS上遇到了一些闪烁问题。您可以应用translateZ来将其放入GPU。在某些情况下,我也遇到了left:更新的问题。改用translateX即可解决。 - dreamlab
如果可能的话,尝试将它们从GPU中移除。有时候如果太多东西在那里会出现问题。 - dreamlab
这篇HTML5 Rocks文章解释了使用transform: translate(320px)进行动画比使用left: 320px更快。我相信这个fiddle在任何地方都非常流畅。(还可以考虑将所有的320px替换为100%。) - Dan Fabulich
显示剩余3条评论

0

我想我弄清楚了,这很棘手。

简而言之:在问题中,我描述了在iOS上闪烁的当前解决方案。在第3点,您需要向DIV2添加position: fixed。这样它就会“粘住”,您就可以避免在第4点处出现闪烁。然后,您需要延迟几毫秒(setTimeout,500ms适合我,但可能可以更小),并在window.scrollTo之后立即将position: absolute再次设置为DIV2。我不确定这是需要延迟的原因,但没有它,屏幕仍然会闪烁。

如果有兴趣,我可以稍后发布PoC。

另外,我发现令人非常失望的是,大多数回答的人并没有完全阅读问题或完全忽略了某些标准(框架独立性,保持原始滚动行为)。他们中的大多数建议的解决方案我已经在问题中明确指出是不可接受的。其中一些甚至在被投票降级时都会声称。

编辑:dreamlab在我发布我的解决方案几分钟前回答。两种解决方案都使用position: fixed。他的解决方案也更加详细。他应该获得奖励。


如果固定位置已更改,则不需要超时。我在我的答案中详细解释了它。 - dreamlab
抱歉,我没有看到你的回答,因为当你发布时我正在输入我的。让我快速浏览一下。 - gphilip

0

使用window.scrollTo(0,1);可以使导航栏消失。这是一种hack方法,但它有效。


2
@gphilip,我认为从你的问题中引用“问题如下:在此实现中,当用户向下滚动时,移动浏览器中的地址栏不会消失”并不公平对待dieortin。我也没有完全理解你的问题。 - tnt-rox
1
请阅读我实际上是在引用他人建议你的解决方案的那一部分:这是因为移动浏览器只有在用户滚动主体时才会执行此操作 - 而不是主体中的容器。有几个解决方案的建议,但它们并不适用于所有目标平台(Android Chrome和本机浏览器,iPhone Safari),而且相当具有hack性。我想保留浏览器的原始行为。 - gphilip
1
这个解决方案可能有效,当用户开始向下滚动您的内部内容时,您可以使用scrollTop函数来隐藏导航栏,向上滚动则相反,我不确定反向功能。 - ymutlu
@gphilip 所以,原始行为存在问题,您不希望出现这个问题,但是您想保留原始行为。这听起来对您来说正确吗?您确定您正在问正确的问题吗? - pkExec

0
为什么不这样做:
<body>
     <div id=header>Header</div>
     <div id=content>Scrollable Content</div>
     <div id=footer>Footer</div>
</body>

然后是 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*/
}

触摸屏响应 <body> 标签,而不是 <div>,因此在 #header#footer 上设置 position:fixed 可以使它们相对于窗口保持位置不变,无论滚动位置如何,当用户滚动内容时,他们滚动的是 <body>

编辑:我已经实现了这个例子:

https://www.museri.com/M

请使用您的移动设备访问。


你的实现中转换在哪里? - gphilip
一秒钟,让我添加它。 - Evan Hendler
好的,现在请查看链接。这是你想要的吗? - Evan Hendler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接