Chrome плавная прокрутка и запрос анимации кадра? - PullRequest
0 голосов
/ 22 января 2020

Я создал автоскроллер с перетаскиванием, где пользователь перетаскивает элемент поверх скрытого div, который запускает прокрутку прокручиваемого div. Я использую scrollBy({top: <val>, behavior: 'smooth'} для плавной прокрутки и requestAnimationFrame для предотвращения слишком частого вызова функции. Это прекрасно работает в Firefox и должно поддерживаться в Chrome изначально согласно caniuse; однако он не работает должным образом в chrome. Событие запускается только один раз, когда пользователь покидает скрытое div. Нет ошибок в консоли. console.log() указывает, что вызывается функция, содержащая scrollBy(). Если я удаляю behavior: 'smooth', это работает, но, конечно, нет плавной прокрутки. тот же результат, если я удаляю опцию и устанавливаю css scroll-behavior: smooth на прокручиваемый div. Я в полной растерянности. MWE функции прокрутки (это в приложении Vue, поэтому любые this. хранятся в объекте данных.

scroll: function () {
  if ( this.autoScrollFn ) cancelAnimationFrame( this.autoScrollFn )
  // this.toScroll is a reference to the HTMLElement
  this.toScroll.scrollBy( {
    top: 100,
    behavior: 'smooth'
  }
  this.autoscrollFn = requestAnimationFrame( this.scroll )
}

1 Ответ

1 голос
/ 29 января 2020

Не уверен, что вы ожидали от своего requestAnimationFrame звонка, но вот что должно произойти:

  • scrollBy с его поведением значение smooth должно фактически начать прокрутку целевого элемента только в следующем кадре рисования, непосредственно перед выполнением обратного вызова кадров анимации ( шаг 7 здесь ).

  • Сразу после этого первого шага плавной прокрутки сработает обратный вызов кадра анимации ( шаг 11 ), отключив первую плавную прокрутку, начав новую (, как здесь определено ).

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

Это действительно будет двигаться в Firefox, пока не достигнет конца, потому что этот браузер имеет линейную плавную прокрутку и прокручивает с первого кадра.
Но Chrome имеет более сложное поведение, облегчающее вывод, что делает первую прокрутку итерации на 0px. Таким образом, в этом браузере вы фактически окажетесь в бесконечном l oop, поскольку на каждой итерации вы будете прокручивать на 0, затем отключите предыдущую прокрутку и снова попросите прокрутить на 0 и т. Д. c. et c.

const trigger = document.getElementById( 'trigger' );
const scroll_container = document.getElementById( 'scroll_container' );

let scrolled = 0;
trigger.onclick = (e) => startScroll();

function startScroll() {
  // in Chome this will actually scroll by some amount in two painting frames
  scroll_container.scrollBy( { top: 100, behavior: 'smooth' } );
  // this will make our previous smooth scroll to be aborted (in all supporting browsers)
  requestAnimationFrame( startScroll );
  
  scroll_content.textContent = ++scrolled;
};
#scroll_container {
  height: 50vh;
  overflow: auto;
}
#scroll_content {
  height: 5000vh;
  background-image: linear-gradient(to bottom, red, green);
  background-size: 100% 100px;
}
<button id="trigger">click to scroll</button>
<div id="scroll_container">
  <div id="scroll_content"></div>
</div>

Так что, если вы действительно хотите избежать многократного вызова этой функции прокрутки, ваш код будет нарушен не только в Chrome, но и также в Firefox (там не остановится прокрутка и после 100px).

В этом случае вам нужно скорее дождаться окончания гладкой прокрутки .
здесь уже есть вопрос о том, чтобы определить, когда закончится гладкое scrollIntoPage, но случай scrollBy немного отличается (проще).

Вот метод, который возвращает Promise, сообщающий вам, когда закончился плавный переход (разрешение при успешной прокрутке к месту назначения и отклонение при отмене другой прокруткой). Идея базового c такая же, как и для этого моего ответа :
Запустите requestAnimationFrame l oop, проверяя на каждом шаге прокрутки, достигли ли мы состояния c позиция. Как только мы оставили два кадра в одной и той же позиции, мы предполагаем, что достигли конца, тогда нам просто нужно проверить, достигли ли мы ожидаемой позиции или нет.

При этом вам просто нужно повысить флаг до тех пор, пока предыдущая плавная прокрутка не закончится, а когда закончите, опустите его вниз.

const trigger = document.getElementById( 'trigger' );
const scroll_container = document.getElementById( 'scroll_container' );

let scrolling = false; // a simple flag letting us know if we're already scrolling
trigger.onclick = (evt) => startScroll();

function startScroll() {
  if( scrolling ) { // we are still processing a previous scroll request
    console.log( 'blocked' );
    return;
  }
  scrolling = true;
  smoothScrollBy( scroll_container, { top: 100 } )
    .catch( (err) => {
      /*
        here you can handle when the smooth-scroll
        gets disabled by an other scrolling
      */
      console.error( 'failed to scroll to target' );
    } )
    // all done, lower the flag
    .then( () => scrolling = false );
};


/* 
 *
 * Promised based scrollBy( { behavior: 'smooth' } )
 * @param { Element } elem
 **  ::An Element on which we'll call scrollIntoView
 * @param { object } [options]
 **  ::An optional scrollToOptions dictionary
 * @return { Promise } (void)
 **  ::Resolves when the scrolling ends
 *
 */
function smoothScrollBy( elem, options ) {
  return new Promise( (resolve, reject) => {
    if( !( elem instanceof Element ) ) {
      throw new TypeError( 'Argument 1 must be an Element' );
    }
    let same = 0; // a counter
    // pass the user defined options along with our default
    const scrollOptions = Object.assign( {
        behavior: 'smooth',
        top: 0,
        left: 0
      }, options );

    // last known scroll positions
    let lastPos_top = elem.scrollTop;
    let lastPos_left = elem.scrollLeft;
    // expected final position
    const maxScroll_top = elem.scrollHeight - elem.clientHeight;
    const maxScroll_left = elem.scrollWidth - elem.clientWidth;
    const targetPos_top = Math.max( 0, Math.min(  maxScroll_top, Math.floor( lastPos_top + scrollOptions.top ) ) );
    const targetPos_left = Math.max( 0, Math.min( maxScroll_left, Math.floor( lastPos_left + scrollOptions.left ) ) );

    // let's begin
    elem.scrollBy( scrollOptions );
    requestAnimationFrame( check );
    
    // this function will be called every painting frame
    // for the duration of the smooth scroll operation
    function check() {
      // check our current position
      const newPos_top = elem.scrollTop;
      const newPos_left = elem.scrollLeft;
      // we add a 1px margin to be safe
      // (can happen with floating values + when reaching one end)
      const at_destination = Math.abs( newPos_top - targetPos_top) <= 1 &&
        Math.abs( newPos_left - targetPos_left ) <= 1;
      // same as previous
      if( newPos_top === lastPos_top &&
        newPos_left === lastPos_left ) {
        if( same ++ > 2 ) { // if it's more than two frames
          if( at_destination ) {
            return resolve();
          }
          return reject();
        }
      }
      else {
        same = 0; // reset our counter
        // remember our current position
        lastPos_top = newPos_top;
        lastPos_left = newPos_left;
      }
      // check again next painting frame
      requestAnimationFrame( check );
    }
  });
}
#scroll_container {
  height: 50vh;
  overflow: auto;
}
#scroll_content {
  height: 5000vh;
  background-image: linear-gradient(to bottom, red, green);
  background-size: 100% 100px;
}
.as-console-wrapper {
  max-height: calc( 50vh - 30px ) !important;
}
<button id="trigger">click to scroll (spam the click to test blocking feature)</button>
<div id="scroll_container">
  <div id="scroll_content"></div>
</div>
...