Обзор технологий скроллинга

Общие проблемы при реализации любых сценариев со скролл-эффектами.

Во-первых, при написании скролл-эффектов нужно учитывать большое количество факторов и величин:

  • Размер всего документа.
  • Размеры и позиции элементов, участвующих в сценарии, а также в некоторых случаях и их контейнеров.
  • Размер и текущее положение видимой части документа (viewport) при скролле.
  • Направление скролла.
  • Адаптация при изменении размеров окна с отзывчивым (responsive) дизайном

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

В-третьих, на мобильных девайсах все работает плохо и с тормозами. Javascript изначально работает медленнее. В добавок к этому, мобильные браузеры блокируют выполнение javascript во время скролла.

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

Custom events

iScroll also emits some useful custom events you can hook to.

To register them you use the method.

myScroll = new IScroll('#wrapper');
myScroll.on('scrollEnd', doSomething);

The above code executes the function every time the content stops scrolling.

The available types are:

  • beforeScrollStart, executed as soon as user touches the screen but before the scrolling has initiated.
  • scrollCancel, scroll initiated but didn’t happen.
  • scrollStart, the scroll started.
  • scroll, the content is scrolling. Available only in edition. See .
  • scrollEnd, content stopped scrolling.
  • flick, user flicked left/right.
  • zoomStart, user started zooming.
  • zoomEnd, zoom ended.

Scrolling programmatically

You silly! Of course you can scroll programmaticaly!

scrollTo(x, y, time, easing)

Say your iScroll instance resides into the variable. You can easily scroll to any position with the following syntax:

myScroll.scrollTo(, -100);

That would scroll down by 100 pixels. Remember: 0 is always the top left corner. To scroll you have to pass negative numbers.

and are optional. They regulates the duration (in ms) and the easing function of the animation respectively.

The easing functions are available in the object. For example to apply a 1 second elastic easing you’d do:

myScroll.scrollTo(, -100, 1000, IScroll.utils.ease.elastic);

The available options are: , , , , .

scrollBy(x, y, time, easing)

Same as above but X and Y are relative to the current position.

myScroll.scrollBy(, -10);

Would scroll 10 pixels down. If you are at -100, you’ll end up at -110.

scrollToElement(el, time, offsetX, offsetY, easing)

You’re gonna like this. Sit tight.

The only mandatory parameter is . Pass an element or a selector and iScroll will try to scroll to the top/left of that element.

is optional and sets the animation duration.

works the same way as per the scrollTo method.

Advanced options

For the hardcore developer.

options.bindToWrapper

The event is normally bound to the document and not the scroll container. When you move the cursor/finger out of the wrapper the scrolling keeps going. This is usually what you want, but you can also bind the move event to wrapper itself. Doing so as soon as the pointer leaves the container the scroll stops.

Default:

options.bounceEasing

is a bit smarter than that. You can also feed a custom easing function, like so:

bounceEasing: {
    style: 'cubic-bezier(0,0,1,1)',
    fn: function (k) { return k; }
}

The above would perform a linear easing. The option is used every time the animation is executed with CSS transitions, is used with . If the easing function is too complex and can’t be represented by a cubic bezier just pass (empty string) as .

Note that and can’t be performed by CSS transitions.

Default:

Duration in millisecond of the bounce animation.

Default:

options.deceleration

This value can be altered to change the momentum animation duration/speed. Higher numbers make the animation shorter. Sensible results can be experienced starting with a value of , bigger than that basically doesn’t make any momentum at all.

Default:

Set the speed of the mouse wheel.

Default:

options.preventDefaultException

These are all the exceptions when would be fired anyway despite the preventDefault option value.

This is a pretty powerful option, if you don’t want to on all elements with formfield class name for example, you could pass the following:

preventDefaultException: { className: /(^|\s)formfield(\s|$)/ }

Default: .

options.resizePolling

When you resize the window iScroll has to recalculate elements position and dimension. This might be a pretty daunting task for the poor little fella. To give it some rest the polling is set to 60 milliseconds.

By reducing this value you get better visual effect but the script becomes more aggressive on the CPU. The default value seems a good compromise.

Default:

Contributing and CLA

As an end user you have to do nothing of course. Actually the CLA ensures that nobody will even come after you asking for your first born for using the iScroll.

