14. Обработка событий
Интерфейс Event
Иерархия интерфейса Event. |
Интерфейс Event имеет подинтерфейсы, которые подключаются к событийному объекту в соответствии с типом произошедшего события. Иерархия интерфейса Event показана на рисунке. Свойства и методы этих интерфейсов приводятся на отдельной странице Event. |
Построим несколько примеров, иллюстрирующих работу с событиями на практике (см. также отдельную страницу примеры).
Пример 3. Картинки, которые возвращаются
Создадим скрипт, который, после подключения к странице, позволил бы все её картинки перетаскивать мышью. Причем, по окончанию перетаскивания, картинка должна самостоятельно возвращаться на прежнее место, и не прыжком, а «мелкой рысью». Сделаем такой скрипт, который наделял бы таким свойством любую страницу, к которой подключается, без каких либо дополнительных разметок этой страницы (полное отделение содержания от поведения).
Сначала построим решение в рамках модели DOM W3C. То есть построенное приложение не будет работать в IE. А в следующем примере (под номером 4) построим универсальное приложение, пригодное для работы во всех браузерах.
praxis/14/examples/03/index.htm (не работает в IE)
Первое, что сделаем, назначим обработчик события mousedown для всех картинок страницы. Для простоты сделаем это в рамках модели нулевого уровня. Кроме того, зададим для всех картинок относительное позиционирование:
// Назначим обработчики события mousedown на все картинки страницы
// и зададим для них относительное позиционирование
for(var i=0; i<document.images.length; i++)
{
// Обработка нажатия кнопки мыши
document.images[i].onmousedown = downHandler;
// Назначим относительное позиционирование
document.images[i].style.position = "relative";
}
В этом коде downHandler — ссылка на функцию, которая и будет обрабатывать событие mousedown.
Относительное позиционирование работает так: если не менять стилевые свойства left и top (они нули по умолчанию), картинка остается на прежнем месте в потоке, а если менять (при перетаскивании), смещается на заданные значения.
Относительное позиционирование удобно для реализации возврата картинки на место после перетаскивания — устремим в анимационном цикле значения свойств left и top к нулю, и картинка вернется в исходное положение.
Теперь надо написать обработчик downHandler — он будет вызываться, когда на картинке будет нажата кнопка мыши. В этом обработчике зарегистрируем два новых обработчика:
- moveHandler — будет реагировать на событие mousemove, возникающее при перетаскивании;
- upHandler — будет вызван при отпускании кнопки мыши (конец перетаскивания).
Но прежде объявим несколько общих переменных, которые пригодятся нашему коду:
// Общие переменные
var xStartMouse, yStartMouse; // Координаты мыши во время нажатия на кнопку
var element; // Элемент на котором выполнено нажатие на кнопку
var isСomeBack = false; // Картинка двигается на место?
Переменную isСomeBack будем использовать как флаг, отменяющий обработку события mousedown на картинке, которая в анимационном цикле возвращается «домой» — пусть себе спокойно закончит движение, и не реагирует на попытки пользователя «поймать её на лету» (или на попытки щелкнуть на новой картинке в то время, как старая на пути к дому).
Ну, а теперь напишем обработчик downHandler.
// Обработать нажатие кнопки мыши
function downHandler(event)
{
if(!isСomeBack) // Реагировать только на неподвижную картинку
{ // (игнорировать щелчок по двигающейся картинке)
// Работа началась, щелчки не обрабатывать
isСomeBack = true;
// Запомним элемент, на котором выполнено нажатие на кнопку
element = this;
// Сохраним координаты мыши во время нажатия на кнопку
xStartMouse = event.clientX;
yStartMouse = event.clientY;
// Поднимем картинку на слой выше
this.style.zIndex = 1;
// Зарегистрируем обработчики событий mousemove и mouseup,
// которые последуют за событием mousedown. Зарегистрируем эти
// события на весь документ, так как перетаскиваемый объект может
// не поспевать за указателем мыши, и события возникнут вне его.
// В методе addEventListener укажем true, что будет означать
// обработку события на фазе захвата.
document.addEventListener("mousemove", moveHandler, true);
document.addEventListener("mouseup", upHandler, true);
// Событие mousedown обработано, прервём
// его дальнейшее распространение
event.stopPropagation();
// На всякий случай запретим действие по умолчанию
event.preventDefault();
}
}
Осталось написать два обработчика moveHandler (он будет работать во время перетаскивания) и upHandler — он сработает на завершающем этапе перетаскивания — отпускании кнопки мыши.
Обработчик moveHandler очень прост. В нём стилевым свойства left и top перетаскиваемого элемента присваиваются приращения координат указателя мыши по отношению к началу перетаскивания (элемент должен следовать за указателем):
// Обработать перемещение мыши
function moveHandler(event)
{
// Переместить элемент в текущие координаты указателя мыши
element.style.left = event.clientX-xStartMouse + "px";
element.style.top = event.clientY-yStartMouse + "px";
// Прервать дальнейшее распространение события
event.stopPropagation();
}
Наконец, в последнем обработчике upHandler нужно удалить отслуживший своё обработчик события mousemove, а заодно и сам текущий обработчик (что не помешает ему нормально закончить свою работу). После этого можно запускать анимацию, которая вернет картинку на место.
// Обработать заключительное событие -- отпускание кнопки мыши
function upHandler(event)
{
// Удалить обработчики событий mouseup и mousemove
document.removeEventListener("mousemove", moveHandler, true);
document.removeEventListener("mouseup", upHandler, true);
// Прервать дальнейшее распространение события
event.stopPropagation();
// Запустить анимацию, которая возвратит элемент на место
toHome();
}
Пример 4. Картинки, которые возвращаются
Создадим скрипт, выполняющий задачу примера 3, но работающий во всех браузерах, в том числе и в браузере IE.
praxis/14/examples/04/index.htm
Алгоритм работы скрипта не изменился, но появились многочисленные проверки, которые направляют выполнение по ветвям с той функциональностью, которую поддерживает браузер.
Например, для регистрации обработчиков событий mousemove и mouseup приходится записывать такой код:
if (document.addEventListener) // Если работает модель DOM W3C
{
// В методе addEventListener укажем true, что будет означать
// обработку события на фазе захвата.
document.addEventListener("mousemove", moveHandler, true);
document.addEventListener("mouseup", upHandler, true);
}
else if (document.attachEvent) // Если работает модель IE5+
{
// В модели IE5+ перехват события производится вызовом
// метода setCapture элемента, выполняющего перехват.
// После вызова этого метода все события мыши будут направляться
// элементу до отмена перехвата (метод releaseCapture)
element.setCapture();
document.attachEvent("onmousemove", moveHandler);
document.attachEvent("onmouseup", upHandler);
// Интерпретировать событие потери перехвата как событие mouseup.
// Потеря перехвата может случится в результате потери браузером
// фокуса ввода, появления модального окна (например, alert),
// отображения системного меню и в других подобных случаях.
document.attachEvent("onlosecapture", upHandler);
}
else // Модель событий IE4
{
// В модели IE4 нельзя использовать attachEvent и setCapture,
// поэтому вставляем обработчики и надеемся на то, что
// требуемые события мыши всплывут к объекту document.
// Предварительно сохраним значения событийных свойств.
oldMoveHandler = document.onmousemove;
oldUpHandler = document.onmouseup;
document.onmousemove = moveHandler;
document.onmouseup = upHandler;
}
Заметьте, скрипт не проверяет марку браузера (их сегодня много, а учитывая версии — очень много), скрипт проверяет функциональность браузера. Если браузер поддерживает метод document.addEventListener, значит, он работает в рамках модели DOM W3C, если браузер поддерживает метод document.attachEvent, значит, действуем в рамках модели IE5+, в противном случае перед нами совсем старый браузер (скорее всего IE4), и в этой ситуации мы делаем всё, что можем, не надеясь на 100% успех.
Пример 5. Почти Арканоид
Построим простейшую игру с клавиатурным интерфейсом — упрощенный до предела вариант известного Арканоида.
Игрок должен контролировать платформу-ракетку, которую можно передвигать горизонтально стрелками клавиатуры от одной стенки до другой, подставляя её под шарик. Шарик запускается в игру нажатием на клавишу пробела.
![]() Вид одного из Арканоидов |
В игровой среде настоящего Арканоида присутствуют еще блоки-кирпичи. Удар шарика по кирпичу приводит к разрушению кирпича. После того как все кирпичи на данном уровне уничтожены, выполняется переход на следующий уровень, с новым набором кирпичей. |
Кирпичи в нашем варианте будут отсутствовать, но при желании вы можете продолжить разработку и реализовать настоящий Арканоид.
В этом примере реализовано движение ракетки по игровому полю, а шарик — неподвижен.
praxis/14/examples/05/index.htm
HTML-код предельно прост:
<BODY>
<H1>Почти Арканоид</H1>
<DIV id="field">
<IMG id="ball" src="pic/disc.gif" width=11 height=11 alt="" title="">
<DIV id="racket"><IMG src="pic/empty.gif"
width=1 height=1 alt="" title=""></DIV>
</DIV>
<P>
Старт — пробел<BR>
Ракетка — стрелки
</P>
</BODY>
В игровое поле field погружаем шарик ball и ракетку racket.
Внутри ракетки записан элемент IMG. Эта картинка содержит одну GIF-точку прозрачного цвета. Этот элемент кажется лишним (так оно и есть), но его пришлось вводить, чтобы ракетка была правильной высоты в IE. Дело в том, что пустой блок <DIV id="racket"></DIV> браузер IE считает наполненным текстовой строкой и увеличивает высоту блока до высоты строки текста. Получается больше, чем надо (можете проверить, убрав этот IMG), кроме того, высота блока становится зависимой от размера шрифта.
В стилевом файле запишем следующие определения:
/* Игровое поле */
#field
{
position:relative;
border:1px solid black;
}
/* Шарик */
#ball
{
position:absolute;
}
/* Ракетка */
#racket
{
position:absolute;
bottom:0;
border:1px solid black;
background:#ff7f00;
}
Для игрового поля указано относительное позиционирование, чтобы можно было абсолютно позиционировать шарик и ракетку относительно его верхнего угла.
Размер поля field будет задан скриптом, также как и размер и ракетки. Эти константы будут содержать специальные переменные. При необходимости их можно будет править в одном месте, а не разыскивать в разных местах файла со скриптом, да еще и в файле со стилями.
Для шарика не заданы свойства left и top, значит, он будет показан в начале координат. Пускай пока побудет там, сначала займёмся ракеткой.
Весь код игры разместим внутри функционального литерала, которым, как обычно, зададим обработчик события load.
Начнем с того, что определим константы для размеров поля и ракетки, определим переменные field и racket со ссылками на объекты, построенные в DOM для этих элементов, программно зададим стилевые правила для них и назначим два обработчика клавиатурных событий в рамках модели уровня 0.
window.onload = function ()
{
// =================== Константы и переменные ======================
// Размеры игрового поля
var widthField = 600;
var heightField = 400;
// Размеры ракетки
var widthRacket = 100;
var heightRacket = 10;
// =================== Ссылки на объекты ===========================
// Ссылки на объекты
var field = document.getElementById("field"); // Поле
var racket = document.getElementById("racket"); // Ракетка
// =================================================================
// =================== Представление ===============================
// Стили игрового поля
field.style.width = widthField+"px";
field.style.height = heightField+"px";
// Стили ракетки
racket.style.width = widthRacket+"px";
racket.style.height = heightRacket+"px";
racket.style.left = 0;
// =================================================================
// =================== Обработчики событий =========================
document.onkeydown = keydownHandler; // Нажата клавиша
document.onkeyup = keyupHandler; // Отпущена клавиша
};
Осталось написать коды обработчиков keydownHandler и keyupHandler.
Обработчик keydownHandler содержит переключатель по коду нажатой клавиши.
Если нажаты клавиши со стрелками (влево/вправо), запускается таймер, который выполняет движение ракетки до тех пор, пока клавиша не будет отпущена или не будет нажата клавиша противоположного направления.
Ракетка начинает движение с ускорением, что имитирует ее инерционную массу. Шаг перемещения ракетки нарастает с коэффициентом aRacket от начального значения step0Racket до максимального stepMaxRacket. Эти константы заданы в следующем фрагменте кода:
var step0Racket=5; // Начальный шаг смещения ракетки
var stepMaxRacket=10; // Максимальный шаг смещения ракетки
var stepRacket=step0Racket; // Текущий шаг смещения ракетки
var aRacket=1.05; // Ускорение: stepRacket *= aRacket на каждом шаге
Обработчик keyupHandler останавливает движение ракетки в двух случаях:
- Отпущена стрелка влево в момент, когда ракетка двигается влево.
- Отпущена стрелка вправо в момент, когда ракетка двигается вправо.
На этом первая часть работы закончена. Шарик отдыхает в левом верхнем углу поля, а ракетка послушна стрелкам клавиатуры.
Пример 6. Почти Арканоид (продолжение)
Разработка продолжена, и функциональность приложения дополнена движением шарика.
praxis/14/examples/06/index.htm
Шарик запускается нажатием на пробел. Этот момент обнаруживается в обработчике keydownHandler, и запускается функция start:
function keydownHandler(event)
{
event = event || window.event;
switch(event.keyCode)
{
...
case 32: // Пробел
start();
}
}
Функция start устанавливает шарик на середину ракетки, генерирует угол и запускает шарик при помощи таймера timerBall:
timerBall = setInterval(moveBall, dtBall);
Таймер через каждые dtBall миллисекунд вызывает функцию moveBall.
Функция moveBall вычисляет новые координаты шарика, обрабатывает отражения от стенок поля и ракетки. Нижняя стенка «поглощает» шарик, и игра заканчивается.
Запустите приложение, поработайте с ним, прочитайте и разберитесь во всех его кодах.