автор
Михаил
Ваваев




Технология
2ScrollBars

Масштабирование элемента position: fixed

В этой статье я изложу очередную проблему элемента position: fixed и ее решение. Она проявляется только на сенсорных экранах (планшетах и смартфонах) во время масштабирования страницы (это делается двумя пальцами: как правило, большим и указательным). Все элементы страницы, включая position: absolute, остаются привязаны к своим местам относительно содержимого, так что масштабирование выглядит вполне естественным. И только элемент position: fixed привязан не к содержимому, а к краям экрана. Поэтому во время масштабирования он постоянно остается на экране и плавает над содержимым. А учитывая то, что он тоже подвергается увеличению, то он начинает занимать на экране все больше места и мешает просмотру основного контента.


Свойство position: fixed обладает столькими недостатками, что в пору было бы от него отказаться. Но, к сожалению, мы не можем этого сделать в силу отсутствия альтернативы. Дело в том, что при помощи JavaScript нельзя так же эффективно зафиксировать элемент. Во время прокрутки он будет дергаться, и избавится от этого я так и не смог. Если у вас есть решение этой проблемы, то прошу изложить его в комментариях.

Это недоразумение, неверное поведение элемента position: fixed (а это несомненно баг) можно устранить при помощи JavaScript. Во время масштабирования надо принудительно задвинуть элемент position: fixed за экран. К сожалению это не получается сделать незаметно. Пока вы удерживаете пальцы на экране, не завершив масштабирования, наш элемент будет маячить перед глазами. Лишь после завершения масштабирования код JavaScript выполняется, и элемент прыгнет на свое место.

Якоря

Для того, чтобы возвращать элемент на свое место, надо знать координаты этого места. Для этого мы будем размещать на странице элемент-якорь, относительно которого будем позиционировать наши плавающие элементы.

В нашем шаблоне два плавающих элемента position: fixed: левая колонка и шапка. Шапка всегда следует за колонкой, поэтому отдельно мы ее позиционировать не будем. Но если бы шапки не было, то можно было бы обойтись позиционированием колонки только по горизонтали. Но шапка может мешать обзору. Совсем ее убирать нельзя, потому что пользователя может заинтересовать сама шапка (кликнуть на ней что-нибудь). Поэтому при масштабировании она должна быть доступна.

Позиционирование колонки по горизонтали мы реализуем очень просто. Мы поместим в верхний левый угол документа элемент-якорь <div id="left_anchor">. Вот его таблица стилей:

div#left_anchor {
	position: absolute;
	top: 0px;
	left: 0px;
}
Он будет прокручиваться вместе с документом. Поэтому наша задача сводится всего лишь к постоянной привязке левой колонки к координатам этого якоря. Значение, получаемое от функции getBoundingClientRect(), надо округлять, потому что некоторые браузеры (в частности интересующий нас Chrome) возвращают дробные значения.
function scroll_zoom()
{
	var left_anchor = document.getElementById('left_anchor').getBoundingClientRect();
	var left = document.getElementById('left');

	left.style.left = Math.round(left_anchor.left) + 'px';
}
Однако позиционирование колонки по вертикали не столь тривиально. Это связано с тем, что во время прокрутки документа (правой полосы прокрутки) положение левой колонки по вертикали все время меняется. Поэтому мною было предложено следующее решение: в режиме увеличенного масштаба поведение левой колонки меняется – она перестает быть плавающей, и начинает прокручиваться вместе с документом. Для этого потребовалось найти критерий, по которому можно было бы определить, масштаб страницы. И такой критерий мною был найден.

Как определить масштаб страницы