Please note that pull requests may take some time to be accepted. Testing iScroll is one of the most time consuming tasks of the project. iScroll works from desktop to smartphone, from tablets to smart TVs. I do not have physical access to all the testing devices, so before I can push a change I have to make sure that the new code is working everywhere.

Critical bugs are usually applied very quickly, but enhancements and coding style changes have to pass a longer review phase. Remember that this is still a side project for me.

Прокрутка (скроллинг) документа

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

Далее, в
современных браузерах прокручивать документ можно через свойства объекта html:

  • document.documentElement.scrollTop

  • document.documentElement.scrollLeft

В некоторых
старых браузерах этот вариант будет работать через body:

  • document.body.scrollTop

  • document.body.scrollLeft

Поэтому, для
написания надежного кода, лучше использовать специальные методы скроллинга
объекта window:

  • window.scrollBy(offX, offY) – прокручивает
    страницу относительно её текущего положения на смещения offX, offY пикселей;

  • window.scrollTo(pageX, pageY) – прокручивает
    страницу до указанных координат pageX и pageY.

Например, вот
такой скрипт:

setInterval(function() {
    window.scrollBy(, 5); 
}, 100);

будет
прокручивать страницу вниз на 5 пикселей каждые 100 мс. А если прописать вот
так:

setInterval(function() {
    window.scrollTo(, 5);
}, 100);

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

window.scrollTo(, );

будет возвращать
нас просто к началу документа.

Для полноты
картины давайте рассмотрим ещё один метод scrollIntoView, который имеет
следующий синтаксис:

elem.scrollIntoView(top = true);

Данный метод
существует у всех объектов-тегов DOM-дерева и прокручивает документ так,
чтобы elem оказался вверху
окна браузера (если значение top=true), или внизу,
при значении top=false. Пусть, в
начале документа имеется заголовок:

<h1 id="header_1">Список свойств метрики</h1>

и мы хотим
прокрутить документ, чтобы он стал виден вверху страницы:

setTimeout(function() {
    header_1.scrollIntoView();
});

Обратите
внимание, что мы вызвали этот метод через планировщик отложенного вызова –
функцию setTimeout с нулевой
задержкой. Если мы просто в скрипте напишем:

header_1.scrollIntoView();

то это работать
не будет. Как мы отмечали в самом начале, все методы скроллинга работают после
полной загрузки документа и построения DOM-дерева. И,
вызывая scrollIntoView через setTimeout, мы как раз и выполняем это
условие.

Правила, их границы и области действия.

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

Чтобы было проще понять о чем идет речь, приведу абстрактный пример:

  1. Нужно плавно показать и плавно скрыть некоторый элемент, когда при скролле он будет входить в область видимости и выходить из нее. Элемент должен начать появляться после того, как его верхняя кромка будет на 100px выше нижней границы видимой области окна и полностью появится, когда его нижняя кромка будет на 100px выше нижней границы видимой области окна. Та же логика с исчезновением, только в симметрично обратном порядке.
  2. Элемент нужно повернуть на 180° во время скролла, пока он будет находится в зоне ±30% от центра видимой зоны.

Давайте договоримся, что мы будем называть видимую область документа словом “viewport”. К сожалению, я не могу найти короткий русский аналог этого слова 🙂

В итоге, здесь мы можем выделить 3 области действия правил c 6-ю границами. Давайте опишем их:

  1. Точка, находящаяся на 100px ниже верхней границы элемента, совпадает с нижней границей viewport (элемент начинает появляться)
  2. Точка, находящаяся на 100px выше верхней границы элемента, совпадает с нижней границей viewport (элемент заканчивает появляться)
  3. Точка, находящаяся на 30% ниже центра viewport, совпадает с центром элемента (элемент начинает поворот)
  4. Точка, находящаяся на 30% выше центра viewport, совпадает с центром элемента (элемент заканчивает поворот)
  5. Точка, находящаяся на 100px ниже верхней границы элемента, совпадает с верхней границей viewport (элемент начинает исчезать)
  6. Точка, находящаяся на 100px выше верхней границы элемента, совпадает с верхней границей viewport (элемент заканчивает исчезать)

