Менеджер курсоров на базе стека
О чем это всё?
Это описание подхода к управлению курсорами, который применяю около 2-х лет на разных проектах
Зачем это читать?
Может это поможет Вам выкинуть сотни строк чудовищной логики из своего проекта.
А может и не поможет - просто интересно, использовал кто нибудь такие подходы или какие применял альтернативы (больше из любопытства, т.к. описываемый метод меня полностью устраивает)
Итак задача:
Написать ActionScript 3 класс(ы) для отображения курсоров, нарисованных художниками (или полученных другим способом) для использования в проекте. И хочется, чтобы это использование было полегче и проблем создавало поменьше.
Критика наиболее распространнённого, "наивного" подхода
(если его не покритиковать - то непонятно, зачем надо было "изобретать велосипед")
Обычно делается менеджер с одним единственным публиным полем, и смена курсора происходит так:
А выключение курсора как-то так:
Первое что бросается в глаза - глобальность. Т.е. отдельные подсистемы приложения будут мешать друг другу устанавливать курсоры.
Но начнём с более простого примера:
1. Есть кнопка, лежащая в окне
Код:
------- button ------------------ window
3. А при вождении по окну показывался курсор "перетащить окно" (тоже нарисованный художниками)
Как это делается:
window.addEventListener(MouseEvent.ROLL_OVER, onWindowRollOver); window.addEventListener(MouseEvent.ROLL_OUT, onWindowRollOut); private function onWindowRollOver(event:MouseEvent):void { // Условно, чтобы не засорять статью, // там может быть что-то типа cursor = new Cursor(new Bmp_move_window()); CursorManager.cursor = "перетащить окно"; } private function onWindowRollOut(event:MouseEvent):void { CursorManager.cursor = null; } button.addEventListener(MouseEvent.ROLL_OVER, onButtonRollOver); button.addEventListener(MouseEvent.ROLL_OUT, onButtonRollOut); private function onButtonRollOver(event:MouseEvent):void { CursorManager.cursor = "нажать"; } private function onButtonRollOut(event:MouseEvent):void { CursorManager.cursor = null; }
- наводим мышку на окно - появляется курсор "перетащить окно";
- переводим мышку на кнопку - появляется курсор "нажать" (пока все идёт нормально)
- переводим мышку с кнопки на окно - курсор исчезает (опаньки!)
Это происходит потому, что кнопка лежит в окне и при уводе курсора с кнопки, события наведения на окно не происходит.
Чешем репу. Наверно, надо при наведении на кнопку запоминать последний курсор в менеджере и при
уводе с кнопки возвращать его:
private var _lastCursor:String; private function onButtonRollOver(event:MouseEvent):void { _lastCursor = CursorManager.cursor; CursorManager.cursor = "нажать"; } private function onButtonRollOut(event:MouseEvent):void { CursorManager.cursor = _lastCursor; }
Код:
--------------------------------------------------> перемещение мыши --------------------------------------------------> время 3-------4 button("cursor3") 2------------------5 window("cursor2") 1------------------------------------6 container ("cursor1") 1. [container OVER] cursor = "cursor1" 2. [window OVER] cursor = "cursor2", window._lastCursor = "cursor1", 3. [button OVER] cursor = "cursor3", button._lastCursor = "cursor2", 4. [button OUT] cursor = (button._lastCursor == "cursor2"), 5. [window OUT] cursor = (window._lastCursor == "cursor1"), 6. [container OUT] cursor = null;
Но хранить кучу ссылок на каждом элементе и следить за ними!?
И так ведь будет хватать другой, нужной, логики...
Может засунуть _lastCursor в менеджер? Не, не получится - будет бажить.
Идем дальше. Обнаруживается, что раз в 30 секунд идет перевтягивание данных окна (это для примера, я так не делаю). Во время перевтягивания надо показывать курсор с песочными часиками (не зависимо от того, где находится курсор).
А вот здесь уже не работает
Код:
------------------------------------------------------> перемещение мыши --------------X waiting("cursor2") X--------------> время | | 1--2----3 4 button("cursor1") 1. [button OVER] cursor = "cursor1", button._lastCursor = null; 2. [waiting on] cursor = "cursor2", waiting._lastCursor = "cursor1"; 3. [button OUT] cursor = (button._lastCursor == null), (Мимо! - должен остаться "cursor2") 4. [waiting off] cursor = (waiting._lastCursor == "cursor1"), (Мимо! - должен быть null)
Обычно в большинстве социалок существует редактирование карты,
когда курсоры меняются в зависимости:
- от того, на какой объект наведена мышка
- от того, какой режим включен (поливка, убирание сорняков, сбор урожая)
- от того, в какой стадии редактирования вы находитесь (выбрали начало дороги - один курсор, начали проводить дорогу - другой)
- в любой момент может появится поп-ап, при наведении на который курсора быть не должно или должен быть свой курсор
- при наведении на кнопки этого поп-апа должны быть свои курсоры тоже.
Чтож, здесь баловаться запоминанием курсоров опасно. Поэтому пишем менеджер с чудесной логикой, знающий о половине классов игры.
Это что, действительно стандартный и нормальный подход?
Теперь критики достаточно, переходим к теме топика.
Принципы работы менеджера на базе стека
Вспомним, как мы хотели упростить логику с запоминаниями:
Может засунуть _lastCursor в менеджер? - не получится, будет бажить.
Одной переменной не получится, а стеком - получится!
И пусть каждая подсистема возьмет себе маленький переключатель и будет включать его и выключать, добавляя и убирая себя из стека.
Все! В этом весь смысл.
Но мы не только упростим таким образом систему "запоминаний" для клиента.
Бонусом мы получим простое решение всех вышеперечисленных проблем.
Теперь разберёмся, как это выглядит в коде.
Та же задача:
Код:
------------------------------------------------------> перемещение мыши --------------X waiting("cursor2") X--------------> время | | 1--2----3 4 button("cursor1")
// "cursor1" - условно, ниже будет приведён пример, который компилируется _buttonSwitcher = CursorManager.newSwitcher().setCursor("cursor1"); button.addEventListener(MouseEvent.ROLL_OVER, onButtonRollOver); button.addEventListener(MouseEvent.ROLL_OUT, onButtonRollOut); private function onButtonRollOver(event:MouseEvent):void { _buttonSwitcher.enabled = true; } private function onButtonRollOut(event:MouseEvent):void { _buttonSwitcher.enabled = false; } ... // Где-то в далёком классе, ведающем включением анимации ожидания _waitingSwitcher = CursorManager.newSwitcher().setCursor("cursor2"); private function startLoading():void { _waitingSwitcher.enabled = true; load(onLoadComplete, onLoadError); } private function onLoadComplete():void { _waitingSwitcher.enabled = false; } private function onLoadError():void { // Ошибка - не повод оставлять курсор ожидания _waitingSwitcher.enabled = false; }
На самом деле даже не обязательно вешать слушатели onButtonRollOver и onButtonRollOut на кнопку, можно сделать тоже самое одной строчкой с помощью HoverSwitcher, но суть не в этом (и для наивного подхода можно было написать класс, навешивающий слушатели).
Как это работает?
Переключатели, у которых вызвали enabled = true добавляем наверх стека, а переключатели, у которых вызвали enabled = false удаляем откуда придется. И выбираем курсор, который принадлежит верхнему переключателю. Всё!
Код:
------------------------------------------------------> перемещение мыши --------------X waiting("cursor2") X--------------> время | | 1--2----3 4 button("cursor1") 1. [button OVER] stack = [buttonSwitcher], cursor = (buttonSwitcher.cursor == "cursor1") 2. [waiting on] stack = [buttonSwitcher, waitingSwitcher], cursor = (waitingSwitcher.cursor == "cursor2") 3. [button OUT] stack = [waitingSwitcher], cursor = (waitingSwitcher.cursor == "cursor2") 4. [waiting off] stack = [], cursor = null
Зачем нужен ещё приоритет?
- вы включили курсор ожидания;
- пользователь навел мышку на кнопку;
- вместо курсора ожидания появился курсор мыши;
- польозватель убрал мышку с кнопки;
- появился курсор ожидания.
Впринципе, с наивным вариантом у нас бы и он не появился, но, согласитесь неприятно
Решается просто - выставляем переключателю анимации ожидания приоритет 1.
Т.е. говорим, этот переключатель главнее других (по умолчанию 0)
Все, никто не перебьет этот курсор (только переключатели с более старшими приоритетами), пока его не выключит владелец _waitingSwitcher.
Но в большинстве случаев можно поставить куче переключателей одинаковый приоритет, пусть даже 0.
И, собственно, пример (использует minimalcomps, сам CursorManager приложен в архиве,
если лень компилировать - в архиве же лежит swf-ка):
private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); // entry point // Инициализация курсоров var defaultCursor:Cursor = new Cursor( new Label(null, 0, 0, "Default cursor"), 10, 14, true); var cursor0:Cursor = new Cursor( new Label(null, 0, 0, "Cursor 0"), 10, 14, true); var cursor1:Cursor = new Cursor( new Label(null, 0, 0, "Cursor 1"), 10, 14, true); var waitCursor:Cursor = new Cursor( new Label(null, 0, 0, "Please wait..."), 0, 0, true, true); // Установка дефолтного курсора CursorManager.init(stage, stage); CursorManager.defaultCursor = defaultCursor; // Добавление курсора на кнопку var button0:PushButton = new PushButton(this, 0, 0, "Button 0"); CursorManager.newHover().setTarget(button0).setCursor(cursor0); // Кнопка с блокировкой курсоров var buttonWithCursorLock:PushButton = new PushButton( this, 0, 30, "Button with cursor lock"); buttonWithCursorLock.width = 120; CursorManager.newHover() .setTarget(buttonWithCursorLock).setCursor(null); // Кнопка без установки курсора new PushButton(this, 0, 60, "Button without cursor").width = 120; // Кнопки в окне var window:Window = new Window( this, 0, 200, "Window without cursor"); window.width = 120; CursorManager.newHover().setTarget(window).setCursor(null); var button1:PushButton = new PushButton( window.content, 10, 10, "Button 1"); CursorManager.newHover().setTarget(button1).setCursor(cursor1); var windowButtonWithoutCursor:PushButton = new PushButton( window.content, 0, 50, "Button without cursor"); windowButtonWithoutCursor.width = 120; window.content.addChild(windowButtonWithoutCursor); // Чек-бокс включения курсора ожидания, затмевающего остальные _waitSwitcher = CursorManager.newSwitcher(1).setCursor(waitCursor); _waitCheckBox = new CheckBox(this, 130, 0, "Enable wait"); _waitCheckBox.addEventListener(MouseEvent.CLICK, onWaitClick); // Изменение курсора кнопки var randomButton:PushButton = new PushButton( this, 130, 30, "Click for random cursor", onSetRandomClick); randomButton.width = 120; randomButton.addEventListener(MouseEvent.CLICK, onSetRandomClick); _randomSwitcher = CursorManager.newHover().setTarget(randomButton); } private var _waitCheckBox:CheckBox; private var _waitSwitcher:Switcher; private function onWaitClick(event:MouseEvent):void { _waitSwitcher.enabled = _waitCheckBox.selected; } private var _randomSwitcher:Switcher; private function onSetRandomClick(event:MouseEvent):void { _randomSwitcher.cursor = new Cursor( new Label(null, 0, 0, "Random cursor: " + Math.random()), 10, 14, true); }
Спасибо что дочитали до этого места! Надеюсь, это было не очень скучно.
Архив с примером, исходниками менеджера и swf-кой примера AS3CursorManager.zip
P.S.
Вообще, раза 2 за всю историю, этот подход использовался для других вещей (не касающихся курсоров). Применялся в случаях, когда компонент может отобразить только одно значение, а понять чье именно надо отобразить - непросто. Но эти ситуации крайне редки - ведь если надо отобразить данные 10-ти компонентов, то все 10 надо показывать, а не выбирать что-то одно.
Всего комментариев 16
Комментарии
21.01.2012 15:17 | |
Использовать систему приоритета для курсора действительно гораздо удобнее, чем менять его в зависимости от событий.
|
21.01.2012 18:31 | |
Цитата:
button.addEventListener(MouseEvent.ROLL_OVER, onButtonRollOut);
Получается если я в одном месте показал курсор с приоритетом 3, а в другом хочу показать с приоритетом 4 то мне самому нужно помнить о приоритете? Метода получения текущего приоритета не вижу. Может стоит добавить метод типа setTopCursor или, что-то типа этого. Можно пояснить в чем разница между newSwitcher и newHover? Ну и плюс, в каждом конкретном проекте, логика может, хоть чуток, да отличаться, а занаследоваться от статического класса я не могу (чтобы подправить его под себя). Получается не очень удобно. |
|
Обновил(-а) Inet_PC 21.01.2012 в 18:51
|
21.01.2012 21:40 | |
Спасибо, поправил.
По поводу приоритетов, 2-х летняя практика применения такая (не подводила): - в половине подсистем просто присваивается приоритет 0 - стек справляется без различий в приоритетах - для другой половины делается перечисление (одно на всё приложение): public class CursorPriority { /** приоритет курсора, блокирующего курсоры над окнами */ public static const WINDOW_LOCK_CURSOR:int = 1; public static const WINDOW_BUTTON:int = 2;// Должен быть выше чем у блокирующего курсоры при наведении на окна /** курсор с песочными часами */ public static const WAIT:int = 100; /** инстурменты на карте при редактировании */ public static const TOOL:int = 0; // Этот приоритет использует куча курсоров } Цитата:
Может стоит добавить метод типа setTopCursor
Цитата:
Можно пояснить в чем разница между newSwitcher и newHover?
Но, например при переключении инструментов вам надо переключать курсор в ручную, соответственно слушатели не нужны, поэтому надо использовать newSwitcher. Т.е. newHover сделан для удобства - не более. Цитата:
Ну и плюс, в каждом конкретном проекте, логика может, хоть чуток, да отличаться, а занаследоваться от статического класса я не могу (чтобы подправить его под себя)
Но никто не запрещает Вам постирать static-и и использовать в виде объекта, наследовать и т.п. Здесь, например, я не стал делать менеджер CCursorManager статическим, просто сделал для него статическую обертку MCursorManger по-дефолту, чтобы кому надо - наследовали и переопределяли, а кому не надо - не мучались. Вообще, да, я огребал с поддержкой статических классов (но не с этим ), здесь просто побоялся усложнить код, т.е. главное в было донести суть подхода. |
|
Обновил(-а) expl 21.01.2012 в 21:53
|
23.01.2012 17:27 | |
Т.е. у вас Cursor - это по сути контроллер, завязанный на приложение?
Впринципе я верю, что с такой логикой можно совладать, если этому менеджеру компоненты системы расказывают в каком они состоянии, а не Cursor следит сам за всем что происходит в приложении. Данных о себе системы передают не много, похоже они все в stateFlags умещаются. Я прав? Цитата:
Потому что если эту логику размазать по приложению, позволяя разным объектам принимать решение - какой курсор показать - рано или поздно будет месиво.
P.S. Сам после написания пары отвратительных менеджеров, которые были осведомлены о половине приложения, перешел на стековую систему и более ничего не испытывал. Может подход со stateFlags тоже работает, но зачем нам писать еще менеджер, зависящий от игры. |
|
Обновил(-а) expl 23.01.2012 в 17:31
|
23.01.2012 21:56 | |
Я джва года ждал этого менеджера.
|
23.01.2012 22:34 | |
А я два года думал публиковать или не стоит
Цитата:
Да нет, он на приложение не завязан. Идея примерно в том же - мы регистрируем нужный курсор для объектов, только без создания переключателей. В stateFlags всего лишь информация о состоянии - например, Cursor.CTRL_PRESSED|Cursor.SHIFT_PRESSED (работает так же, как флаги в Array.sort)
Цитата: Не поверите - месиво не наступает. Вся логика, которую вы реализуете в менеджере у меня сводилась к стеку и приоритетам. Ну не попадалось таких проектов, на которых требовалось делать что-то большее. Да, я присмотрелся к коду - логика управления примерно такая же - установили курсор объекту, поменяли его или убрали... Только у меня не попадалось надобности назначать курсорам еще и приоритеты, а идею со стеком я не совсем понял =) А со стеком - проще всего понять если взять все курсоры с приоритетом 0 и назначить их отдельным вложенным друг в друга объектам. Там автоматом над внутренними объектами будет показываться их курсор, а при переводе мышки на более внешние - курсор внешних. Т.е. при установке нового курсора - показывается он, при убирании - последний добавленный активный(к тому моменту некоторые могут быть уже убраны из стека - деактивированы) - это удобно и при ручной установке курсоров. Да и у Вас ведь, когда добавляете курсор - он же в каком-нибудь массиве или другой структуре хранится вместе с другими и вы как-то выбираете из этой структуры какой именно отображать. Только Вы выбираете из этой структуры на основе какой-то своей логики, а я просто беру верхний с наибольшим приоритетом. |
|
Обновил(-а) expl 23.01.2012 в 22:49
|
23.01.2012 23:24 | |
Цитата:
Сообщение от expl
А я два года думал публиковать или не стоит
пруф |
23.01.2012 23:40 | |
«Дайте я вас расцелую» (c)
|
25.01.2012 15:44 | |
Цитата:
Будут проблемы с удалением объекта - менеджер не предусматривает хранения ссылок и ликвидации HoverSwitcher-ов, а те, в свою очередь регистрируют слушателей в объектах ссылку на который получили в аргументах вызова, пока слушатели не будут отписаны объект будет находиться в памяти.
Следим внимательно: - создается HoverSwitcher, если его никакому полю не присвоили - на него ссылается только target через подписку(благодаря этой подписке его не сносит GC) - при активации курсора ссылка на Switcher попадает в CursorManager, при деактивации - исчезает - убрали target со сцены, обнулили ссылки - если курсор был неактивен - на switcher не осталось ссылок и его спокойно собирает GC (ведь target становится подписанным на объект, который никто не мешает GC удалить из памяти). Все время ожидания срабатывания GC на target навешаны слушатели ROLL_OVER и ROLL_OUT - они никогда не сработают для объекта, не находящегося в списке отображения - значит ресурсов в ожидании удаления тоже жрать этот switcher не будет. Может ли оказаться так, что сносят target с наведенной на него мышкой и активным курсором без срабатывания события roll_out (т.е. можно ли снести target с активным курсором)? - сомневаюсь. 2. Во-вторых. Механизм отписки есть - достаточно присвоить HoverSwitcher::target = null. Проблемы действительно могут возникнуть, если вы навешиваете несколько переключателей на один target - тогда надо сохранять ссылку на предыдущий switcher, присваивать target = null (там в сеттере отписка) и вешать новый. (или без навески нового свитчера - если курсоры боле не нужны) Цитата:
Выход: хранить ссылку на свитчер где-то "снаружи", что конечно не кошерно...
Просто кому Вы предлагаете заниматся отпиской, если не switcher-у? И, если нужно менять курсор target-а, то ссылку на switcher все равно надо где-то сохранить. P.S. В конце концов, можно вообще игнорировать этот HoverSwitcher и навешивать слушатели самому - кроме увеличения объемов кода проблем у вас не будет. HoverSwitcher - это чисто вспомогательный функционал. (хотя простой switcher вам придется хранить в каком-нибудь поле) |
|
Обновил(-а) expl 25.01.2012 в 15:59
|
03.02.2014 18:15 | |
expl большое Вам спасибо за менеджер, но я немного не понимаю принцип с приоритетами. Если Вас не затруднит, не могли бы ответить в этой теме.
|
Последние записи от expl
- Конвеер Потапенко (07.02.2012)
- Менеджер курсоров на базе стека (21.01.2012)
- Unit-тестирование haXe во FlashDevelop (08.03.2011)