Масштаб страницы определяется из соотношения двух величин. Первая – это clientHeight. Это высота так называемой клиентской части окна браузера. Эта величина отражает число пикселей документа, которые позволяет отобразить окно браузера. Эта величина не изменяется в процессе масштабирования документа (т.е. она не зависит от документа), а меняется только когда изменяется размер окна. Для получения второй величины нам потребуется еще один элемент-якорь. Это будет элемент <div id="fixed_anchor"> со следующей таблицей стилей:
div#fixed_anchor {
	top: 0px;
	left: 0px;
	width: 1px;
	height: 100%;
	position: fixed;
}
В данный момент нас интересует то, что его высота равна 100%. Т.е. по высоте он растянут на весь экран, но поскольку он принадлежит области документа, то его фактическая высота будет зависеть от масштабирования. Чтобы найти его высоту надо воспользоваться функцией getBoundingClientRect(), которая возвращает координаты элемента относительно окна браузера. Так как верхняя граница всегда равна 0, то достаточно взять координату нижней границы и мы получим высоту элемента. Эта величина отражает число пикселей документа, которые отображены в окне браузера в данный момент.

Из соотношения этих двух величин можно определить масштаб документа. Надо заметить, что в браузерах настольных компьютеров масштаб всегда равен 1, даже если вы пользуетесь ползунком масштабирования в Opera. А вот на маленьких экранах смартфонов эти две величины изначально не равны. Из-за малого размера экрана браузер пытается втиснуть всю страницу в ширину экрана, т.е. происходит уменьшение масштаба – величина clientHeight будет меньше второго значения (не знаю, как его по-короче обозвать). При увеличении масштаба (то, что нас интересует) вторая величина начинает уменьшаться. Т.е. вы увеличиваете документ и фактически видите на экране все меньшее число пикселей, но зато крупно.
Итак, наш критерий масштабирования наступает, когда величина clientHeight становится больше второй величины (масштаб больше 1).
var fixed = document.getElementById('fixed_anchor').getBoundingClientRect();
if ( document.body.clientHeight > fixed.bottom ) {...}

Режим масштабирования

Как только наступает этот критерий, мы, во-первых, увеличиваем высоту левой колонки, присваивая ей значение величины clientHeight. А во-вторых, левая колонка перестает быть фиксированной. Через изменение CSS свойства position этого сделать нельзя (это ограничение политики безопасности браузера). Поэтому мы достигнем это при помощи JavaScript. Когда документ не масштабирован, мы постоянно запоминаем последнюю координату якоря <div id="left_anchor"> в переменную prev_scroll. Поскольку якорь находится в самом верху документа и прокручивается вместе с содержимым, то эта величина отражает число прокрученных пикселей документа с отрицательным знаком. Когда же наступает критерий масштабирования мы вычисляем разницу между нынешним и предыдущим положением якоря и присваиваем это значение CSS свойству top левой колонки. Вычитание нужно потому, что свойство top определяет положение относительно экрана, а переменная prev_scroll хранит значение относительно начала документа. Значение prev_scroll в режиме масштабирования не обновляется. Таким образом, левая колонка относительно документа остается на одном месте.
// инициализируем переменную
var prev_scroll = 0;
Переменную prev_scroll надо инициализировать вне функции scroll_zoom(), чтобы она сохранялась между вызовами функции.
var fixed = document.getElementById('fixed_anchor').getBoundingClientRect();

if ( document.body.clientHeight > fixed.bottom ) {
	left.style.top = (Math.round( left_anchor.top ) - prev_scroll) + 'px';
	left.style.height = document.body.clientHeight + 'px';
}
else {
	left.style.top = '0px';
	left.style.height = '100%';
	prev_scroll = Math.round( left_anchor.top );
}
Однако это не все. В режиме масштабирования может возникнуть ситуация, когда окно просмотра станет выше или ниже элемента левой колонки. В этом случае левую колонку надо переместить вслед за окном просмотра. Для этого перед изменением свойства top левой колонки надо сделать две проверки – для верхней и нижней границы. Верхнюю границу проверить легко – надо проверить, не будет ли будущее свойство top больше 0. Если да, то изменяем переменную prev_scroll, таким образом, что результат вычитания будет равен 0. А вот для проверки нижней границы надо немного пораскинуть мозгами. При этом надо учитывать, что одни величины имеют положительный знак, а другие отрицательный. При изменении свойства top, надо так же помнить, что оно может быть только отрицательным или равным 0.
if ( Math.round( left_anchor.top ) - prev_scroll > 0 )
	prev_scroll = Math.round( left_anchor.top );