А теперь подумайте, с чего Вы начали бы описывать всю эту логику? Даже в таком простом сценарии с одним элементом в вычислениях участвуют размер документа, размер viewport, положение viewport, размер элемента, положение элемента, положение скролла… черт возьми, как же не запутаться?

Запретить прокрутку

Иногда нам нужно сделать документ «непрокручиваемым». Например, при показе большого диалогового окна над документом – чтобы посетитель мог прокручивать это окно, но не документ.

Чтобы запретить прокрутку страницы, достаточно установить .

Попробуйте сами:

Первая кнопка останавливает прокрутку, вторая возобновляет её.

Аналогичным образом мы можем «заморозить» прокрутку для других элементов, а не только для .

Недостатком этого способа является то, что сама полоса прокрутки исчезает. Если она занимала некоторую ширину, то теперь эта ширина освободится, и содержимое страницы расширится, текст «прыгнет», заняв освободившееся место.

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

Scrollbars

The scrollbars are more than just what the name suggests. In fact internally they are referenced as indicators.

An indicator listens to the scroller position and normally it just shows its position in relation to whole, but what it can do is so much more.

Let’s start with the basis.

options.scrollbars

As we mentioned in the there’s only one thing that you got to do to activate the scrollbars in all their splendor, and that one thing is:

var myScroll = new IScroll('#wrapper', {
    scrollbars: true
});

Of course the default behavior can be personalized.

When not in use the scrollbar fades away. Leave this to to spare resources.

Default:

The scrollbar becomes draggable and user can interact with it.

Default:

options.resizeScrollbars

The scrollbar size changes based on the proportion between the wrapper and the scroller width/height. Setting this to makes the scrollbar a fixed size. This might be useful in case of custom styled scrollbars ().

Default:

options.shrinkScrollbars

When scrolling outside of the boundaries the scrollbar is shrunk by a small amount.

Valid values are: and .

just moves the indicator outside of its container, the impression is that the scrollbar shrinks but it is simply moving out of the screen. If you can live with the visual effect this option immensely improves overall performance.

turns off hence all animations are served with . The indicator is actually varied in size and the end result is nicer to the eye.

Default:

Note that resizing can’t be performed by the GPU, so is all on the CPU.

If your application runs on multiple devices my suggestion would be to switch this option to , or based on the platform responsiveness (eg: on older mobile devices you could set this to and on desktop browser to ).

So you don’t like the default scrollbar styling and you think you could do better. Help yourself! iScroll makes dressing the scrollbar a snap. First of all set the option to :

var myScroll = new IScroll('#wrapper', {
    scrollbars: 'custom'
});

Then use the following CSS classes to style the little bastards.

  • .iScrollHorizontalScrollbar, this is applied to the horizontal container. The element that actually hosts the scrollbar indicator.
  • .iScrollVerticalScrollbar, same as above but for the vertical container.
  • .iScrollIndicator, the actual scrollbar indicator.
  • .iScrollBothScrollbars, this is added to the container elements when both scrollbars are shown. Normally just one (horizontal or vertical) is visible.

If you set you could make the scrollbar of a fixed size, otherwise it would be resized based on the scroller length.

Please keep reading to the following section for a revelation that will shake your world.

Basic features

options.bounce

When the scroller meets the boundary it performs a small bounce animation. Disabling bounce may help reach smoother results on old or slow devices.

Default:

options.click

To override the native scrolling iScroll has to inhibit some default browser behaviors, such as mouse clicks. If you want your application to respond to the click event you have to explicitly set this option to . Please note that it is suggested to use the custom event instead (see below).

Default:

options.disableMouseoptions.disablePointeroptions.disableTouch

By default iScroll listens to all pointer events and reacts to the first one that occurs. It may seem a waste of resources but feature detection has proven quite unreliable and this listen-to-all approach is our safest bet for wide browser/device compatibility.

If you have an internal mechanism for device detection or you know in advance where your script will run on, you may want to disable all event sets you don’t need (mouse, pointer or touch events).

For example to disable mouse and pointer events:

var myScroll = new IScroll('#wrapper', {
    disableMouse: true,
    disablePointer: true
});

Default:

options.eventPassthrough

Sometimes you want to preserve native vertical scroll but being able to add an horizontal iScroll (maybe a carousel). Set this to and the iScroll area will react to horizontal swipes only. Vertical swipes will naturally scroll the whole page.

