14. Обработка событий

Интерфейс Event

Иерархия интерфейса 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>
Старт&nbsp;&#8212; пробел<BR>
Ракетка&nbsp;&#8212; стрелки
    </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 вычисляет новые координаты шарика, обрабатывает отражения от стенок поля и ракетки. Нижняя стенка «поглощает» шарик, и игра заканчивается.

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