else if ( document.body.clientHeight - prev_scroll < fixed.bottom - Math.round(left_anchor.top))
	prev_scroll = Math.round(left_anchor.top) - fixed.bottom + document.body.clientHeight;

Результаты

Ну вот и все. Полный код функции scroll_zoom() выглядит следующим образом:
var prev_scroll = 0;
/*****************************************************************************
 * Функция scroll_zoom()
 *
 * Функция корректирует поведение элемента position: fixed во время
 * масштабирования на сенсорном экране, возвращая его на нужное место.
 *
 * Параметры:
 *		без параметров
 *
 * Возвращаемые значения:
 *		ничего не возвращает
 ****************************************************************************/
function scroll_zoom()
{
	var left_anchor = document.getElementById('left_anchor').getBoundingClientRect();
	var fixed = document.getElementById('fixed_anchor').getBoundingClientRect();
	var left = document.getElementById('left');

	left.style.left = Math.round(left_anchor.left) + 'px';

	if ( document.body.clientHeight > fixed.bottom - fixed.top ) {
		if ( Math.round( left_anchor.top ) - prev_scroll > 0 )
			prev_scroll = Math.round( left_anchor.top );
		else if ( document.body.clientHeight - prev_scroll <
				  fixed.bottom - Math.round( left_anchor.top ) )
			prev_scroll = Math.round( left_anchor.top ) -
				fixed.bottom + document.body.clientHeight;

		left.style.top = (Math.round( left_anchor.top ) - prev_scroll) + 'px';
		left.style.height = document.body.clientHeight + 'px';
	}
	else {
		left.style.top = '0px';
		left.style.height = '100%';
		prev_scroll = Math.round( left_anchor.top );
	}
}
Эту функцию необходимо вызывать из функции-обработчика правого скроллинга rightScroll(). После необходимо вызвать функцию позиционирования шапки scroll_head(), т.к. шапка привязана к якорю левой колонки.

Однако в браузере IE 6, где position: fixed не работает, эта функция будет работать некорректно. Но она там и не нужна. Поэтому для IE 6 мы эту функцию не вызываем.
/*****************************************************************************
 * Функция rightScroll()
 *
 * Функция обрабатывает событие правой полосы прокрутки.
 *
 * Параметры:
 *		без параметров
 *
 * Возвращаемые значения:
 *		ничего не возвращает
 ****************************************************************************/
function rightScroll()
{
	if ( !detectIE6() ) scroll_zoom(); scroll_head();
}
Функция detectIE6() определения браузера IE6 выглядит следующим образом:
/*****************************************************************************
 * Функция detectIE6()
 *
 * Функция определяет, является ли браузер IE6.
 *
 * Параметры:
 *		без параметров
 *
 * Возвращаемые значения:
 *		true или false
 ****************************************************************************/
function detectIE6()
{
	if ( navigator.appName == "Microsoft Internet Explorer" )
	{
		var b_version = navigator.appVersion;
		var re = /MSIEs+(d.db)/;
		var res = b_version.match( re );
		if ( res[1] <= 6 ) return true;
	}

	return false;
}
Функцию scroll_head() так же надо назначить обработчиком события окна браузера onresize. При изменении размеров окна, положение шапки тоже надо корректировать. Следующую строчку кода, которая назначает этот обработчик, можно поместить в любое место Javascript кода вне функций.
window.onresize = scroll_head;
Обработчик правого скроллинга (прокрутка содержимого документа) необходимо прописывать сразу в оба тега: <html onscroll="rightScroll()"> и <body onscroll="rightScroll()">. Это требование кроссбраузерности. Какие-то браузеры отсчитывают документ уже начиная с элемента <html>, а другие только с элемента <body>.

Перепишем HTML-код нашего шаблона с учетом последних изменений. Посмотреть его можно здесь.

Все комментарии

нет комментариев

Ваш комментарий

Имя:
вставить:
Введите код с картинки