options.freeScroll

This is useful mainly on 2D scrollers (when you need to scroll both horizontally and vertically). Normally when you start scrolling in one direction the other is locked.

Default:

options.keyBindings

Set this to to activate keyboard (and remote controls) interaction. See the section below for more information.

Default:

options.invertWheelDirection

Meaningful when mouse wheel support is activated, in which case it just inverts the scrolling direction. (ie. going down scrolls up and vice-versa).

Default:

options.momentum

You can turn on/off the momentum animation performed when the user quickly flicks on screen. Turning this off greatly enhances performance.

Default:

Listen to the mouse wheel event.

Default:

options.preventDefault

Whether or not to when events are fired. This should be left unless you really know what you are doing.

See in the for more control over the preventDefault behavior.

Default:

Wheter or not to display the default scrollbars. See more in the section.

Default: .

options.scrollXoptions.scrollY

See also the freeScroll option.

Default: ,

Note that has the same effect as . Setting one direction to helps to spare some checks and thus CPU cycles.

options.startXoptions.startY

By default iScroll starts at (top left) position, you can instruct the scroller to kickoff at a different location.

Default:

options.tap

Set this to to let iScroll emit a custom event when the scroll area is clicked/tapped but not scrolled.

This is the suggested way to handle user interaction with clickable elements. To listen to the tap event you would add an event listener as you would do for a standard event. Example:

element.addEventListener('tap', doSomething, false); \\ Native
$('#element').on('tap', doSomething); \\ jQuery

You can also customize the event name by passing a string. Eg:

tap: 'myCustomTapEvent'

In this case you’d listen to .

Default:

Getting started

So you want to be an iScroll master. Cool, because that is what I’ll make you into.

The best way to learn the iScroll is by looking at the demos. In the archive you’ll find a folder stuffed with examples. Most of the script features are outlined there.

is a class that needs to be initiated for each scrolling area. There’s no limit to the number of iScrolls you can have in each page if not that imposed by the device CPU/Memory.

Try to keep the DOM as simple as possible. iScroll uses the hardware compositing layer but there’s a limit to the elements the hardware can handle.

The optimal HTML structure is:

<div id="wrapper">
    <ul>
        <li>...</li>
        <li>...</li>
        ...
    </ul>
</div>

iScroll must be applied to the wrapper of the scrolling area. In the above example the element will be scrolled. Only the first child of the container element is scrolled, additional children are simply ignored.

, , and alpha channels are all properties that don’t go very well together with hardware acceleration. Scrolling might look good with few elements but as soon as your DOM becomes more complex you’ll start experiencing lag and jerkiness.

Sometimes a background image to simulate the shadow performs better than . The bottom line is: experiment with CSS properties, you’ll be surprised by the difference in performance a small CSS change can do.

The minimal call to initiate the script is as follow:

<script type="text/javascript">
var myScroll = new IScroll('#wrapper');
</script>

The first parameter can be a string representing the DOM selector of the scroll container element OR a reference to the element itself. The following is a valid syntax too:

var wrapper = document.getElementById('wrapper');
var myScroll = new IScroll(wrapper);

So basically either you pass the element directly or a string that will be given to . Consequently to select a wrapper by its class name instead of the ID, you’d do:

var myScroll = new IScroll('.wrapper');

Note that iScroll uses not , so only the first occurrence of the selector is used. If you need to apply iScroll to multiple objects you’ll have to build your own cycle.

You don’t strictly need to assign the instance to a variable (), but it is handy to keep a reference to the iScroll.

For example you could later check the or when you don’t need the iScroll anymore.

Zoom

To use the pinch/zoom functionality you better use the script.

Set this to to activate zoom.

Default:

Maximum zoom level.

Default:

Minimum zoom level.

Default:

Starting zoom level.

Default:

options.wheelAction

Wheel action can be set to to have the wheel regulate the zoom level instead of scrolling position.

Default: (ie: the mouse wheel scrolls)

To sum up, a nice zoom config would be:

myScroll = new IScroll('#wrapper', {
    zoom: true,
    mouseWheel: true,
    wheelAction: 'zoom'
});

The zoom is performed with CSS transform. iScroll can zoom only on browsers that support that.

