Дождитесь окончания перекомпоновки после requestFullscreen

Ладно, парни и девчонки, вот вам, волшебники, хитрый вопрос...

Я работаю над иммерсивным веб-приложением, которое переопределяет поведение сенсорной прокрутки по умолчанию на мобильных устройствах. Содержимое разделено на страницы, которые используют 100% области просмотра, а навигация осуществляется с помощью прокрутки вверх и вниз между страницами.

При первом свайпе я вызываю requestFullscreen() для элемента body, что, конечно же, вызывает перекомпоновку при изменении размеров области просмотра. Проблема в том, что я также хочу, чтобы этот первый жест запускал пользовательское поведение прокрутки, но я использую Element.nextElementSibling.scrollIntoView({ block : start, behavior : 'smooth' }), и пока перекомпоновка не будет завершена, верхний край следующей страницы (HTMLSectionElement) уже виден, поэтому прокрутка не происходит.

Если я использую setTimeout, чтобы подождать около 600 мс, пока не завершится перекомпоновка, эффект прокрутки будет работать, как и ожидалось, но мне не нравится этот хакерский обходной путь, и я бы предпочел использовать более элегантное асинхронное решение.

Сначала я попытался вызвать эффект прокрутки изнутри исполнителя resolve обещания, возвращаемого requestFullscreen, но это не помогло. Это обещание разрешается очень рано в потоке выполнения.

Затем я попытался изнутри обработчика событий fullscreenchange. Здесь тоже не повезло, так как это событие запускается непосредственно до перехода в полноэкранный режим.

Наконец, я попытался изнутри обработчика событий окна resize, но это срабатывает до того, как произойдет перекомпоновка. Я также добавил здесь requestIdleCallback, но это не имело никакого значения.

Итак, мой вопрос: Есть ли какой-либо надежный способ определить конец операции перекомпоновки? Или, альтернативно... есть ли у кого-нибудь план Б получше, чем отказаться от использования scrollIntoView и написать собственный код? эффект прокрутки в обработчик изменения размера окна.


person Besworks    schedule 31.05.2019    source источник
comment
Операция reflow является синхронной. Чего может не быть, так это когда его вызывают. См. этот мой ответ. Итак, нет, на самом деле вы не пытаетесь обнаружить конец операции перекомпоновки. Я не уверен, что вы ищете, потому что в вашем вопросе отсутствует минимальный воспроизводимый пример. Может быть, вы скорее хотите принудительно перекомпоновать, чтобы CSSOM был актуальным, когда вы начинаете прокрутку, или вы хотите определить, когда изменение размера закончится? Но вы задаете не правильный вопрос.   -  person Kaiido    schedule 16.03.2021
comment
Я спросил об этом почти 2 года назад о конкретной проблеме с веб-приложением, над которым я работал в то время. К сожалению, Fullscreen API запросы не работают внутри iframe элементов, поэтому добавить работающий фрагмент проблемы было невозможно. Конкретная проблема, которая у меня была 2 года назад, в любом случае больше не имеет значения, поэтому я, вероятно, перепишу это, чтобы сделать его более общим и полезным вопросом для более широкой аудитории.   -  person Besworks    schedule 16.03.2021


Ответы (2)


В случае, если кто-то захочет знать, я справился с этой ситуацией, запустив эффект прокрутки из прослушивателя событий изменения размера окна через debounce. Он по-прежнему использует setTimeout, но постоянно сбрасывает счетчик, чтобы гарантировать, что прокрутка произойдет после того, как все события изменения размера сработают (4 в случае Chrome).

Это не самое элегантное решение, и эффект прокрутки может быть отложен больше, чем это абсолютно необходимо, но, по крайней мере, прокрутка никогда не будет заблокирована перекомпоновкой, и я могу с этим смириться.

Надеюсь, однажды у нас будет событие ReflowEndEvent или что-то подобное, которое мы сможем послушать.

person Besworks    schedule 03.06.2019

Хорошо, будущие гуглеры, через пару лет я вернулся с НАСТОЯЩИМ, не хакерским ответом на эту проблему.

Хитрость заключается в использовании двухэтапного requestAnimationFrame. цепочка внутри ResizeObserver. Первый обратный вызов запускается прямо перед тем, как происходит перекомпоновка. Вы можете использовать этот обратный вызов для внесения любых изменений в DOM в последнюю секунду. Затем внутри этого обратного вызова мы снова используем requestAnimationFrame, чтобы указать другой обратный вызов, который произойдет после отрисовки из предыдущего кадра.

Вы можете проверить синхронизацию этого метода, раскомментировав debugger; строки в приведенном ниже примере кода.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="description" content="React to element size changes after layout reflow has occured.">
    <title> Reflow Observer Test </title>
    <style>
      * { box-sizing: border-box; }
      html, body { height: 100vh; margin: 0; padding: 0; }
      body { background: grey; }
      body:fullscreen { background: orange;}
      body:fullscreen::backdrop { background: transparent; }
      #toggle { margin: 1rem; }
    </style>
  </head>
  <body>
    <button id="toggle"> Toggle Fullscreen </button>
    <script>
      let resizableNode = document.body;
      
      resizableNode.addEventListener('reflow', event => {
        console.log(event.timeStamp, 'do stuff after reflow here');
      });
      
      let observer = new ResizeObserver(entries => {
        for (let entry of entries) {
          requestAnimationFrame(timestamp => {
            // console.log(timestamp, 'about to reflow'); debugger;
            requestAnimationFrame(timestamp => {
              // console.log(timestamp, 'reflow complete'); debugger;
              entry.target?.dispatchEvent(new CustomEvent('reflow'));
            });
          });
        }
      });
      
      observer.observe(resizableNode);
      
      function toggleFullscreen(element) {
        if (document.fullscreenElement) {
          document.exitFullscreen();
        } else {
          element.requestFullscreen();
        }
      }
      
      document.getElementById('toggle').addEventListener('click', event => {
        toggleFullscreen(resizableNode);
      });
    </script>
  </body>
</html>

В этом примере я использую переход в полноэкранный режим для элемента body в качестве триггера, но это можно легко применить к любой операции изменения размера на любом узле DOM.

person Besworks    schedule 15.03.2021
comment
Я обернул это в хороший аккуратный класс ES6, доступный здесь github.com/besworks/ReflowObserver - person Besworks; 16.03.2021
comment
Зачем снова ждать полный кадр? Просто подождите одну задачу или, что еще лучше, используйте requestPostAnimationFrame, где это возможно, см. of-a-frame-and-not-at/58090066#58090066">этот мой ответ. Также имейте в виду, что все браузеры не запускают auto-reflow одновременно. Например, Safari вызовет его в режиме ожидания еще до того, как сработает rAF, поэтому в этом браузере, если вы испортите CSSOM в rAF, вы фактически вызовете новую, ненужную перекомпоновку прямо перед отрисовкой. - person Kaiido; 16.03.2021
comment
Отличные заметки @kaiido, я не слышал о requestPostAnimationFrame. Это действительно работает вместо второго requestAnimationFrame, когда я тестировал в Chrome 89. В моем случае меня беспокоят только браузеры на основе Chromium, но ваши заметки о времени в других браузерах могут помочь другим сузить круг их проблем. - person Besworks; 16.03.2021
comment
Да, и если вы ориентируетесь только на браузеры Chromium, остерегайтесь их реализации requestAnimationFrame полностью сломан. Если вы не вызываете rAF из анимированного документа, вы можете вообще не оказаться перед следующей отрисовкой. - person Kaiido; 16.03.2021