Одностраничные переходы на экран Webapp с сохранением положения прокрутки

Я создаю одностраничное веб-приложение для мобильных телефонов. Приложение должно реализовывать переходы между "экранами" (как и любое другое мобильное приложение, например 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. Есть несколько предложений для решения, но они не работают на всех целевых платформах (Android Chrome и собственный браузер, iPhone Safari) и довольно хаки. Я бы хотел сохранить исходное поведение браузера, как есть.

По этой причине - по-видимому - нужно оставить прокрутку на теле. Это означает, что контейнеры должны быть "полноразмерными" (а не переполненными), которые все еще расположены рядом друг с другом. Здесь переходы становятся трудными, если вы думаете об этом.

Мое текущее решение имеет следующие шаги при переходе с DIV1 на DIV2:

  • позиция top от DIV2 до текущего scrollTop окна
  • оживить свойства DIV1 и DIV2 left, чтобы DIV2 заполнил экран
  • переместите DIV2 top в 0 после завершения анимации, чтобы пользователь не мог прокручивать назад дальше, чем верхняя часть этого экрана.
  • Переместить окно scrollTop в 0
  • Скрыть DIV1 (если он длиннее DIV2)

Переход обратно в DIV1 аналогичен, наоборот. На самом деле это работает довольно неплохо (хотя он безумно сложный и использует прослушиватели событий перехода), но, к сожалению, там действительно ужасный мерцающий эффект между шагами 3 и 4 под iOS Safari, потому что он отображает страницу сразу после шага 3, не дожидаясь шага 4.

Я ищу независимую от структуры (vanilla JS).

Ответ 1

ваш подход был совершенно прав. вы, вероятно, получите мерцание из-за положения изменения прокрутки. трюк заключается в том, чтобы сменить div на position: fixed при прокрутке и, вместо этого, изменить их назад.

выполняются следующие шаги:

  • сохранить текущую вертикальную позицию прокрутки
  • измените div на position: fixed
  • измените div scrollTop на 0 - scrollPosition
  • начать горизонтальный переход

после перехода:

  • измените положение прокрутки окна с помощью scrollTo()
  • верните position: fixed в div, чтобы работать с естественным браузером.

вот простой пример ванильного 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 (используется в примере)
  • используйте событие all div, но просто делайте изменения, соответствующие событию div
  • анимировать контейнер со всем div внутри. поэтому вам просто нужно одно событие.
  • если вы не можете использовать событие transitionEnd requestAnimationFrame() и анимировать вручную через js

Я также использую контейнер с фиксированной высотой для переходов в этом примере. если у вас есть div с разной высотой, вам придется изменить высоту контейнеров после перехода. в идеале перед возвратом от position: fixed.

помните, что изменение div на position: fixed покажет его, даже если оно находится в контейнере с overflow: hidden. в случае мобильного webapp это не будет проблемой, потому что div находится за пределами экрана. на ПК вам, возможно, придется поместить другой div поверх другого, чтобы скрыть тот, который переходит.

Ответ 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/

и для сафари

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

Надеюсь, вам это поможет!

Ответ 3

Используя window.scrollTo(0,1);, вы можете отключить навигационную панель. Это взлом, но он работает.

Ответ 4

Почему бы и нет:

<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>, поэтому установка position:fixed на #header и #footer позволяет им сохранять позицию относительно окна независимо от положения прокрутки, а затем когда пользователь прокручивает содержимое, они прокручивают <body>

EDIT: я применил это как пример:

https://www.museri.com/M

Посетите мобильное устройство.

Ответ 5

Я думаю, что понял, это сложно.

Вкратце: в вопросе я описываю свое текущее решение, которое мерцает в iOS. В точке 3 вам нужно добавить position: fixed в DIV2. Таким образом, он будет "придерживаться", и вы избегаете мерцания в точке 4. Затем вам нужно задержать точку 4 за пару миллисекунд (setTimeout, 500 мс для меня, но, вероятно, может быть меньше) и снова установить position: absolute на DIV2 сразу после window.scrollTo. Я не уверен, что причина вам нужна задержка, но без нее экран все еще мерцает.

Если есть интерес, я могу опубликовать PoC позже.

В качестве побочной заметки я счел довольно разочаровывающим, что большинство, если люди, которые ответили, не полностью прочитали вопрос или просто полностью проигнорировали некоторые критерии (независимость структуры, сохраняя оригинальное поведение прокрутки). Большинство из них предложили решения, которые я уже специально связал в вопросе, как неприемлемые. Некоторые из них даже возвращаются, когда их останавливают.

EDIT: dreamlab ответил всего за пару минут, прежде чем отправил свое решение. Оба решения используют position: fixed. Его решение более подробно. Он заслуживает щедрости.