Some browsers (notably webkit based ones) take a snapshot of the zooming area as soon as they are placed on the hardware compositing layer (say as soon as you apply a transform to them). This snapshot is used as a texture for the zooming area and it can hardly be updated. This means that your texture will be based on elements at scale 1 and zooming in will result in blurred, low definition text and images.

A simple solution is to load content at double (or triple) its actual resolution and scale it down inside a div. This should be enough to grant you a better result. I hope to be able to post more demos soon

zoom(scale, x, y, time)

Juicy method that lets you zoom programmatically.

is the zoom factor.

and the focus point, aka the center of the zoom. If not specified, the center of the screen will be used.

is the duration of the animation in milliseconds (optional).

onScroll event

The event is available on iScroll probe edition only (). The probe behavior can be altered through the option.

options.probeType

This regulates the probe aggressiveness or the frequency at which the event is fired. Valid values are: , , . The higher the number the more aggressive the probe. The more aggressive the probe the higher the impact on the CPU.

has no impact on performance. The event is fired only when the scroller is not busy doing its stuff.

always executes the event except during momentum and bounce. This resembles the native event.

emits the event with a to-the-pixel precision. Note that the scrolling is forced to (ie: ).

Useful scroller info

iScroll stores many useful information that you can use to augment your application.

You will probably find useful:

  • myScroll.x/y, current position
  • myScroll.directionX/Y, last direction (-1 down/right, 0 still, 1 up/left)
  • myScroll.currentPage, current snap point info

These pieces of information may be useful when dealing with custom events. Eg:

myScroll = new IScroll('#wrapper');
myScroll.on('scrollEnd', function () {
    if ( this.x < -1000 ) {
        // do something
    }
});

The above executes some code if the position is lower than -1000px when the scroller stops. Note that I used instead of , you can use both of course, but iScroll passes itself as context when firing custom event functions.

Не стоит брать width/height из CSS

Мы рассмотрели метрики, которые есть у DOM-элементов, и которые можно использовать для получения различных высот, ширин и прочих расстояний.

Но как мы знаем из главы Стили и классы, CSS-высоту и ширину можно извлечь, используя .

Так почему бы не получать, к примеру, ширину элемента при помощи , вот так?

Почему мы должны использовать свойства-метрики вместо этого? На то есть две причины:

  1. Во-первых, CSS-свойства зависят от другого свойства – , которое определяет, «что такое», собственно, эти CSS-ширина и высота. Получается, что изменение , к примеру, для более удобной вёрстки, сломает такой JavaScript.

  2. Во-вторых, в CSS свойства могут быть равны , например, для инлайнового элемента:

    Конечно, с точки зрения CSS – совершенно нормально, но нам-то в JavaScript нужен конкретный размер в , который мы могли бы использовать для вычислений. Получается, что в данном случае ширина из CSS вообще бесполезна.

Есть и ещё одна причина: полоса прокрутки. Бывает, без полосы прокрутки код работает прекрасно, но стоит ей появиться, как начинают проявляться баги. Так происходит потому, что полоса прокрутки «отъедает» место от области внутреннего содержимого в некоторых браузерах. Таким образом, реальная ширина содержимого меньше CSS-ширины. Как раз это и учитывают свойства .

…Но с ситуация иная. Некоторые браузеры (например, Chrome) возвращают реальную внутреннюю ширину с вычетом ширины полосы прокрутки, а некоторые (например, Firefox) – именно CSS-свойство (игнорируя полосу прокрутки). Эти кроссбраузерные отличия – ещё один повод не использовать , а использовать свойства-метрики.

Если ваш браузер показывает полосу прокрутки (например, под Windows почти все браузеры так делают), то вы можете протестировать это сами, нажав на кнопку в ифрейме ниже.

У элемента с текстом в стилях указано CSS-свойство .

На ОС Windows браузеры Firefox, Chrome и Edge резервируют место для полосы прокрутки. Но Firefox отображает , в то время как Chrome и Edge – меньше. Это из-за того, что Firefox возвращает именно CSS-ширину, а остальные браузеры – «реальную» ширину за вычетом прокрутки.

Обратите внимание: описанные различия касаются только чтения свойства из JavaScript, визуальное отображение корректно в обоих случаях