Изграждане на полу динамично, дървовидно меню

FlashBG Форуми

Пример

Автор: mAze | Последна промяна: 09.12.2003
Вижте готовия пример: Dynamic menu with XML | Dynamic menu with PHP | Свалете примера архивиран!

Описание

В примера е показано изграждането на полу-динамично дървовидно меню, реализирано с помоща на AS и готов прототип на елементите на менюто. Структурата на менюто се взема от PHP скрипт като променливи или от XML файл.

Предварителна подготовка

Нека да се запознаем с концепцията ми за построяване на файловете, от които ще се чете структурата на дървото

  1. PHP script

    Синтаксисът на стринга съдържащ структурата на дървото е в основата на целия алгоритъм - дори генерираното от XML дърво се приравнява до такъв изходен стринг, затова разбирането му е ЗАДЪЛЖИТЕЛНО! Ето и синтаксиса:

    1. menuX_name = Заглавие на даден елемент на менюто
    2. menuX_link = Линк, който се отваря при щракане върху елемента
    3. menuX_tooltip = Допълнително информационо поле
    4. menuX_n = Колко деца има дадения елемент
    5. menuX_cc1 = Номер на Дете 1
    6. menuX_cc3 = Номер на Дете 3
    7. ...
    8. menuX_ccN = Номер на дете N

    Имайки основната идея, нека да навлезем в подробности. X е поредният номер на елемента; конкретният номер и спазване на някаква поредност не са задължителни. Единствено корена на дървото ТРЯБВА да има номер 0. Нещото, на което трябва да обърнете внимание, е, че номерата записвани в полетата на menuX_ccY трябва да са валидни поредни номера на елементи от менюто. В крайна сметка трябва да се получи плоска структура, изразяваща връзките (родител -> деца) в дървото. Ето и следния пример:

    menu0_name=root
    	&menu0_n=3&menu0_cc1=1&menu0_cc2=2&menu0_cc3=3
    	&menu1_name=L1_1&menu1_n=2&menu1_cc1=4&menu1_cc2=9
    	&menu2_name=L1_2&menu2_n=0
    	&menu3_name=L1_3&menu3_n=1&menu3_cc1=5
    	&menu4_name=L2_4&menu4_n=1&menu4_cc1=6
    	&menu5_name=L2_5&menu5_n=2&menu5_cc1=7&menu5_cc2=8
    	&menu6_name=L3_6&menu6_n=0
    	&menu7_name=L3_7&menu7_n=0
    	&menu8_name=L3_8&menu8_n=0
    	&menu9_name=L2_9&menu9_n=0
    	

    Символът "&" е нужен за да отговаря стринга на стандарта за URL Encoding, който енкодинг се изисква от LoadVars обекта. Примерният PHP script може да видите тук.

  2. XML файл

    За изграждането на XML файла има две основни правила:

    1. myXML.firstChild.firstChild.nodeValue="root" - абсолютно задължително
    2. Имената на елементите да са винаги описани преди <branch> таговете

    Примерен XML код може да видите тук.

Имаме скелета на дървото, какво следва?

  1. Създаваме нов файл във Flash
  2. Създаваме MovieClip, като използваме или New Symbol... (Ctrl+F8) или конвертираме(F8) някой Shape
  3. В клипа създаваме Dynamic Text поле, след което кръщаваме променливата му dt
  4. В Library (F11) даваме име на клипа за връзка с ActionScript: mc_main (с десния бутон на мишката върху клипа и после Linkage...)
  5. Създаваме слой в главния Timeline с име actions и поставяме кода на програмата на Frame 1
  6. Пълният код за Frame 1 можете да видите тук
  7. За да сме съвсем сигурни, че всичко е наред, ще създадем още един клип
  8. Insert->New Symbol..., MovieClip
  9. През Library прехвърляме instance на клипа върху сцената на Frame 1 и го кръщаваме killer
  10. Редактираме клипа като на Frame 20 (примерно) от него слагаме ключов кадър със следният код:
    _parent.hideRECSubMenu(0);
  11. На Frame 1 от клипа поставяме само
    stop();
  12. Записвме файла, който е вече готов за експерименти

Задълбочен анализ на кода

  1. Ще започнем с декларирането на нов клас menu(), който ще прикачим към вече готовия прототип на елемента. Спокойно може да се мине и без това, но така нещата ми се струват малко по-елегантни. Object.registerClass("mc_main",menu) закача дефиницията на класа към клипа "mc_main" който ще използваме за база за всеки едни от елемнтите на менюто.

    var XMLenabled = true;
    	function menu()	{};
    	menu.prototype = new MovieClip();
    	menu.prototype.showSubMenu = showSubMenu;
    	menu.prototype.hideSubMenu = hideSubMenu;
    	menu.prototype.hideSLSubMenu = hideSLSubMenu;
    	Object.registerClass("mc_main",menu);
    	
  2. Следва дефиниция на новите функции, както и една помощна рекурсивна функция за обхождане на нашето меню. Очевидно е, че menu.showSubMenu() е най-простата от всички. Просто показва елементите (_visible = true) точно под текущия възел. Заслужава да се обърне внимание на някои нетипични променливи за класа, а именно this.CC и this.CV[]. Те се добавят динамично при построяването на менюто. CC е броя на децата, а CV[] е масив съдържащ техните уникални поредни номера.

    function showSubMenu() 			// shows one level down
    		{
    			this._visble = true;
    			var i,n;
    			n=this.CC;
    			n++;
    			for (i=1;i < n;i++)
    				{
    					_root["menu"+this.CV[i]]._visible=true;
    				};
    		};
    	
  3. Преминаваме към функциите за затваряне на части от менюто. menu.hideSubMenu() извлича децата като предната функция, но за разлика от нея влиза рекурсивно под всяко от тях и затваря елемнтите им. Така получаваме затваряне на всички елементи на менюто, които се намират на повече от едно ниво под текущия елемент.

    function hideSubMenu()			// closes second levels and down 
    		{
    			this._visible = true;
    			var i,n;
    			n=this.CC;
    			n++;
    			for (i=1;i < n;i++)
    				{
    					hideRECSubMenu(this.CV[i]);
    				};
    		};
    	
  4. За да приключим със затварянето трябва да добавим и menu.hideSLSubMenu(). Тази функция затваря всички деца на менютата, които са на нивото на текуия елемнт и реализира движението в едно и също ниво на менюто (без навлизане в дълбочина). Как работи? За да е всичко както трябва, беше добавена връка от всеки елемент към своя родител. Затова служи променливата this.pidx - чрез нея се получава достъп до списъка с останалите елементи в нивото (те са деца на родителя на текущия елемент). За всяко така получено дете се изпълнява рекурсивно обхождане на неговите деца и затварянето им. Изключение се прави само за текущия елемент. if (_root["menu"+this.pidx].CV[i]!=this.idx), където this.idx е поредния уникален номер на елемента във който сме в момента. Забележете, че в самото начало изпълняваме затваряне само ако this.idx!=0. Това е така защото 0 е достатъчно условие да мислим, че това е корена на менюто, а той няма родител. Следователно страничен ефект от алгоритъма е че нивото веднага под корена не може да се затвори само с движение на мишката.

    function hideSLSubMenu()		// close Same Level Sub Menus
    		{
    			if(this.idx!=0)
    			{
    			this._visible = true;
    			var i,n;
    			n=_root["menu"+this.pidx].CC;
    			n++;
    			for (i=1;i < n;i++)
    				{
    					if (_root["menu"+this.pidx].CV[i]!=this.idx){hideRECSubMenu(_root["menu"+this.pidx].CV[i]);};
    				};
    			};
    		};
    	
  5. Ще си позволя едно лирическо отклонение за да поясня, че би могло hideRECSubMenu(submenu) да се реализира като метод на класа menu(). Така бихме се отървали от гнусния параметър submenu, но така се е получи, за което моля да ме извините. Редовете, коментирани с // rollOver efect, са свързани с някаква анимация предефинирана в скелета на елемнта и по-скоро с връщането на елемента в изходно положение. Това е козметика, която няма нищо общо с алгоритъма и чистата реализация на менюто. Поне на мен рекурсията ми се струва чиста и не виждам смисъл от доизясняване при положение, че вече се запознахме със съдържанието на променливите CC и CV[]. В заключение ще кажа, че функцията hideRECSubMenu(submenu) обхожда рекурсивно всички елемнти под елемента с номер submenu и ги затваря.

    function hideRECSubMenu(submenu)
    		{
    			_root["menu"+submenu].gotoAndStop(1);			// rollOver effect
    			var i,n;
    			n=_root["menu"+submenu].CC;
    			n++;
    			for (i=1;i < n;i++)
    				{
    					hideRECSubMenu(_root["menu"+submenu].CV[i]);
    					_root["menu"+_root["menu"+submenu].CV[i]].gotoAndStop(1);		// rollOver effect
    					_root["menu"+_root["menu"+submenu].CV[i]]._visible=false;
    					
    				};
    		};
    	
  6. Тук ще разгледаме двете функции CREATE() и createSubMenu(idx,pl,cn,cID,pmc,x,y) понеже са неразривно свързани и едната зависи от другата за своето гладко изпълнение. Понеже createSubMenu е рекурсивна, логично е да има някакъв механизъм за предаване на параметри и обратна връзка. В самата функция използваните променливи са обяснени, а тези които не са, вече познаваме (динамично построените). С няколко думи ще опиша всяка от тях:

    • idx - пореден уникален номер, който трябва да приеме построявания в моемнта елемент от менюто
    • pmc - пореден уникален номер на родителя на в момента построявания елемент
    • cID - промелива, показваща дълбочината за построяване на клипа във функцията attachMovie(); с всеки елемент се увеличава с 1;
    • pl - ниво на родителя, тази променлива се увеличава винаги когато се преминава в дълбочина на менюто; основното й предназначение е изчисляване на координати за построяване на текущия елемент
    • cn - като pl, обаче не в дълбочина, а във височина(пореден номер в дадено ниво), пак за изчисляване на отместването по Y-координатата спрямо корена на менюто
    • x,y - задават координати за построяване на текущия елемент; замислени са за ползване единствено при първоначално построяване на корена. Във всички други случаи трябва да са 0, защото се ползват като флаг дали елемента е корен или не.
    • c - променливата от тип LoadVars() или Object() която ползваме като източник на дънамичната структура.

    Препоръчвам ви да погледнете темата във форума, от където произлезе това меню, за да може по-лесно да разберете какво ще се опитам да ви обясня след малко.

    Да напишеш едно динамично меню не се изисква кой знае какъв акъл. Има, разбира се, и разни особености, като например:

    • Когато ползвате функциите movieClip.attachMenu() или moveiClip.duplicateMovieClip(), depth трябва да е уникална стойност за всеки един от елементите му
    • НЕ ПОЛЗВАЙТЕ movieClip.onRollOut() за директно затваряне на менютата. Ако сте прочели темата във форума вече знаете защо, а ако не сте - приемете го на доверие.

    Сега да разбулим малката магия, която действително е много малка и елегантна, макар и не изцяло изпълнена на ActionScript. ЗАТВАРЯНЕ НА МЕНЮТО ПРИ ИЛИЗАНЕ ОТ НЕГО, съпроводено с кратка пауза (timeout), в която потребителя може да се върне в менюто ненаказано. за целта ще използваме малкото клипче, което създадохме в началото и по детински нарекохме killer. Може би вече се досещате, че точно то ще е главния герой в разказа. И така, как да постъпим? В процеса на събитията се случва onRollOut() на някой от нашите елементи по менюто, от тук имаме две възможни продължения: или потребителя излиза извън менюто или влиза директно в друга част от него. Така или иначе от самото събитие onRollOut() това не може да стане ясно и ето защо не предприемаме директно затваряне на менюто, а на негово място пускаме обратно броене за затваряне. Точно това представлява клипът killer и действието предвидено в неговият край. Така ако предположим, че потребителят се е заблудил и е илязъл извън менюто по погрешка, има на разположение този таймаут за да се върне в менюто и да прекрати обратното броене. Броенето се прекратява от всяко събитие onRollOver на кой да е елемент от нашето меню чрез командата _root.killer.gotoAndStop(1). Така покриваме двете възможни развития, ако потребителя иска да остане в менюто след случване на onRollOut, но ако потребителя не иска да остане, то просто малкия чудесен клип се изпълнява и затваря менюто. Елегантно мисля, но вие бихте отсъдили по-добре. Ще ви обърна внимание на още нещо свързано със забраната за ползване на onRollOut() - както виждате, всички действия по менюто (отваряне/затваряне на подменютата) се изпълняват при onRollOver.

    	function createSubMenu(idx,pl,cn,cID,pmc,x,y)		// recursive 
    		{	
    		// idx - ID of the MC to be built
    		// pmc - parent MC ID
    		// cID - curent level
    		// pl  - parent MC level (graphical x-calculation)
    		// cn  - child number (graphical y-calculation)
    		// x   - root-x ; if =0 also a root flag 
    		// y   - root-y ; if =0 also a root flag
    			var nl,j;
    			cID++;
    	
    			_root.attachMovie("mc_main","menu"+idx,cID);
    			_root["menu"+idx].idx=idx;
    			_root["menu"+idx].pidx=pmc;
    			if (x!=0){_root["menu"+idx]._x=x;_root["menu"+idx]._visible = true;} else	{_root["menu"+idx]._x=_root.menu0._x+101*(pl+1);_root["menu"+idx]._visible = false;};
    			if (y!=0){_root["menu"+idx]._y=y} else	{_root["menu"+idx]._y=_root["menu"+pmc]._y+(cn-1)*21};
    			_root["menu"+idx].dt.text=c["menu"+idx+"_name"];
    			nl = c["menu"+idx+"_n"];
    			_root["menu"+idx].CC = nl;
    			_root["menu"+idx].CV = new Array();
    			_root["menu"+idx].onRollOver = function () { _root.killer.gotoAndStop(1);this.gotoAndPlay(2);this.showSubMenu();this.hideSubMenu();this.hideSLSubMenu();};
    			_root["menu"+idx].onRollOut  = function () { _root.killer.gotoAndPlay(2);};
    			if (x!=0) {_root["menu"+idx].onRelease = function() {hideRECSubMenu(0)};};
    			nl++;
    			for (j=1; j < nl;j++)
    				{
    					_root["menu"+idx].CV[j] = c["menu"+idx+"_cc"+j];
    					cID = createSubMenu(c["menu"+idx+"_cc"+j],pl+1,j,cID,idx,0,0);
    					
    				};
    			return cID;	
    		};
    	
    	function CREATE() {createSubMenu(0,-1,0,_cID,"",60,20);};
    	
  7. Функциите по приемането и предаването на параметри от външни файлове не смятам да коментирам в този пример. Ще ви обърна внимание само на следното: на първият ред в кода на програмата с XMLEnabled=... се оказва кой метод за получаване на структурата ще използваме. XMLenabled=true е очевиден, XMLenable=false означава, че ще вземаме данните от PHP скрипт (примерно ако данните се генерират динамично). Другата особеност е, че function xmlREC (xmlnode,xml_ID) прехвърля данните от XML() обекта в съвместими със струкорния стринг изходни данни (за запазване съвместиноста със PHP, ASP, JSP и Бог знае какво още, както и поради други причини, предимно мързел). Във функциите c.sendAndLoad("menu.php",c,"POST") и Receiver.load('data.xml') задайте пътя до вашите източници на данни.

    	var _cID = 1;
    	if (!XMLenabled)
    		{
    			var c = new LoadVars();
    			c.onLoad = CREATE;
    			c.sendAndLoad("http://www.desi9n.org/shared/php/advmenuDB.php",c,"POST");
    		}
    			else
    		{
    			var c_xml = new Object();
    			var c = c_xml;
    			var _xml_ID = 0;
    			Receiver = new XML();
    			Receiver.ignoreWhite = true; 
    			Receiver.onLoad = function (success) 
    										{
    											if (!success) return trace("Failed Loading Xml File");
    											_xml_ID=xmlREC(Receiver.firstChild,_xml_ID);
    											_root.dt2.text=_xml_ID;
    											CREATE();
    										};
    			Receiver.load('http://www.desi9n.org/shared/php/data.xml');
    	
    		};
    	
    	function xmlREC (xmlnode,xml_ID)
    							{   var i;
    								if (xmlnode.nodeType==3)
    									{
    										c_xml["menu"+xml_ID+"_name"]=xmlnode.nodeValue;
    										c_xml["menu"+xml_ID+"_n"]=0;
    										xmlnode.ID=xml_ID;
    										c_xml["menu"+xml_ID+"_link"]=xmlnode.parentNode.attributes.link;
    										c_xml["menu"+xml_ID+"_tooltip"]=xmlnode.parentNode.attributes.tooltip;
    										if (xmlnode.nodeValue!="root")
    											{
    												c_xml["menu"+xmlnode.parentNode.parentNode.parentNode.childNodes[0].ID+"_n"]++;
    												c_xml["menu"+xmlnode.parentNode.parentNode.parentNode.childNodes[0].ID+"_cc"+c_xml["menu"+xmlnode.parentNode.parentNode.parentNode.childNodes[0].ID+"_n"]]=xml_ID;
    											};
    										xml_ID++;	
    										_root.dt1.text=xmlnode.nodeValue;
    										_root.dt2.text=xmlnode.parentNode.attributes.link;
    										_root.dt.text=xmlnode.parentNode.attributes.tooltip;
    									}
    									else
    								for(i=0;xmlnode.childNodes[i]!=null;)
    									{	
    										xml_ID = xmlREC(xmlnode.childNodes[i],xml_ID);
    										i++;
    									};
    								return xml_ID;
    							};
    	

Заключение

В заключение ще кажа, че структурата от данни, съхраняваща връзките между отделните елементи на менюто не е от НИКАКВО значение. Същия пример може да се напише с дърво от обекти, съдържащи не само връзките, но и самите клипове, с вградени функции за обхождане и прочие и прочие... Това обаче не е урок по обектно ориентирано програмиране :), а само пример и насока как да си изградите ваше собствено полу-динамично меню. И внимавайте как използвате onRollOut(), по възможност не я използвайте директно за затваряне! Това беше всичко! Ако имате въпроси, не се колебайте да ги зададете на форумите ни!