Статьи

AS3 101: ООП — Дополнительные понятия

В этой мега-части нашего введения в объектно-ориентированное программирование в ActionScript 3 мы рассмотрим несколько выдающихся тем, а также соберем все воедино и организуем простой проект вокруг нескольких независимых, но совместимых объектов.


Если вы уже следили за серией AS3 101 , у вас не возникнет проблем с общими приемами, описанными в этом руководстве. Вы сосредоточитесь на том, как различные элементы всего ролика Flash взаимодействуют друг с другом, и изучите основы создания объектно-ориентированных приложений. Если вы обнаружите, что это немного над вашей головой, я бы посоветовал вам вернуться и освежить в памяти другие темы, затронутые в этой серии, а именно, касающиеся работы с XML , загрузки внешних изображений и первых двух частей этой серии ООП.


Я собираюсь начать этот урок с обсуждения нескольких концепций, которые не вошли в предыдущие уроки ООП. Идея статических членов, пакетов, импортов, исходных путей и компоновки будет необходима как для проекта просмотра изображений, так и для общего образа жизни объектно-ориентированного программиста. Как и в большинстве моих учебных пособий по AS3 101, мы сначала сосредоточимся в основном на концепциях и теории, а затем будем использовать их более практичным образом.


Все свойства и методы, которые мы писали до сих пор, известны как свойства экземпляра (или методы экземпляра , в зависимости от ситуации). Что это обозначает? Что свойства и методы принадлежат экземпляру. Помните, как мы говорили, что все дома, построенные по одному и тому же проекту, имеют входную дверь, но характеристики двери могут быть уникальными для каждого дома? Да, это было очень давно, еще в начале первого урока ООП, но это то, что является свойством экземпляра. Каждый экземпляр может иметь свое уникальное значение, хранящееся в этой переменной. Можно присвоить одному экземпляру Button101 ярлык «Абракадабра», а другому — «Hocus Pocus». Поскольку каждый объект имеет свое собственное свойство, каждый экземпляр имеет свое собственное значение, хранящееся в нем.

Однако что, если вам просто нужно место для хранения значения, значения, которое не нужно изменять независимо с каждым экземпляром объекта? Что если у Button101 есть свойство углового радиуса, которое вы хотите сохранить согласованным во всех экземплярах кнопок, чтобы все кнопки имели одинаковый внешний вид?

Если это так, то зачем каждому экземпляру нужна своя копия того же значения?

Это не так. Если мы уверены, что все экземпляры смогут использовать одно и то же значение, то мы можем иметь все экземпляры на самом деле с одним и тем же свойством . Мы делаем это, объявляя свойство как статическое . Это означает, что свойство (или метод, если вы должны были сделать то же самое с методом) не принадлежит экземпляру, а классу. Это похоже на указание на план, что все дома должны иметь красную деревянную дверь.

На практике это выглядит так:

1
private static var cornerRadius:Number = 5;

Все, что мы сделали, это добавили ключевое слово static . Это превращает его в свойство класса (или, опять же, метод класса ). Они также называются статическими свойствами (или … да, вы поняли).

Обратите внимание, что, как и ключевое слово override , порядок между static и public/private/internal/protected имеет значения. Опять же, разумно выбрать стандарт и придерживаться его.

Если мы сделаем это (объявим свойство как статическое, а не выберем стандарт), тогда все экземпляры будут иметь одинаковое значение для cornerRadius . Это не копия значения; на самом деле это ссылка на одно и то же значение во всех экземплярах.

Зачем это делать? Ну, например, есть места, где это просто имеет смысл. В сценарии, описанном на этом шаге, это имеет смысл, потому что у нас никогда не было изменений в этом конкретном значении. С другой стороны, объявление свойств или методов как статических может уменьшить использование памяти. Поскольку свойство указывает на одно значение в памяти, а не на уникальные экземпляры значений с одинаковыми именами, это оказывает меньшее влияние на память. Очевидно, что в нашем простом примере создание одной переменной Number вместо двух вряд ли стоит рассматривать, но в тех случаях, когда объекты создаются несколько раз, а значение, содержащееся в свойстве, представляет собой существенную структуру данных, статические свойства могут создавать значимые значения. экономия.

Не упустите возможность обсудить, когда использовать статические методы и свойства в ближайшем будущем.


Для более глубокого изучения того, когда использовать статические методы и свойства, обратите внимание на краткий совет, который скоро будет опубликован, по Activetuts +.


Другой остаток — понятие пакетов . До сих пор мы совершенно не знали о пакетах, но я думаю, что вы готовы с этим справиться. Пакеты — это просто способ организации вашей интро-группы кода и подгрупп. И это во многом связано с тем ключевым словом package которое начинается с каждого файла класса.

Технически, мы все время использовали пакеты. Это невозможно не сделать; это ключевое слово package определяет пакет, к которому принадлежит ваш класс. Тем не менее, мы использовали так называемый пакет верхнего уровня . Когда вы пишете это:

1
2
3
package {
    // Class code here…
}

Вы помещаете свой класс в пакет верхнего уровня.

Однако, по разным причинам, вы можете начать размещать файлы классов в папках. Организация, вероятно, самая распространенная причина; Вы хотели бы объединить различные аспекты проекта и отделить их друг от друга, если это необходимо. Вы также можете повторно использовать имя класса. Если бы вы могли хранить Main класс одного SWF-файла в отдельной папке от Main класса другого SWF-файла, то вы могли бы использовать имя дважды.

Пакеты — это способ организации ваших классов в папки. Это двухэтапный процесс: сначала вам нужно создать файл в структуре папок, отражающей организацию, которую вы хотите. Затем вы отражаете структуру папок после ключевого слова package в файле класса.


Мы создадим образец пакета прямо сейчас, чтобы проиллюстрировать, как это делается. Мы фактически модифицируем проект предыдущего урока, чтобы использовать пакеты. Если у вас нет этих файлов под рукой, его копию можно найти в пакете загрузки как «button_tutorial».

В вашей файловой системе создайте папку для размещения всего проекта.

Скопируйте файл Flash в эту папку

Создайте серию папок в этой папке. Они должны следовать этой структуре:

  • [папка проекта] /
    • as3101 /
      • образец/
      • щ /

Теперь в as3101/sample/ скопируйте файл DocumentClass . Скопируйте класс as3101/ui/ папку as3101/ui/ .

Это первая часть; у нас есть организационная схема в нашей файловой системе. Теперь нам нужно отредактировать эти два файла, чтобы пакет соответствовал этой структуре папок.

Откройте DocumentClass и измените первую строку следующим образом:

1
package as3101.sample {

Аналогично, откройте Button101 и сделайте так, чтобы первая строка читалась:

1
package as3101.ui {

Другими словами, мы отредактировали файлы так, чтобы бит между package и открывающей фигурной скобкой отражал структуру папок, ведущую к самому файлу класса, относительно файла Flash. Мы используем точки, чтобы отделить элементы папки, но не косую черту или обратную косую черту. Обратите внимание, что само имя класса не включено; пакет — это просто вложенные папки. Имя класса по-прежнему указывается после ключевого слова class .

Как организационная техника, это открывает гораздо больше возможностей для структурирования ваших проектов. По мере того, как вы будете писать все больше и больше классов, вы захотите сгруппировать связанные классы вместе и создать иерархии группировок. Пакеты позволят вам сделать именно это.

Тем не менее, мы еще не закончили; если вы попытаетесь запустить проект прямо сейчас, вы ничего не получите; поскольку мы переместили DocumentClass, файл Flash больше не может его найти.


Один тонкий момент, который стоит иметь в виду, заключается в том, что как только вы используете пакет, имя вашего класса становится более сложным. Теперь, для всех намерений и целей, наши два класса по-прежнему называются « DocumentClass » и « Button101 ». Когда вы обсуждаете их с другими членами команды, вы можете ссылаться на них по этим именам, и даже в коде вы все равно можете использовать эти имена. Однако, технически, использование пакета превращает имя класса в более длинное имя. Имя становится package + "." + class name package + "." + class name . То есть наш DocumentClass действительно as3101.sample.DocumentClass , а Button101 действительно as3101.ui.Button101 .

Это называется полным именем класса , и оно сыграет важную роль в организации наших файлов классов.

Вы также можете увидеть это в следующем виде: as3101.ui::Button101 . Это на самом деле не означает ничего отличного от многоточечной версии. Просто знайте, как его читать (вы увидите это чаще всего, когда происходит ошибка во время выполнения, ошибка описывает классы, в которых произошла ошибка).

Итак, почему наш документ Flash теперь ничего не делает? Потому что в свойствах документа класс документа установлен на DocumentClass . Но наш класс на самом деле называется as3101.sample.DocumentClass . Обычно такое неправильное именование приводит к ошибке компилятора, но в этой ситуации Flash пытается помочь. Он знает, как искать класс DocumentClass в качестве класса DocumentClass файла Flash. Но он не может его найти (это точное имя класса больше не существует, так как мы переместили класс в пакет). Итак, Flash молча создает класс документа, который называется DocumentClass для нас. Как это происходит, этот класс не делает ничего, кроме расширения MovieClip и его называют DocumentClass . Поэтому, если мы нажмем «Тестировать ролик», мы ничего не получим; пустой SWF, отражающий пустой класс документа, созданный для нас.

Прежде чем исправить это, сначала докажите это, открыв параметры публикации (Файл> Параметры публикации …) для документа. Нажмите кнопку «Настройки…» рядом с элементом управления «Сценарий: ActionScript 3.0». В появившемся окне нажмите кнопку с галочкой рядом с входом «Класс документа:»:

Окно настроек ActionScript 3.0;  нажмите на кнопку галочки.

Это приведет к следующему сообщению об ошибке:

Сообщение об ошибке, когда Flash не может найти класс документа.

Это способ Флэша сказать то же самое, что я только что сказал вам. Только Flash не скажет вам этого, если вы не предпримете какие-то действия, как мы только что сделали.

Чтобы все исправить, измените значение этого поля класса документа на as3101.sample.DocumentClass . Когда вы нажимаете Return, ничего не должно происходить (другой вариант — получить похожее диалоговое окно с предупреждением). Когда вы нажмете на иконку карандаша, вы увидите, что класс открыт. Это означает, что мы успешно повторно связали наш Flash-файл с его недавно переименованным классом документов.

Однако, если вы тестируете сейчас, все равно не работает. С другой стороны, вы, по крайней мере, получите некоторые ошибки, объясняющие, почему что-то не работает:

Ошибки компилятора

По сути, мы столкнулись с той же проблемой с классом Button101 что и с DocumentClass .


Как только вы начинаете помещать классы в пакеты, вам внезапно приходится указывать Flash, где их искать. Остерегайтесь проблемы, раскрытой на последнем шаге, но, к счастью, повсюду в другом месте вопрос указания Flash, где искать классы — это простая строка кода. Это оператор import .

Вы уже использовали их, и, возможно, уже можете догадаться, почему они там. Мы написали довольно много import flash.display.Sprite; виды строк в последних двух уроках, но теперь мы можем написать их для наших собственных классов.

Ошибка, Button101 в конце последнего шага, говорит о том, что тип не найден: Button101 . Это может немного сбивать с толку (и по общему признанию, ошибки компилятора Adobe, как правило, формулируются менее чем полезно). Это просто означает, что мы использовали что-то под названием Button101 но никогда не определяли, что это такое. Мы используем класс, но мы не используем это полное имя, чтобы указывать Flash в правильном направлении.

Вы можете предположить, что все, что вам нужно сделать, это что-то вроде этого:

1
var button:as3101.ui.Button101 = new as3101.ui.Button101();

Не то чтобы что-то не так с использованием полного имени, но это не решает проблему под рукой. Чтобы сообщить Flash, где находится этот недавно перемещенный класс Button101 , нам нужно его импортировать .

строки import должны идти до объявления class . Что это, после того, как пакет открывается, но до того, как класс открывается. Как это:

1
2
3
4
5
6
package as3101.sample {
 
    // Imports go here.
 
    public class DocumentClass extends MovieClip {
        // …

Они могут быть в любом порядке, который вам нравится, но они должны быть сгруппированы в этом месте. Большинство разработчиков тратят время на алфавитное выражение своих операторов импорта (фактически Flash Builder делает это за вас).

Теперь, чтобы решить нашу текущую проблему, мы просто импортируем класс Button101 :

1
2
3
4
5
6
7
package as3101.sample {
 
    import as3101.ui.Button101;
    // Other imports…
 
    public class DocumentClass extends MovieClip {
        // …

Обратите внимание, что как только класс был импортирован с использованием его полного имени, он становится доступным классу по его короткому имени. Итак, строки, которые читают var button:Button101 = new Button101(); не нужно менять. В этом нет ничего плохого, по сути, с использованием полного имени. Но импорт позволяет нам использовать сокращенное имя, что означает, что у нас меньше символов, загромождающих код, и это хорошо.

Протестируйте фильм сейчас, и, когда Flash знает, где находятся различные классы, вы должны вернуться в бизнес.

Еще одно небольшое замечание об импорте: технически вам не нужно писать операторы импорта для классов, которые находятся в том же пакете, что и класс, выполняющий импорт. Однако ничего не повредит, если включить их, и, на мой взгляд, самодокументированные заявления о ввозе природы делают это целесообразной практикой.

Готовые файлы для этого примера (включая изменение пакета, включение импорта и обновленный FLA для отображения нового класса Document) находятся в пакете загрузки как «button_tutorial_packages».

Подводя итог этим пунктам об импорте:

  • Если класс, который вы хотите использовать, находится в другом пакете, вы должны использовать оператор import .
  • Это позволяет вам использовать «короткое имя» класса, но можно использовать полное имя, если вам нужно.
  • Если класс, который вы хотите использовать, находится в том же пакете, оператор import не требуется, но его можно использовать, если хотите.

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

Однако вы можете использовать импорт с использованием подстановочных знаков . Это выглядит так:

1
import as3101.ui.*;

И он делает то, что вы, вероятно, ожидаете. Любой класс внутри пакета пользовательского ui теперь доступен классу, выполняющему импорт по его короткому имени.

Есть несколько вещей, которые стоит отметить в этой технике.

  1. Вы не можете сделать это: import as3101.*; , Это вам ничего не даст. Подстановочные знаки импортируют только те классы, которые являются прямыми потомками
    указанный пакет Поскольку Button101 и DocumentClass являются членами подпакетов as3101, мы ничего не добьемся
    эта попытка.
  2. Использование подстановочного знака автоматически не компилирует каждый класс в пакете в ваш SWF. Я повторяю: это не так .
    Компилятор Flash достаточно умен, чтобы компилировать только те классы, которые он видит, используемые вашим SWF. Вам не нужно
    беспокоиться о ненужном увеличении размера файла, используя подстановочный знак.

Зная это, использовать групповые символы или нет, должно стать личным предпочтением. Лично я использую их только на классах Flash Player (те, которые начинаются с flash.… ). В этих случаях я использую так много flash.net или flash.display что просто убедиться, что они все доступны, без необходимости возвращаться в раздел импорта и добавлять новый импорт. Тем не менее, с классами, которые я пишу, я считаю полезным специально перечислять импорт по отдельности, как форму документации. Вы можете посмотреть в разделе импорта и увидеть, на какие другие классы опирается данный класс. Однако выбор за вами, и я бы посоветовал вам определить свое соглашение и придерживаться его.


Я напишу краткий совет, который углубится в импорт. Это будет выпущено в ближайшее время, поэтому будьте внимательны, если хотите узнать больше о тонких деталях операторов импорта.


Для более глубокого обсуждения импорта с подстановочными знаками следите за появлением Activetuts + Quick Tip на эту тему.


Если мы собираемся поговорить об использовании пакетов для организации наших файлов классов, нам следует расширить обсуждение до путей к исходным текстам. Исходный путь — это каталог, в котором Flash будет искать классы для компиляции. По умолчанию каждый документ Flash знает, что нужно искать файлы классов в собственном каталоге документа Flash. Вот почему наши примеры работали до сих пор; наши классы всегда находились в той же папке, что и документ Flash (имейте в виду, что наше недавнее занятие по пакетам технически помещает файл класса в другую папку, но полное имя класса разрешает класс в серию папок плюс файл. И корневая папка наших пакетов находится в той же папке, что и документ Flash).

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

  1. У вас есть многократно используемые служебные классы (возможно, TweenMax, Away3D или что-то полезное, что вы написали). Вы хотите использовать эти классы
    во всех проектах, и не нужно копировать файлы из одного проекта в другой.
  2. Вы хотели бы организовать свой проект так, чтобы по какой-то причине ваши Flash-файлы находились в другом каталоге, чем ваш класс
    файлы. Это действительно имеет смысл, когда у вас большой проект, включающий много Flash-файлов. Просто чтобы сохранить структуру проекта
    Проще говоря, вы можете решить оставить папку «classes» и папку «flas». Таким образом, Flash-файлы сгруппированы, но все еще могут
    делиться классами, общими для всего проекта.

Естественно, вы можете выполнить эти задачи. Я бы не поднял их, если бы ты не смог. Я имею в виду, но не это значит.

И, конечно, у вас есть два варианта сделать это. Оба включают определение исходного пути . Вы можете определить их на уровне приложения (в этом случае исходный путь доступен для каждого документа Flash, который вы открываете на этом компьютере) или на уровне документа (в этом случае исходный путь доступен только для этого документа). В случае причины № 1 выше, путь источника уровня приложения имеет смысл. Что касается причины № 2, путь источника уровня документа будет лучше.

Чтобы определить исходный путь уровня приложения

Откройте настройки Flash (на Mac: Flash> Настройки; на ПК: «Правка»> «Настройки»). Нажмите на категорию ActionScript слева. В нижней части появившегося окна нажмите кнопку «Параметры ActionScript 3.0…».

Получение к окну пути источника уровня приложения

В появившемся окне есть три области: исходные пути — верхняя.

окно исходного пути уровня приложения

Нажмите на кнопку «+» и введите свой путь. Или нажмите на кнопку папки, чтобы перейти к ней. Однако, если вы введете его, нажмите «ОК», пока не вернетесь к Flash. На данный момент вы можете свободно импортировать и использовать любой класс из этого каталога.

Чтобы определить исходный путь на уровне документа

Открыв документ, откройте «Параметры публикации» (выберите «Файл»> «Параметры публикации…»). Нажмите на вкладку «Flash». Нажмите кнопку «Настройки…» рядом с элементом управления версией ActionScript. Откроется новое окно, и оно будет очень похоже на окно, которое вы получите с настройками приложения. Его расположение немного отличается, но идея та же. Убедитесь, что вы находитесь на «Source Path» и введите свой путь. Нажмите «ОК» и помните, что это только для этого документа .

Окно исходного пути уровня документа

Между пакетами и исходными путями у вас есть много вариантов организации ваших классов.


В предыдущем уроке мы много внимания уделяли наследованию. Существует методика, которая может считаться противоположной наследственности. Это называется композиция .

Теперь нет ничего плохого в наследовании. Не то чтобы я сидел на троне лжи и кормил тебя злыми тарадидлами в этом последнем уроке. Но вы можете встретить фразу «предпочесть композицию наследованию» в вашем объектно-ориентированном путешествии. Если (когда) вы начнете изучать шаблоны проектирования, вы наверняка услышите это, и вы даже увидите, как это проявится, если будете достаточно долго работать с продвинутыми программистами. Недавно Big Spaceship (на момент написания этой статьи) опубликовал обсуждение методологии, которую они использовали при разработке пакета отображения своей библиотеки Github-ed ActionScript . В этом они объясняют свои причины, по существу, композиции вместо наследования.

Так что же это? Хорошей новостью является то, что вы уже знаете, как это сделать, и делали это все время. Композиция — это когда один объект сохраняет другой объект в свойстве. Если у вашего класса есть свойство myLoader и в нем хранится объект Loader , то говорят, что ваш класс составляет объект Loader. Это происходит все время; фактически сам Loader имеет свойство contentLoaderInfo которое само составляет объект LoaderInfo , который предоставляет вам информацию о фактической загрузке.


Композиция кажется далекой от наследства, не так ли? Эти два, вероятно, не кажутся связанными вообще. Но Button101 это: в последнем уроке мы провели большую часть времени, создавая класс Button101 , расширяющий Sprite . Это сделало все то, чем является Sprite (и все, что представляет собой DisplayObject , и все, чем является DisplayObjectContainer , и… хорошо, вы помните это обсуждение). Это, безусловно, было полезно, и опять же, я здесь не для того, чтобы говорить вам, что это был неправильный способ сделать это.

Но альтернативный способ сделать это — не расширять что-либо конкретное, а вместо этого позволить классу Button101 создать экземпляр своего собственного объекта Sprite или получить целевой объект Sprite в качестве параметра конструктора, или каким-то образом составить Sprite и не наследовать его. ,

Как это будет работать, может быть не сразу понятно. Позвольте мне бросить кучу кода в вас, как дробовик, чтобы сделать это более ясным.

Сначала рассмотрим оригинальную реализацию. Вот полностью класс Button101 из последнего урока (вы можете найти проект в папке «button_tutorial» в пакете загрузки):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package {
    import flash.display.Shape;
    import flash.display.Sprite;
    import flash.events.MouseEvent;
    import flash.text.TextField;
    import flash.text.TextFormat;
 
    public class Button101 extends Sprite {
 
        private var bgd:Shape;
        private var labelField:TextField;
        private var _url:String;
 
 
        public function Button101() {
            bgd = new Shape();
            bgd.graphics.beginFill(0x999999, 1);
            bgd.graphics.drawRect(0, 0, 200, 50);
            addChild(bgd);
 
            labelField = new TextField();
            labelField.width = 200;
            labelField.height = 30;
            labelField.y = 15;
            var format:TextFormat = new TextFormat();
            format.align = «center»;
            format.size = 14;
            format.font = «Verdana»;
            labelField.defaultTextFormat = format;
            addChild(labelField);
 
            addEventListener(MouseEvent.ROLL_OVER, onOver);
            addEventListener(MouseEvent.ROLL_OUT, onOut);
 
            mouseChildren = false;
            buttonMode = true;
        }
        public function set label(text:String):void {
            labelField.text = text;
            var autoWidth:Number = Math.max(200, labelField.textWidth + 40);
            this.width = autoWidth;
        }
        public function get label():String {
            return labelField.text;
        }
        private function onOver(e:MouseEvent):void {
            bgd.alpha = 0.8;
        }
        private function onOut(e:MouseEvent):void {
            bgd.alpha = 1;
        }
 
        public function set url(theUrl:String):void {
            if (theUrl.indexOf(«http://») == -1) {
                theUrl = «http://» + theUrl;
            }
            _url = theUrl;
        }
 
        public function get url():String {
            return _url;
        }
 
        override public function set width(w:Number):void {
            labelField.width = w;
            bgd.width = w;
        }
        override public function set height(h:Number):void {
            labelField.height = h;
            labelField.y = (h — labelField.textHeight) / 2 — 3;
            bgd.height = h;
        }
    }
}

А вот фрагмент кода из класса DocumentClass, который устанавливает Button101 :

1
2
3
4
5
6
7
button = new Button101();
button.x = 10;
button.y = 200;
button.label = «Active Tuts»;
button.url = «http://active.tutsplus.com»;
addChild(button);
button.addEventListener(MouseEvent.CLICK, onButtonClick);

Теперь вот эквивалентный код, использующий только композицию вместо наследования. Это обновленный класс Button101 (измененные и добавленные строки выделены):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package {
    import flash.display.Shape;
    import flash.display.Sprite;
    import flash.events.MouseEvent;
    import flash.text.TextField;
    import flash.text.TextFormat;
 
    public class Button101 {
 
        private var _target:Sprite;
        private var bgd:Shape;
        private var labelField:TextField;
        private var _url:String;
 
 
        public function Button101() {
            _target = new Sprite();
 
            bgd = new Shape();
            bgd.graphics.beginFill(0x999999, 1);
            bgd.graphics.drawRect(0, 0, 200, 50);
            _target.addChild(bgd);
 
            labelField = new TextField();
            labelField.width = 200;
            labelField.height = 30;
            labelField.y = 15;
            var format:TextFormat = new TextFormat();
            format.align = «center»;
            format.size = 14;
            format.font = «Verdana»;
            labelField.defaultTextFormat = format;
            _target.addChild(labelField);
 
            _target.addEventListener(MouseEvent.ROLL_OVER, onOver);
            _target.addEventListener(MouseEvent.ROLL_OUT, onOut);
 
            _target.mouseChildren = false;
            _target.buttonMode = true;
        }
        public function set label(text:String):void {
            labelField.text = text;
            var autoWidth:Number = Math.max(200, labelField.textWidth + 40);
            this.width = autoWidth;
        }
        public function get label():String {
            return labelField.text;
        }
        private function onOver(e:MouseEvent):void {
            bgd.alpha = 0.8;
        }
        private function onOut(e:MouseEvent):void {
            bgd.alpha = 1;
        }
 
        public function set url(theUrl:String):void {
            if (theUrl.indexOf(«http://») == -1) {
                theUrl = «http://» + theUrl;
            }
            _url = theUrl;
        }
 
        public function get url():String {
            return _url;
        }
 
        public function set width(w:Number):void {
            labelField.width = w;
            bgd.width = w;
        }
        public function set height(h:Number):void {
            labelField.height = h;
            labelField.y = (h — labelField.textHeight) / 2 — 3;
            bgd.height = h;
        }
 
        public function get target():Sprite {
            return _target;
        }
    }
}

И вот обновленный фрагмент кода, необходимый для установки обновленного Button101 (опять же, изменения выделены):

1
2
3
4
5
6
7
button = new Button101();
button.target.x = 10;
button.target.y = 200;
button.label = «Active Tuts»;
button.url = «http://active.tutsplus.com»;
addChild(button.target);
button.target.addEventListener(MouseEvent.CLICK, onButtonClick);

Это не намного больше кода. Это больше о добавлении свойства _target и его использовании вместо неявного this когда речь шла о Sprite подобных вещах.

Вот более тщательная разбивка:

  • Сначала в строке 8 мы удаляем бит extends Sprite чтобы мы не использовали наследование.
  • В строке 10 мы добавили свойство target . Это будет хранить наш Sprite (то есть, он составляет Sprite), который был ранее
    наследуется. Это настраивается с помощью композиции, а не наследования.
  • В строке 17, первой строке конструктора, мы создаем новый Sprite и _target его в нашем _target . В этот момент,
    мы успешно используем композицию.
  • В строках 22, 33 и 35-39 мы просто добавили ссылку на _target перед вызовами методов, которые ранее предоставлялись
    Суперкласс Sprite . Поскольку мы больше не расширяем Sprite , у нас больше нет метода addChild или addEventListener ,
    или свойства mouseChildren или buttonMode . У нас, однако, есть Sprite на котором мы можем называть эти вещи, поэтому мы передаем
    ответственность там.
  • В строках 67 и 71 нам нужно удалить ключевое слово override , поскольку мы ничего не расширяем, поэтому нечего переопределять.
  • В конце класса (строки 77-79) мы добавляем совершенно новый метод. Это функция получения, которая возвращает ссылку на
    составленный Sprite . Этот шаг имеет решающее значение для изменений в DocumentClass.
  • В DocumentClass все изменения связаны с направлением Sprite подобного поведения к кнопке, а не к кнопке.
    сам. Например, в строках 2 и 3 вместо позиционирования объекта кнопки мы позиционируем цель кнопки. Так же,
    мы не можем добавить button как дочерний элемент в список отображения, но мы можем добавить button.target .

Тогда конечный результат должен быть одинаковым. Разница заключается в том, как Button101 этот Button101 .

На самом деле, конечный результат не работает без каких-либо дополнительных модификаций, так как обработчик кликов требует немного дополнительного внимания. Объяснение необходимых изменений немного выходит за рамки этого руководства, но вы можете найти полностью работающий пример в пакете загрузки в разделе » button_tutorial_composition «


Итак, если и наследование, и композиция являются приемлемыми методами, как вы узнаете, когда и какие использовать? Ну, во-первых, есть упомянутый ранее архетипический ответ «предпочитаю композицию наследованию». Если вы чувствуете, что у вас есть возможность пойти с одним из них, возможно, вы захотите ошибиться на стороне композиции, хотя бы потому, что вы знаете какого-то лживого старого автора учебников, который сказал вам, что вы делаете это. Однако со всей серьезностью вы можете верить, что люди умнее вас (и умнее меня) придумали эту идиому предпочтения, и нет никаких оснований не поверить на это их словам.

Но как насчет более практического совета? Популярным методом оценки соответствия наследования и композиции является тест «Имеет» / «Есть».

Это идет так. Вы берете имена ваших двух классов (на данный момент мы представим, что это «ClassA» и «ClassB») и пробуем их в следующих предложениях:

ClassB — это ClassA.

ClassB имеет ClassA.

И обратите внимание на тот, который является более подходящими отношениями.

Если это более уместно, то это отношения наследования. Если есть более подходящим, то это композиционные отношения.

Например, давайте рассмотрим встроенный класс TextField . Скажите следующее вслух (громко и убедительно, особенно если вы находитесь в людной комнате).

TextField является DisplayObject .

TextField имеет DisplayObject .

Для меня, по крайней мере, отношения имеют больше смысла. TextField является отображаемым, поэтому это DisplayObject . Теперь TextField может также стилизовать свой текст с помощью класса TextFormat . Как насчет следующего:

TextField является TextFormat .

TextField имеет TextFormat .

На данный момент, мы надеемся, что мы согласны с тем, что для TextField имеет больше смысла использовать ( иметь ) TextFormat . Это полезный компонент TextField , но TextField сам по себе не определяется как TextFormat .

Давайте подумаем о другом примере — встроенном классе Loader . Помните, что Loader — это тип экранного объекта, который может загружать внешний контент. Давайте попробуем следующие утверждения:

Loader является DisplayObject

Loader имеет DisplayObject

Если вы ответили, что отношения имеют больше смысла, вы правы. Loader сам по себе отображаем. Если вы ответили, что отношения имеют больше смысла, вы тоже правы! В лагере AS3 101 нет проигравших! В Loader есть свойство content которое является отображаемым содержимым, которое было загружено.

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

Button101 — это Button101 .

У Button101 есть Sprite .

Ну, это наши дебаты с предыдущего шага, не так ли? Что имеет больше смысла? Для меня любой имеет смысл. Кнопка вполне может быть DisplayObject некоторого типа, или это может быть что-то еще и просто управлять рассматриваемым DisplayObject . В этом конкретном примере, я думаю, ответ немного зависит от личных предпочтений. Для протокола, я предпочитаю отдавать предпочтение композиции. Вы можете сделать свои собственные выводы, но помните, что каждый раз, когда вы выбираете наследство, Бог убивает котенка.

(Да, это была шутка. Лично я не верю в котят.)

Вы можете узнать больше, выполнив поиск «наследование по сравнению с составом» или «предпочитать наследование по составу». Многие результаты, которые я обнаружил, были связаны с техническими аспектами от среднего до продвинутого, но если вы захотите более глубоко обсудить эту тему, Google может помочь вам.


Если вам нужно более наглядное объяснение наследования и композиции, подумайте о следующем.

Наследование похоже на Робокопа . Он типичный человек расширенный с дополнительной функциональностью. Или, другими словами, он робот, который наследует основные функциональные возможности человека.

Робокоп

http://www.internationalhero.co.uk/r/robocop.jpg

Композиция похожа на Майкла Найта и Китта . Майкл Найт — типичный человек, и остается типичным человеком, только он владеет (создает) самосознающим автомобилем.

Рыцарь всадник

http://manggoblog.files.wordpress.com/2008/10/hoff-knight-rider-mustang.jpg

Оба полностью рад, но они достигли своей великолепности с помощью различных методов.

Но из этих двух только Хофф стал звездой в Baywatch. Поэтому предпочитайте композицию наследству.

Baywatch

http://stupidcelebrities.net/wp-content/hasselhoff2.jpg

(Я должен отметить, что я не придумал эту аналогию, но я затрудняюсь найти первоначальный источник. Если кто-нибудь знает, пожалуйста, сообщите нам об этом в комментариях)


Моя точка зрения при создании композиции больше связана со сборкой более крупной программы из объектов, а не с обсуждением более тонких моментов того, целесообразно ли это по сравнению с наследованием в различных ситуациях. Эти тонкие моменты важны, и вы будете сталкиваться с ними много раз по мере того, как вы будете расти как объектно-ориентированный разработчик, но сейчас нам нужно посмотреть, как мы применяем наши методы объектно-ориентированного программирования для создания более крупного приложения. ,

Наиболее распространенное разочарование и замешательство, которое я вижу у начинающих разработчиков ООП, связано с взаимодействием между различными объектами.Большинство людей на этом этапе своего образования понимают классы и предметы, а также то, как писать данный класс. Следующий шаг — начать использовать несколько классов и объектов для создания чего-то полезного (например, веб-сайта), но кажется, что естественная путаница возникает из-за того, что объекты должны быть инкапсулированы (помните это обсуждение? Это было рассмотрено в моем первом ООП). учебник ). Тогда как один объект взаимодействует с другим объектом, чтобы создать систему, из которой можно построить эту полезную программу?

Большинство первых попыток, которые я засвидетельствовал, связаны либо с ужасными, ужасными нарушениями принципа инкапсуляции, сферой ответственности для данного класса, либо с обоими. Или же попытки просто не работают. Я хотел бы выделить время (и пространство статьи), чтобы показать вам пример плохо написанной системы объектов.


Я надеюсь, что заголовок сделал точку.

В этом примере я представляю два класса Button101BadIdea, и DocumentClassBadIdea. Они основаны на предыдущих Button101примерах, но вы увидите модификации, специально предназначенные для иллюстрации плохой техники. Я сократил оба класса, пытаясь немного сфокусировать внимание на соответствующем коде.

Вот DocumentClassBadIdea

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package {
    import flash.display. MovieClip;
    import flash.text.TextField;
    import flash.events.MouseEvent;
    import flash.net.*;
 
    public class DocumentClassBadIdea extends MovieClip {
 
        private var tf:TextField;
        private var button1:Button101BadIdea;
        private var button2:Button101BadIdea;
 
        public function DocumentClass() {
            tf = new TextField();
            addChild(tf);
            tf.text = "Hello World";
 
            button1 = new Button101BadIdea(tf, "You clicked button 1");
            button1.x = 10;
            button1.y = 200;
            button1.label = "Button 1";
            addChild(button1);
 
            button2 = new Button101BadIdea(tf, "You clicked button 2");
            button2.x = 220;
            button2.y = 200;
            button2.label = "Button 2";
            addChild(button2);
 
        }
    }
}

А вот Button101BadIdea :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package {
    import flash.display.Shape;
    import flash.display.Sprite;
    import flash.events.MouseEvent;
    import flash.text.TextField;
    import flash.text.TextFormat;
 
    public class Button101BadIdea extends Sprite {
 
        private var bgd:Shape;
        private var labelField:TextField;
        private var _clickField:TextField;
        private var _text:String;
 
 
        public function Button101BadIdea(tf:TextField, text:String) {
            _clickField = tf;
            _text = text;
 
            bgd = new Shape();
            bgd.graphics.beginFill(0x999999, 1);
            bgd.graphics.drawRect(0, 0, 200, 50);
            addChild(bgd);
 
            labelField = new TextField();
            labelField.width = 200;
            labelField.height = 30;
            labelField.y = 15;
            var format:TextFormat = new TextFormat();
            format.align = "center";
            format.size = 14;
            format.font = "Verdana";
            labelField.defaultTextFormat = format;
            addChild(labelField);
 
            addEventListener(MouseEvent.ROLL_OVER, onOver);
            addEventListener(MouseEvent.ROLL_OUT, onOut);
            addEventListener(MouseEvent.CLICK, onClick);
 
            mouseChildren = false;
            buttonMode = true;
        }
        public function set label(text:String):void {
            labelField.text = text;
        }
        public function get label():String {
            return labelField.text;
        }
        private function onOver(e:MouseEvent):void {
            bgd.alpha = 0.8;
        }
        private function onOut(e:MouseEvent):void {
            bgd.alpha = 1;
        }
        private function onClick(e:MouseEvent):void {
            _clickField.text = _text;
        }
 
    }
}

Изменение неуловимо. И если вы попробуете это (посмотрите в пакете загрузки для проекта «bad_idea»), вы увидите, что он работает просто отлично. Но вот что изменилось:

  • В DocumentClassBadIdea в строках 18 и 24 конструктору передаются два аргумента Button101BadIdea, ссылка TextFieldна сцену и небольшой текст.
  • Мы также удалили строки, которые добавили CLICKслушателя к кнопкам, и функцию прослушивания щелчком.
  • В Button101BadIdea в строке 16 мы получаем два параметра (the TextFieldи the String), а затем сохраняем их в свойствах экземпляра в следующих двух строках.
  • В строке 38 мы добавляем нашего собственного CLICKслушателя для внутренней обработки.
  • На линиях 55-57 у нас есть CLICKслушатель. Эти новые свойства используются для применения Stringк текстовому значению TextField.

Теперь, если это работает, почему это плохая идея? Обычно довольно субъективно считать правильность кода. С одной стороны, если это работает, то это не может быть так плохо. С другой стороны, если код должен поддерживаться , то удобство сопровождения кода так же важно, как и функциональность.

Код, представленный здесь, не подлежит обслуживанию. Самая большая проблема прямо здесь (в Button101BadIdea):

1
_clickField.text = _text;

Мы только что заблокировали наш класс кнопки для выполнения одной и только одной задачи: помещения текста в TextField. Теперь, конечно, он настроен на использование любого текста и любого TextField, но на самом деле эта кнопка имеет очень конкретное назначение. Если вам когда-нибудь понадобится кнопка, чтобы сделать что-то еще — чтонибудь еще — вы застряли бы с необходимостью создавать больше классов кнопок. Даже если внешний вид кнопки должен был быть таким же.


Конечно, лучшим решением было то, что уже было представлено в предыдущих уроках (см. Проект «button_tutorial» в пакете загрузки, если вам нужно обновить). Но что делает это лучше?

Для начала, Button101класс гораздо более пригоден для повторного использования, если он не предполагает, что щелчок должен привести к вводу текста в TextField. При необходимости этот конечный результат все еще может быть достигнут, но может быть и любой другой результат. Но мы достигаем результата не в рамках Button101объекта; вместо этого эта логика находится в DocumentClassобъекте. Или, в более общем смысле, эта логика находится в объекте, который «владеет» Button101объектом.

Теперь объектом, который владеет Button101объектом, может быть практически любой объект, для которого требуется кнопка. Затем за кнопкой следует конкретный конечный результат, необходимый при нажатии кнопки. И это ответственность за объект, которому нужна кнопка, а не сама кнопка.

Итак, в оригинальном и лучшем решении мы имеем один объект, который составляет другой объект. Составляемый объект является довольно автономным, но предоставляет определенные общедоступные свойства и методы, с помощью которых он может быть настроен в определенной степени. Объект, выполняющий компоновку, находится под контролем и манипулирует составным объектом через его общедоступные свойства и методы.

Эти отношения являются основным строительным блоком более крупных приложений и фокусом оставшейся части этого урока.


Чтобы обеспечить двустороннюю связь между классами, нам нужно начать думать с точки зрения иерархии композиции. Как правило, если объект A составляет объект B, то обратное явно не соответствует действительности. Я должен повторить, что, естественно, есть исключения из этого правила. Но эта конкретная методология хорошо работает в подавляющем большинстве случаев и хорошо вписывается в встроенную систему событий в ActionScript 3.

Подумай об этом так. Ваш SWF начинается с объекта документа. Этот объект, помимо прочего, будет состоять из других объектов, таких как MovieClip, или объектов, представляющих MovieClip. Может даже существовать объект, представляющий коллекцию миниатюр или сетку кнопок. Объект может «принадлежать» объекту документа («принадлежат», я имею в виду, что объект документа составляет объект коллекции большого пальца). Теперь объект коллекции большого пальца может составлять несколько объектов миниатюр, которые сами состоят из Loaderобъекта и TextFieldобъекта.

Теперь, имеет ли смысл для объекта коллекции большого пальца составлять объект документа? Или для отдельных миниатюр для создания объекта коллекции большого пальца? Возможно ли даже для встроенного класса Loaderсоздать собственный класс Thumbnail?

Иерархия композиции начинает становиться очевидной. Вы заметите, что существует не только структура того, как вещи составлены, но и сами объекты начинаются с самого грандиозного (объект документа) и прокладывают себе путь к более и более конкретным и сфокусированным объектам (таким как объект миниатюр, или основной строительный блок Loaderобъекта).

Мы можем проиллюстрировать эту иерархию следующим образом:

Гипотетическая иерархия, иллюстрированная

На диаграмме выше объекты представлены прямоугольниками с именами классов в них. Один объект составляет другой, если соединительная линия заканчивается ромбовидной формой на конце композиции . То есть, на приведенном выше рисунке, DocumentClassсоставляет Thumbnail, и, Thumbnailв свою очередь, составляет Loaderи TextField.


ActionScript 3 в значительной степени основан на идее диспетчеризации и прослушивания событий. Я не буду вдаваться в основные события здесь; Вы можете прочитать этот учебник ActiveTuts для получения дополнительной информации, если вам это нужно. События являются отличным способом обеспечения связи (это почти то, о чем они). Отправка события от одного объекта позволяет практически любому другому объекту действовать в ответ на него.

Однако обратите внимание на разницу между выполнением открытых методов и обработкой события . Например, представьте объект A и объект B. Объект A может выполнять любой из открытых методов объекта B, и это одна из форм связи. Если, с другой стороны, Объект A отправляет и Событие, для которого Объект B прослушивает, то это еще одна форма связи. Но обратите внимание на разницу. Первый активен : объект A напрямую манипулирует объектом B. Второй пассивен : объект A просто отправляет событие. Объект B может даже не прослушивать, и если это так, Объект A не имеет никакого контроля над тем, что Объект B может сделать в результате.


Итак, у нас есть иерархия композиции. Обычно объект документа составляет объект коллекции большого пальца. И из-за этой связи объект документа получает возможность выполнять публичные методы, непосредственно манипулируя / активно взаимодействуя с объектом коллекции большого пальца. Но коллекция большого пальца не знает об объекте, который «владеет» им. В идеале, он должен быть написан для повторного использования, чтобы позволить его использовать любому другому объекту, который хочет использовать коллекцию большого пальца. Поэтому мы не можем привязать его к какому-либо конкретному объекту документа.

Но мы хотим, чтобы объект документа удалил свой предварительный загрузчик, когда вся коллекция большого пальца загружена; как коллекция большого пальца взаимодействует с объектом документа, если он не составляет его? Как вы уже догадались, это происходит через события. Коллекция большого пальца связывается косвенно / пассивно с объектом документа. На самом деле, это немного вводит в заблуждение, говоря, что он «связывается с объектом документа». Это действительно просто говорит в пустоту: «Мои изображения загружены, на случай, если кому-то все равно». И, к счастью, есть кто-то, кто слушает в пустоте, и им все равно, потому что теперь они уберут свой предзагрузчик.

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

Теперь, пожалуй, наиболее распространенной формой двусторонней связи, которая не является только что описанным методом, является способность публичного метода возвращать значение. Я бы сказал, что это не столько двусторонняя связь, сколько способ извлечения информации из другого объекта. То есть, если Объект A вызывает метод для Объекта B и получает возвращаемое значение, тогда Объекту B может быть сказано что-то сделать, но в действительности объект A не должен делать что-либо в ответ на получение значения обратно. Это раскалывает волосы, и хотя этот маневр ценен, мы здесь не фокусируемся.

Мы на мгновение разберем это в более практичном подходе, но помните об этой концепции при создании нашего средства просмотра изображений.


Давайте засучить наши рукава. Перво-наперво, давайте рассмотрим наш проект. Последний пример показан в верхней части статьи, поэтому, если вы забудете, вернитесь туда на мгновение. Этот шаг будет о определении наших потребностей и разделении наших объектов.

Во-первых, давайте запишем цели

  • Показать группу миниатюр изображений
  • При нажатии на уменьшенное изображение отобразится увеличенная версия изображения.
  • Это также приведет к появлению «выбранного» состояния на миниатюре
  • Нажатие на кнопку «следующий» или «предыдущий» приведет к отображению соответствующего изображения в большей области, а также к выбору соответствующего эскиза.
  • Если пользователь находится на первом изображении в последовательности, кнопка «предыдущий» будет отключена и будет отображаться как таковая. Аналогичное действие со «следующей кнопкой» в конце последовательности.
  • Нажатие на увеличенное изображение приводит к ссылке на исходное изображение на Flickr
  • Управляйте отображаемыми изображениями и связанными данными с помощью документа XML, загружаемого во время выполнения.

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

Класс документов
«Работает» в SWF. Собирает все вместе в рабочее приложение

ThumbnailCollection
Управляет группой Thumbnailобъектов. Отвечает за создание и верстку отдельных миниатюр на основе входящих данных. Также отвечает за выбор миниатюр.

Миниатюра
Индивидуальный эскиз. Действует как кнопка и загружает и отображает изображение. Также может отображать выбранное состояние, которое можно включить или отключить с помощью открытых методов.

DetailImage
Загружает увеличенное изображение. Можно щелкнуть и отправить это событие.

Пагинация
Контролирует их PaginationButtons, повторно отправляя их CLICKсобытия и управляя, когда они включены и отключены.

PaginationButton Отдельная
кнопка «следующий» или «предыдущий» вариант. В основном просто кнопка, хотя их можно включать и отключать, и обновлять их внешний вид соответственно.

DataLoader
Все остальные классы, упомянутые до сих пор, были визуальными; этого нет. В его обязанности входит загрузка XML-файла, его анализ и извлечение соответствующих данных по запросу. Этот вид класса обычно кажется более чуждым начинающим объектно-ориентированным программистам, но он не более и не менее полезен, чем визуальная сторона вещей. Это включение здесь частично иллюстрирует этот момент.

ImageData
Объект-значение, единственная цель которого — переносить биты данных, относящиеся к одному изображению («Объект-значение» — это просто объект с, как правило, набором свойств и несколькими, если таковые имеются, методами. Цель состоит в том, чтобы собрать несколько битов информация — ценности — в единый объект).

ImageChangeEvent
Нам понадобится собственный класс события, чтобы упаковать некоторую полезную информацию вместе с событием. Событие, в частности, будет, когда изображение должно измениться из-за какого-то взаимодействия.

Имея это в виду, давайте начнем.


У нас есть хорошее представление о вовлеченных классах, поэтому мы можем успешно создать проект. Я подробно опишу процесс, используя файловую систему и ОС компьютера, но если вам удобен хороший проектно-ориентированный редактор кода, такой как Flash Builder, FlashDevelop или TextMate, не стесняйтесь создавать проект с помощью этих инструментов.

Сначала создайте папку проекта. Я позвоню в мою программу просмотра изображений .

Внутри этой папки создайте еще две папки, » src » и » bin «. «src» — это место, куда мы помещаем исходные файлы, такие как FLA-файлы, AS-файлы и все остальное, что не нужно развертывать для запуска конечного продукта. «bin» — это место, где размещаются различные файлы, такие как SWF-файлы, XML-файлы и файлы изображений, загружаемые SWF-файлом.

Внутри папки src создайте еще две папки, « flas » и « classes ». У нас будет только один FLA-файл, но мы будем стремиться организовать наш проект, сохранив FLA-файл отдельно от AS-файлов.

Далее мы создадим несколько папок в классах, которые будут нашей структурой пакета. Создайте эту структуру внутри классов :

1
2
3
4
5
6
7
8
[source="text"]
classes/
    data/
    events/
    ui/
        detail/
        pagination/
        thumbs/

И, наконец, создайте еще одну папку в bin, чтобы сохранить ее организованность. Нам нужны папки для XML и изображений, в частности.

1
2
3
4
5
6
[source="text"]
bin/
    images/
        thumbs/
        detail/
    data/

Окончательная структура папок должна выглядеть следующим образом:

Структура папок всего проекта

Теперь создайте файл Flash (конечно, ActionScript 3.0) и сохраните его в папке src \ flas . Я звоню моему ImageViewer.fla . Кроме того, вы можете начать с FLA-файла, предоставленного в пакете загрузки, в image_viewier_start . В нем есть графика для различных элементов, поэтому вам не нужно тратить время на рисование кнопок и тому подобное.

Даже со стартовым FLA нам нужно настроить несколько вещей.

Выберите пункт меню « Файл»> «Параметры публикации» . Убедитесь, что выбрана вкладка « Форматы », и выберите имя для своего SWF. Более важно, однако, это установить путь. Мы хотим пойти в мусорное ведро , поэтому введите ../../bin/ImageViewer.swf.

Затем нажмите на вкладку Flash , а затем на кнопку « Настройки…» рядом с версией ActionScript. Здесь убедитесь, что выбрана вкладка « Исходный путь », и нажмите кнопку « .

В полученной записи введите ../classes/.

Установка относительного пути для путей публикации и исходных путей документов делает проект более переносимым. Не поддавайтесь искушению нажать на кнопку «цель», так как это приведет к абсолютному пути к папке классов. Это значительно затрудняет перемещение проекта в новое место на жестком диске или передачу кому-то другому для работы на другом компьютере.


Создайте новый текстовый файл и сохраните его как ImageViewer.as в [project]/src/classes/ui. Добавьте следующее в файл:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
package ui {
 
    import flash.display.*;
    import flash.events.*;
 
    public class ImageViewer extends Sprite {
 
        public function ImageViewer() {
            trace("Ready.")
        }
 
    }
 
}

Обратите внимание, что нам нужно заполнить пакет в классе, поскольку мы технически в uiпакете. Если вы пропустили его, помните, что мы не помещаем его classesв пакет, поскольку classesпапка является нашим исходным путем. Мы начинаем там, но мы не включаем его как часть пакета.

В вашем Flash-файле убедитесь, что ничего не выбрано (нажмите Command-Shift-A) и откройте окно свойств. Где написано Class , введите:

1
2
[source="text"]
ui.ImageViewer

Помните, как использование пакетов делает наше полное имя класса длиннее? Нам нужно полное имя в поле класса документа, поэтому нам нужно добавить к имени короткое имя ( ImageViewer).

Теперь, если все пойдет хорошо, мы сможем проверить фильм на этом этапе и увидеть след «Готово». на панели «Вывод»:

Панель «Вывод» говорит, что она готова

На данный момент у нас есть проект, над которым нужно работать, и у нас есть класс документа, подключенный к FLA. Скоро мы добавим больше классов, но сначала давайте подготовим наши загружаемые ресурсы.


Вы можете найти несколько изображений, готовых к использованию в пакете загрузки. Вы найдете их в image_viewer / bin / images / . Вы можете просмотреть файлы, если хотите, и вы, вероятно, найдете их предсказуемо организованными. В одной папке находятся изображения размером с миниатюру, а в другой папке — более крупные версии тех же изображений. Поместите маленькие изображения в вашем проекте bin/images/thumbs/и большие изображения в bin/images/detail/.

Все изображения взяты из Flickr и лицензированы Creative Commons. Атрибуты можно найти на следующем шаге, а также в окончательном проекте.

Если вы хотите использовать свои собственные изображения, увеличьте размер миниатюр до 100×100, а детали — до 400×300 и просто поместите их в соответствующие папки.


Теперь мы будем каталогизировать эти изображения в XML-документе. Flash использует этот файл и анализирует нужные ему данные. Структура этого файла важна для следования вместе с остальной частью учебника. Однако, если вы использовали свои собственные изображения, убедитесь, что правильно настроили содержимое XML. Формат не сложный, надеюсь, вы можете сделать замены самостоятельно.

Создайте новый текстовый файл и сохраните его как images.xml в bin/data/.

Наша структура XML в основном представляет собой единый список элементов изображения, отдельный узел которого выглядит следующим образом:

1
<image name="flickr.jpg" attribution="Photo by Flickr, available under an attribution license" link="http://www.flickr.com" />

Отдельное изображение имеет имя изображения, атрибуцию и исходную веб-страницу. Кажется разумным, если вам не интересно, как мы загружаем изображение только из имени изображения. Чтобы сократить объем ввода, мы добавим несколько атрибутов в корневой узел, чтобы определить пути к папкам с превью и сведениями, например:

1
<images thumbPath="images/thumbs/" detailPath="images/detail/">

Что это делает в три раза:

  1. Нам не нужно будет указывать полный путь к изображению для каждого изображения, потому что мы предполагаем, что все изображения находятся в одном каталоге.
  2. Нам не нужно указывать отдельные пути для миниатюры и деталей, и вместо этого мы полагаемся на две версии, имеющие
    одинаковое имя файла, просто находящиеся в разных каталогах.
  3. При желании мы можем легко настроить расположение папок с изображениями.

Вот полный документ XML, заполненный данными, относящимися к изображениям в пакете загрузки:

1
2
3
4
5
6
7
8
<images thumbPath="images/thumbs/" detailPath="images/detail/">
    <image name="light-reading.jpg" attribution="Photo by MikeSchinkel, available under a Creative Commons Attribution license" link="http://www.flickr.com/photos/mikeschinkel/2680776058/" />
    <image name="glocal-similarity-map.jpg" attribution="Photo by blprnt_van, available under a Creative Commons Attribution license" link="http://www.flickr.com/photos/blprnt/3189182327/" />
    <image name="brimelows-bathroom.jpg" attribution="Photo by LeeBrimelow, available under a Creative Commons Attribution license" link="http://www.flickr.com/photos/brimelow/71209993/" />
    <image name="blue-cubes.jpg" attribution="Photo by frankdouwes, available under a Creative Commons Attribution license" link="http://www.flickr.com/photos/frankdouwes/4015001782/" />
    <image name="right-to-left.jpg" attribution="Photo by modern_carpentry, available under a Creative Commons Attribution license" link="http://www.flickr.com/photos/modern_carpentry/3705876429/" />
    <image name="recursive3.jpg" attribution="Photo by flavouz, available under a Creative Commons Attribution license" link="http://www.flickr.com/photos/flavouz/503682800/" />
</images>

Мы начинаем прямо с нашего первого класса без документов, и это также не визуальный класс. Начните с создания нового текстового документа и сохранения его как DataLoader.as в src/classes/data/.

Теперь, поскольку это не визуальный класс, определенно нет необходимости расширять Spriteили MovieClip. Однако нам нужно отправлять события, поэтому будет полезно расширять их EventDispatcher. Вот наш первый проход в коде, который является умеренно сложным. Отдельные методы, используемые здесь, не должны быть новыми для вас, поэтому я не буду откладывать загрузку и обработку XML. Однако после кода есть краткое прохождение.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package data {
 
    import flash.events.*;
    import flash.net.*;
 
    public class DataLoader extends EventDispatcher {
 
        private var _xml:XML;
 
        public function DataLoader(src:String) {
            var loader:URLLoader = new URLLoader(new URLRequest(src));
            loader.addEventListener(Event.COMPLETE, onXmlLoad);
            loader.addEventListener(IOErrorEvent.IO_ERROR, onXmlError);
        }
 
        private function onXmlLoad(e:Event):void {
            _xml = new XML(e.target.data);
            dispatchEvent(new Event(Event.COMPLETE));
        }
 
        private function onXmlError(e:IOErrorEvent):void {
            trace("There was a problem loading the XML file: " + e.text);
        }
 
    }
 
}

Короче говоря, DataLoaderобъект загружает файл XML и сохраняет файл как XML после загрузки. Чтобы загрузить его, нам нужно передать URL в документ XML конструктору. Начинается загрузка, и преобразователь необработанного текста в объект XML обрабатывается COMPLETEслушателем. Существует также простой обработчик ошибок, который будет выводить некоторую полезную информацию на панель «Вывод», но более всего предотвращает прерывание программы программой IOError в случае чего-то плохого.


Создав наш DataLoaderкласс (даже если он еще не закончен), мы можем его использовать. Это хороший способ работы: начните с минимальной программы (такой как FLA и почти пустой класс документа), добавьте один едва работающий объект, убедитесь, что он работает, добавьте функциональность, протестируйте, повторите. Не печатая слишком много, мы можем минимизировать ошибки. Наши частые тесты делают более очевидным, где были введены ошибки, и, начиная с нулевой функциональности, мы органично прогрессируем в сложности.

В любом случае, вернитесь к классу ImageViewer и попросите его создать и использовать DataLoader:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package ui {
 
    import flash.display.*;
    import flash.events.*;
 
    import data.DataLoader;
 
    public class ImageViewer extends Sprite {
 
        private var _imageData:DataLoader;
 
        public function ImageViewer() {
            _imageData = new DataLoader("data/images.xml");
            _imageData.addEventListener(Event.COMPLETE, onImageDataLoad);
        }
 
        private function onImageDataLoad(e:Event):void {
            trace("Data loaded.");
        }
 
    }
 
}

Протестируйте его, и вы увидите единственную трассировку, указывающую, что данные закончили загрузку.

Данные загружены.

Теперь давайте посмотрим на выбор, который мы сделали при разработке вещей до сих пор.

Во-первых, мы решили, чтобы DataLoader содержал свой сырой XML в частном свойстве, и мы не написали для него открытый метод получения. Причины этого заключаются в инкапсуляции необработанных данных. Наши дальнейшие шаги будут включать в себя написание некоторых специализированных методов для доступа к данным, поэтому мы продолжим это обсуждение, но суть в том, что мы хотим, чтобы объект отвечал за обслуживание данных. Ограничивая публичный доступ к необработанному XML, мы можем контролировать доступ к данным. Не только это, но и написание методов для извлечения данных, мы можем — на более позднем этапе — изменить базовый механизм структурирования данных без необходимости обновления нескольких файлов классов в ответ на это изменение. Нам нужно только обновить DataLoaderкласс.

Во-вторых, у нас есть DataLoaderдескриптор его собственных событий ошибок. Теперь, на самом деле, если бы ошибки были реальной проблемой (например, если вы загружали XML из другого домена, который может быть недоступен вне вашего контроля), вы бы хотели обрабатывать вещи немного более тщательно. Но для наших целей мы уверены, что XML будет загружаться. В случае, если это не так (скажем, мы неправильно написали имя файла), мы получили бы событие ошибки. Если событие ошибки не обрабатывается (то есть, нет перехватчика для URLLoader«S IOErrorEvent.IO_ERRORсобытий), то ошибка во время выполнения запускается. Подключив что-то , мы можем, по крайней мере, предотвратить срыв выполнения программы. Но дело в том, что мы обрабатываем это событие внутренне, так что когда мы создаемDataLoaderв классе документа нам не нужно беспокоиться об этой ошибке. DataLoaderНемного больше черного ящика, герметизирующего внутренний механизм нагрузки.

Наконец, мы решили отправить COMPLETEсобытие после загрузки файла XML и его преобразования в объект XML. Это делает две вещи для нас. Во- первых, диспетчерская наше собственное мероприятие, в отличие от позволяя класс документа послушай непосредственно к URLLoader«s COMPLETEсобытие, мы можем гарантировать , что событие наше COMPLETE мероприятие. То есть недостаточно, чтобы внешний файл загрузился. Мы также хотим убедиться, что у нас есть экземпляр объекта XML. Поэтому мы отправляем событие, когда будем готовы, после сохранения XML.

Более важной вещью, которую выполняет COMPLETEсобытие, является возможность двустороннего общения. Это будет работать так: класс документа создает новый DataLoader, эффективно взаимодействующий с ним, чтобы загрузить определенный файл. Когда файл загружен, DataLoaderдиспетчеризует COMPLETEсобытие, в некотором смысле связываясь с классом документа, чтобы он продолжил свою остальную логику.

Но это тонкий момент, и я продолжу указывать на него: в то время как класс документа взаимодействует напрямую DataLoader, давая ему определенный файл для загрузки, он DataLoaderпросто отправляет событие. Класс документа может или не может прослушивать, насколько DataLoaderэто касается. На данный момент, DataLoaderможет быть использован в совершенно другом проекте, при условии, что мы хотим использовать XML-файл с той же структурой.

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

До сих пор иерархия композиции

Не забывайте, однако, что даже в этот момент есть еще один уровень композиции, как DataLoaderсостовляющие URLLoaderи XMLобъекты.


Давайте дадим DataLoaderспособ раскрыть основные данные. Однако, как обсуждалось на предыдущем шаге, мы не будем раскрывать объект XML и вместо этого создадим методы доступа к данным по мере необходимости.

В DataLoader добавьте следующий метод:

1
2
3
4
5
6
7
8
9
public function getThumbnailPaths():Array {
    var paths:Array = [];
    var srcList:XMLList = _xml.image.@name;
    var thumbPath:String = _xml.@thumbPath;
    for each (var src:String in srcList) {
        paths.push(thumbPath + src);
    }
    return paths;
}

Если вы не знакомы со стандартным синтаксическим анализом XML с помощью E4X, возможно, вы захотите в этом разобраться , так как мы будем делать подобные вещи на следующих шагах.

Я хочу сосредоточиться на том, что мы создали метод, который будет анализировать XML для нас и просто возвращать список путей к миниатюрным изображениям. Это разделяет обязанности объектов: класс документа просто захочет получить этот список путей, и он не будет погружен в детали разбора XML для этой информации. И наоборот, этот DataLoaderкласс полностью посвящен деталям структуры XML и квалифицирован для ее анализа; Итак, давайте разместим логику там, где она принадлежит.


Вернемся в ImageViewer , давайте использовать тот getThumbnailPathsметод, который мы только что создали. Измените onImageDataLoadметод так, чтобы он выглядел так:

1
2
3
4
private function onImageDataLoad(e:Event):void {
    var thumbPaths:Array = _imageData.getThumbnailPaths();
    trace(thumbPaths.join("\n"));
}

Пока это не добавляет много функциональности, но распечатывает что-то более интересное, чем раньше. Вы должны увидеть список полных путей к миниатюрным изображениям:

Список путей к изображениям

Это показывает, что наш getThumbnailPathsметод работает, а также что мы используем составной объект, напрямую вызывая методы для него.


Нашим следующим шагом будет передача этих thumbPathsданных ThumbnailCollectionклассу, чтобы он мог затем выполнять свою работу по управлению несколькими отдельными Thumbnailобъектами. Очевидно, чтобы заставить его работать, нам нужно развивать Thumbnailкласс одновременно. Мы начнем с Thumbnailкласса, а затем «подключим» класс документа к Thumbnailобъектам через ThumbnailCollectionобъект.

Имейте в виду, что это на самом деле не работает; класс документа не будет знать об этом человеке, Thumbnailsпотому что он будет инкапсулирован в ThumbnailCollection. И наоборот, потому что мы будем отправлять события из Thumbnailв ThumbnailCollection, которые затем будут пересылать события по мере необходимости обратно в класс документа.

Имея это в виду, давайте построим Thumbnailкласс. Создайте новый текстовый файл и сохраните его как Thumbnail.as в classes/ui/thumbs/. Добавьте следующий код в класс:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package ui.thumbs {
 
    import flash.display.*;
    import flash.events.*;
    import flash.geom.*;
    import flash.net.*;
 
    public class Thumbnail extends EventDispatcher {
 
        private var _container:ThumbnailGraphic;
        private var _loader:Loader;
        private var _highlight:Sprite;
 
        public function Thumbnail() {
            _container = new ThumbnailGraphic();
            _container.addEventListener(MouseEvent.ROLL_OVER, onOver);
            _container.addEventListener(MouseEvent.ROLL_OUT, onOut);
            _container.mouseChildren = false;
            _container.buttonMode = true;
 
            _loader = new Loader();
            _loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onComplete);
            _loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onError);
 
            _highlight = _container.getChildByName("highlight_mc") as Sprite;
            _highlight.alpha = 0;
 
            var highlightDepth = _container.getChildIndex(_highlight);
            _container.addChildAt(_loader, highlightDepth);
        }
 
        public function load(url:String):void {
            _loader.load(new URLRequest(url));
        }
 
        public function select():void {
            _container.removeEventListener(MouseEvent.ROLL_OVER, onOver);
            _container.removeEventListener(MouseEvent.ROLL_OUT, onOut);
            _container.mouseEnabled = false;
            _highlight.alpha = .8;
        }
 
        public function deselect():void {
            _container.addEventListener(MouseEvent.ROLL_OVER, onOver);
            _container.addEventListener(MouseEvent.ROLL_OUT, onOut);
            _container.mouseEnabled = true;
            _highlight.alpha = 0;
        }
 
        private function onComplete(e:Event):void {
            var x:Number = (_loader.width - 100) / 2;
            var y:Number = (_loader.height - 100) / 2;
            _loader.scrollRect = new Rectangle(x, y, 100, 100);
            dispatchEvent(new Event(Event.COMPLETE));
        }
 
        private function onError(e:IOErrorEvent):void {
            trace("Problem loading this thumbnail image: " + _loader.contentLoaderInfo.url);
        }
 
        private function onOver(e:MouseEvent):void {
            _highlight.alpha = .4;
        }
 
        private function onOut(e:MouseEvent):void {
            _highlight.alpha = 0;
        }
 
        public function get container():ThumbnailGraphic {
            return _container;
        }
 
    }
 
}

В целом класс умеренно сложный, но ничего страшного. Несколько разъяснений:

ThumbnailGraphicКласс, если вы не заметили еще, класс обеспечивается FLA в качестве символа, экспортированного для ActionScript. Таким образом, для него нет файла класса (хотя, возможно, мы могли бы на самом деле сделать экспортируемый класс Thumbnailклассом, но наше внимание сосредоточено на композиции, а не на расширении, которое DisplayObjectмы создаем).

Мы используем Loaderобъект для обработки загрузки и отображения изображения. Мы обрабатываем события завершения и ошибки. Событие ошибки является основным следом. Полный обработчик немного сложнее. Тем не менее, это просто отображение загруженного изображения в квадрате 100 x 100. Если изображение больше этого, то в результате вычислений scrollRectбудет обрезано изображение до центра. Если он меньше, изображение будет отцентрировано. Это не является заменой неправильной обрезки миниатюрных изображений, но помогает предотвратить неприглядное кровотечение в случае ошибки пользователя при предоставлении изображений.

Наведите курсор мыши достаточно просто, используя выделенную рамку внутри символа Flash. Не стесняйтесь ставить его здесь и реализовать анимацию, используя TweenMax или Tweener или пакет анимации по вашему выбору. Анимации всегда добавляют приятное ощущение большинству пьес, но я уберу касательную для своих целей.

CLICKСобытие было зацепило, но мы оставим логику слушателя до поры до времени.

selectИ deselectметоды привыкнет немного дальше вниз по линии. Но мы устанавливаем их здесь. Идея состоит в том, что выделенное Thumbnailбудет иметь выделение (чуть более «твердое» из альфа-настройки), но в то же время не будет интерактивным. Это предотвратит щелчки по одному и тому же Thumbnailподряд и предотвратит неожиданное действие выделения. deselectпросто меняет все, что делает Thumbnailвозражение выбранным.

Есть containerсвойство для свойства, которое является экземпляром ThumbnailGraphicсимвола. Это обеспечивает доступ к экранному объекту, который необходимо расположить и добавить на сцену. Будучи публичной функцией, мы увидим это снова на следующем шаге, когда будем писать ThumbnailCollectionкласс.

Мы вернемся к этому классу позже, но сейчас давайте обратим наше внимание на ThumbnailCollectionкласс.


ThumbnailCollectionКласс, опять же , отвечает за управление несколькими Thumbnailобъектами. Создайте другой текстовый файл и сохраните его как ThumbnailCollection.as в /classes/ui/thumbs. Вот наш начальный код для вставки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package ui.thumbs {
 
    import flash.display.*;
    import flash.events.*;
 
    public class ThumbnailCollection extends EventDispatcher {
 
        private var _container:Sprite;
        private var _thumbnails:Array;
 
        public function ThumbnailCollection():void {
            _container = new Sprite();
            _thumbnails = new Array();
        }
 
        public function addThumbAt(url:String, index:uint):void {
            var thumb:Thumbnail = new Thumbnail();
            thumb.load(url);
            _container.addChild(thumb.container);
            thumb.container.x = 100 * index;
            _thumbnails[index] = thumb;
        }
 
        public function get container():Sprite {
            return _container;
        }
 
    }
 
}

Вы можете думать об этом классе как о прославленном массиве. Его основная функция — хранить коллекцию Thumbnailобъектов (отсюда и название класса), и это делается в массиве. Но это также защищает этот массив. Если мы используем ThumbnailCollectionобъект (и мы сделаем это на следующем шаге), то нам нужно верить, что он выполнит свою работу по управлению коллекцией, и, таким образом, у нас нет прямого доступа к этому массиву. Мы можем добавлять элементы в массив косвенно, через addThumbAt, но это все. Не говоря уже о том, что этот метод также делает кучу других вещей для создания Thumbnail.


Давайте сложим все это вместе и посмотрим, что мы можем сделать. Вернувшись в ImageViewer , мы создадим ThumbnailCollectionэкземпляр и заполним его, используя список путей из большого пальца DataLoader.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package ui {
 
    import flash.display.*;
    import flash.events.*;
 
    import data.DataLoader;
    import ui.thumbs.ThumbnailCollection;
 
    public class ImageViewer extends Sprite {
 
        private var _imageData:DataLoader;
        private var _thumbnails:ThumbnailCollection;
 
        public function ImageViewer() {
            _imageData = new DataLoader("data/images.xml");
            _imageData.addEventListener(Event.COMPLETE, onImageDataLoad);
 
            _thumbnails = new ThumbnailCollection();
            addChild(_thumbnails.container);
            _thumbnails.container.y = 300;
        }
 
        private function onImageDataLoad(e:Event):void {
            var thumbPaths:Array = _imageData.getThumbnailPaths();
            for (var i:uint = 0; i < thumbPaths.length; i++) {
                _thumbnails.addThumbAt(thumbPaths[i], i);
            }
        }
 
    }
 
}

На этом этапе наша иерархия композиций выглядит следующим образом (линия с ромбом означает, что класс на стороне ромбов составляет класс на другом конце линии):

Составная иерархия до сих пор

Обратите внимание, что ImageViewerсоставление ThumbnailCollection, и, следовательно, вызывает методы непосредственно в. Но также обратите внимание, что DataLoaderи ThumbnailCollectionполностью независимы друг от друга (успешная инкапсуляция), но могут сотрудничать через посредничество ImageViewer. Опять же, объекты более высокого уровня, как правило, составляют более сфокусированные объекты и, следовательно, обладают знаниями о них. Другая сторона медали, тем не менее, состоит в том, что более сфокусированные объекты, как правило, ничего не знают об объектах, составляющих их, и вместо этого предоставляют услугу диспетчеризации событий.

Хорошо, на данный момент, мы должны быть в состоянии заставить его работать! Протестируйте фильм, и вместо следов мы увидим, что некоторые изображения появляются вдоль нижней части SWF. Вы также должны иметь возможность взаимодействовать с миниатюрами, чтобы подсветка отображалась при наведении мыши.

Миниатюры загружены, отображаются и интерактивные

Мы собираемся обработать щелчок на Thumbnailобъекте. Индивидуум CLICKдолжен быть обнаружен Thumbnailсам по себе, а затем отправлен на ThumbnailCollectionобъект. Оттуда он определяет индекс, по Thumbnailкоторому щелкнули, и повторно отправляет пользовательское событие. ImageViewerбудет прослушивать это событие и использовать его, чтобы определить, что будет дальше.

Давайте начнем с пользовательского класса событий. Это будет стандарт Event, только у него есть место для valueсобственности. Это будет определяться ThumbnailCollectionи использоваться ImageViewer. Наше пользовательское событие также определит некоторые пользовательские типы событий.

Создайте еще один новый текстовый файл и сохраните его как ImageChangeEvent в classes/events/. Это будет типичный Eventподкласс, о котором мы не говорили конкретно до сих пор. Полное обсуждение после кода:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package events {
 
    import flash.events.*;
 
    public class ImageChangeEvent extends Event {
 
        public static const INDEX_CHANGE:String = "indexChange";
 
        private var _value:int;
 
        public function ImageChangeEvent(type:String, value:int, bubbles:Boolean=true, cancelable:Boolean=false) {
            _value = value;
            super(type, bubbles, cancelable);
        }
 
        override public function clone():Event {
            return new ImageChangeEvent(type, value, bubbles, cancelable);
        }
 
        override public function toString():String {
            return formatToString("ImageChangeEvent", "type", "value");
        }
 
        public function get value():int {
            return _value;
        }
 
    }
 
}

Во-первых, этот класс находится в eventsпакете. Это не обязательно, но обычно размещать ваши пользовательские события в eventsпакете. Это позволяет им группироваться вместе, а также концептуально отделять их от других классов. Настраиваемое событие, как и обычное событие, должно быть чрезвычайно легким и не зависеть от слишком большого количества, чтобы выполнить свою работу. Помещение событий в их собственный пакет усиливает представление о том, что это событие может быть технически отправлено из любого объекта в проекте, а не только, скажем, из объектов в thumbsпакете (это, кстати, предвещает. Мы будем отправь это событие снова, запомни мои слова).

Далее нам нужно импортировать пакет встроенных событий, в частности Event, потому что мы расширяем Event. Мы используем наследование здесь, потому что у нас есть только один выбор. Чтобы создать пользовательское событие, мы должны создать подкласс Event. Просто так, как оно идет.

Первые несколько строк самого класса являются свойствами. Существует открытая статическая константа, которая определяет Stringтип события. Наше событие на самом деле пока определяет только этот один тип, но нередко перечислять несколько таких типов здесь. Это дает нам тип события, похожий на ImageChangeEvent.INDEX_CHANGE, похожий на что-то вроде Event.COMPLETEили MouseEvent.CLICK. Другое свойство — это просто частная переменная экземпляра, в которой мы будем хранить значение индекса изображения, на которое мы меняемся.

Конструктор довольно типичен для Eventподкласса. Параметры type, bubblesи cancelableпросто передаются в супер конструктор. Нам просто нужно убедиться, что мы делаем это. Параметр является частью нестандартной работы, и в действительности весь смысл создания настраиваемого события value. Это наше собственное дополнение, поэтому мы сохраняем это в нашем свойстве экземпляра и передаем остальные параметры суперклассу.

Следующие два метода являются переопределениями методов в суперклассе. Это не обязательно необходимо, но это неплохая идея. cloneМетод обеспечивает легкий способ создать идентичную копию данного события. Как правило, это так же просто, как создать новый объект события, используя значения, сохраненные в исходном событии, и вернуть его. toStringМетод позволяет нам обеспечить информативный вывод , если событие никогда не получает прослежено. Мы используем метод суперкласса formatToStringи просто передаем имя класса и любые свойства, которые мы хотим включить в вывод. Мы увидим это в действии чуть позже.

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

Здесь нечего проверять, поэтому давайте перейдем к фактическому использованию этого объекта события.


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

Мы начнем с того места Thumbnail, где сначала прослушаем нормальное CLICKсобытие ThumbnailGraphic, а затем просто повторно отправим это событие.

Сначала добавьте CLICKпрослушиватель событий:

1
2
3
4
5
6
7
public function Thumbnail() {
    _container = new ThumbnailGraphic();
    _container.addEventListener(MouseEvent.ROLL_OVER, onOver);
    _container.addEventListener(MouseEvent.ROLL_OUT, onOut);
    _container.addEventListener(MouseEvent.CLICK, onClick);
    _container.mouseChildren = false;
    // …

Кроме того, принять его во внимание в selectи deselectметодов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public function select():void {
    _container.removeEventListener(MouseEvent.ROLL_OVER, onOver);
    _container.removeEventListener(MouseEvent.ROLL_OUT, onOut);
    _container.removeEventListener(MouseEvent.CLICK, onClick);
    _container.mouseEnabled = false;
    _highlight.alpha = .8;
}
 
public function deselect():void {
    _container.addEventListener(MouseEvent.ROLL_OVER, onOver);
    _container.addEventListener(MouseEvent.ROLL_OUT, onOut);
    _container.addEventListener(MouseEvent.CLICK, onClick);
    _container.mouseEnabled = true;
    _highlight.alpha = 0;
}

А затем напишите функцию слушателя:

1
2
3
private function onClick(e:MouseEvent):void {
    dispatchEvent(e);
}

Обращаясь к ThumbnailCollectionнам, мы должны убедиться, что мы прослушиваем CLICKсобытия для всех Thumbnails, а затем отправляем результат ImageChangeEventв результате, посмотрев на индекс вызывающего события Thumbnail.

Сначала импортируйте ImageChangeEvent:

1
import events.ImageChangeEvent;

Затем в addThumbAtметоде обязательно прослушайте CLICKсобытия:

1
2
3
4
5
6
7
8
public function addThumbAt(url:String, index:uint):void {
    var thumb:Thumbnail = new Thumbnail();
    thumb.load(url);
    _container.addChild(thumb);
    thumb.x = 100 * index;
    thumb.addEventListener(MouseEvent.CLICK, onThumbClick);
    _thumbnails[index] = thumb;
}

И напоследок напишите обработчик события. Этот бит может занять небольшое объяснение:

1
2
3
4
5
private function onThumbClick(e:MouseEvent):void {
    var thumb:Thumbnail = e.target as Thumbnail;
    var index:int = _thumbnails.indexOf(thumb);
    dispatchEvent(new ImageChangeEvent(ImageChangeEvent.INDEX_CHANGE, index));
}

Код на самом деле довольно прост, но вот суть: во-первых, возьмите цель события и приведите ее как Thumbnail. Это дает нам Thumbnailобъект, который был нажат. Затем найдите индекс в _thumbnailsмассиве этого объекта. indexOfМетод делает именно то, что: он возвращает индекс массива объекта , переданного в метод. Наконец, используя этот индекс, мы создаем новый, ImageChangeEventкоторый затем отправляем.

Последнее, что нужно сделать, и это слушать и обрабатывать ImageChangeEventс ImageViewer. В этом классе импортируйте ImageChangeEvent:

1
import events.ImageChangeEvent;

Затем нам нужно свойство экземпляра, чтобы сохранить выбранный индекс изображения (причины, которые будут понятны в ближайшем будущем):

1
private var _currentImageIndex:int;

А затем обновите код для прослушивания события в конструкторе:

1
2
3
4
5
6
7
8
9
public function ImageViewer() {
    _imageData = new DataLoader("data/images.xml");
    _imageData.addEventListener(Event.COMPLETE, onImageDataLoad);
 
    _thumbnails = new ThumbnailCollection();
    addChild(_thumbnails.container);
    _thumbnails.container.y = 300;
    _thumbnails.addEventListener(ImageChangeEvent.INDEX_CHANGE, onImageChange);
}

Наконец, нам нужно написать этот метод прослушивания событий. Мы перейдем к более интересному коду на следующем шаге, но сейчас давайте выпишем метод и проследим индекс:

1
2
3
4
private function onImageChange(e:ImageChangeEvent):void {
    _currentImageIndex = e.value;
    trace("image changed. index: " + _currentImageIndex);
}

Это было много редактирования, но фактический код был просто горсткой строк. Просто продолжайте говорить себе, что это все во имя иерархии композиции.

И мы должны быть в состоянии проверить это сейчас! Идите вперед и запустите фильм, и нажмите на миниатюры. Вы увидите индекс миниатюры, по которой вы щелкнули, на панели «Вывод».

Панель «Вывод» после нажатия на несколько миниатюр

Для справки, вот полный список кода для трех затронутых классов:

ImageViewer

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package ui {
 
    import flash.display.*;
    import flash.events.*;
 
    import data.DataLoader;
    import events.ImageChangeEvent;
    import ui.thumbs.ThumbnailCollection;
 
    public class ImageViewer extends Sprite {
 
        private var _imageData:DataLoader;
        private var _thumbnails:ThumbnailCollection;
 
        private var _currentImageIndex:int;
 
        public function ImageViewer() {
            _imageData = new DataLoader("data/images.xml");
            _imageData.addEventListener(Event.COMPLETE, onImageDataLoad);
 
            _thumbnails = new ThumbnailCollection();
            addChild(_thumbnails.container);
            _thumbnails.container.y = 300;
            _thumbnails.addEventListener(ImageChangeEvent.INDEX_CHANGE, onImageChange);
        }
 
        private function onImageDataLoad(e:Event):void {
            var thumbPaths:Array = _imageData.getThumbnailPaths();
            for (var i:uint = 0; i < thumbPaths.length; i++) {
                _thumbnails.addThumbAt(thumbPaths[i], i);
            }
        }
 
        private function onImageChange(e:ImageChangeEvent):void {
            _currentImageIndex = e.value;
            trace("image changed. index: " + _currentImageIndex);
        }
 
    }
 
}

ThumbnailCollection

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package ui.thumbs {
 
    import flash.display.*;
    import flash.events.*;
 
    import events.ImageChangeEvent;
 
    public class ThumbnailCollection extends EventDispatcher {
 
        private var _container:Sprite;
        private var _thumbnails:Array;
 
        public function ThumbnailCollection():void {
            _container = new Sprite();
            _thumbnails = new Array();
        }
 
        public function addThumbAt(url:String, index:uint):void {
            var thumb:Thumbnail = new Thumbnail();
            thumb.load(url);
            _container.addChild(thumb.container);
            thumb.container.x = 100 * index;
            thumb.addEventListener(MouseEvent.CLICK, onThumbClick);
            _thumbnails[index] = thumb;
        }
 
        public function get container():Sprite {
            return _container;
        }
 
        private function onThumbClick(e:MouseEvent):void {
            var thumb:Thumbnail = e.target as Thumbnail;
            var index:int = _thumbnails.indexOf(thumb);
            dispatchEvent(new ImageChangeEvent(ImageChangeEvent.INDEX_CHANGE, index));
        }
 
    }
 
}

Thumbnail

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package ui.thumbs {
 
    import flash.display.*;
    import flash.events.*;
    import flash.geom.*;
    import flash.net.*;
 
    public class Thumbnail extends EventDispatcher {
 
        private var _container:ThumbnailGraphic;
        private var _loader:Loader;
        private var _highlight:Sprite;
 
        public function Thumbnail() {
            _container = new ThumbnailGraphic();
            _container.addEventListener(MouseEvent.ROLL_OVER, onOver);
            _container.addEventListener(MouseEvent.ROLL_OUT, onOut);
            _container.addEventListener(MouseEvent.CLICK, onClick);
            _container.mouseChildren = false;
            _container.buttonMode = true;
 
            _loader = new Loader();
            _loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onComplete);
            _loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onError);
 
            _highlight = _container.getChildByName("highlight_mc") as Sprite;
            _highlight.alpha = 0;
 
            var highlightDepth = _container.getChildIndex(_highlight);
            _container.addChildAt(_loader, highlightDepth);
        }
 
        public function load(url:String):void {
            _loader.load(new URLRequest(url));
        }
 
        public function select():void {
            _container.removeEventListener(MouseEvent.ROLL_OVER, onOver);
            _container.removeEventListener(MouseEvent.ROLL_OUT, onOut);
            _container.removeEventListener(MouseEvent.CLICK, onClick);
            _container.mouseEnabled = false;
            _highlight.alpha = .8;
        }
 
        public function deselect():void {
            _container.addEventListener(MouseEvent.ROLL_OVER, onOver);
            _container.addEventListener(MouseEvent.ROLL_OUT, onOut);
            _container.addEventListener(MouseEvent.CLICK, onClick);
            _container.mouseEnabled = true;
            _highlight.alpha = 0;
        }
 
        private function onComplete(e:Event):void {
            var x:Number = (_loader.width - 100) / 2;
            var y:Number = (_loader.height - 100) / 2;
            _loader.scrollRect = new Rectangle(x, y, 100, 100);
            dispatchEvent(new Event(Event.COMPLETE));
        }
 
        private function onError(e:IOErrorEvent):void {
            trace("Problem loading this thumbnail image: " + _loader.contentLoaderInfo.url);
        }
 
        private function onOver(e:MouseEvent):void {
            _highlight.alpha = .4;
        }
 
        private function onOut(e:MouseEvent):void {
            _highlight.alpha = 0;
        }
 
        private function onClick(e:MouseEvent):void {
            dispatchEvent(e);
        }
 
        public function get container():ThumbnailGraphic {
            return _container;
        }
 
    }
 
}

Для того, чтобы получить данные изображения из DataLoader, было бы удобно , чтобы укутаться всю необходимую информацию (помните, узел содержит name, attributionи linkатрибуты) в одном объекте. Мы могли бы использовать обычный старый объект, например так:

1
{name:"name", attribution:"Attributed to John", link:"www.flickr.com"}

Но это учебник по объектно-ориентированному программированию, поэтому мы собираемся добавить еще один класс. Ответственность будет заключаться исключительно в том, чтобы содержать свойства, связанные с изображением. Это будет очень простой класс, но он предоставит нам строго типизированное значение, которое мы можем использовать для передачи этой информации.

Создайте новый текстовый файл и сохраните его как ImageData.asв classes/data/. Добавьте этот код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
package data {
 
    public class ImageData {
 
        public var source:String;
        public var attribution:String;
        public var link:String;
 
        public function ImageData(source:String, attribution:String, link:String) {
            this.source = source;
            this.attribution = attribution;
            this.link = link;
        }
 
    }
 
}

Класс существует для предоставления трех свойств. Это означает, что мы можем перенести данные из узла в XML на ImageDataобъект. Пока мы писали класс, мы также добавили удобство в конструктор: передайте три аргумента и получите те свойства, которые установлены для вас. Таким образом, мы можем создать ImageDataобъект одной строкой, например так:

1
new ImageData("path/to/image.jpg", "Attributed to John", "www.flickr.com");

Теперь, хотя это очень простой класс, зачем идти на неприятности, а не просто использовать обычный объект? Преимущество заключается в наборе данных и строгой проверке ошибок во время компиляции. Если у вас есть переменная, типизированная как Object, компилятор никак не может узнать, sourceявляется или не является допустимым свойством переменной или что оно является или не является String. Наш ImageDataкласс, однако, имеет только три допустимых свойства, каждое из которых имеет свой собственный тип данных. Компилятор будет ловить непреднамеренные злоупотребления объектом, вызывая ошибки.

Не только это, но и философски, мы можем мыслить с точки зрения объектов во всем приложении. Даже такая простая вещь, как «данные об изображении», может быть представлена ​​как объект, и, таким образом, мы можем создать класс именно для этой цели.


Ну, у нас есть индекс. Нам удалось получить индекс того, на Thumbnailкоторый нажали, но что теперь? Нам нужно вернуться к данным, чтобы получить больше информации, касающейся индекса. По сути, нам нужна вся информация — как ImageDataобъект — которая связана с имеющимся у нас индексом.

Откройте DataLoaderи добавьте этот метод:

1
2
3
4
5
6
7
8
public function getImageByIndex(index:int):ImageData {
    var imageNode:XML = _xml.image[index];
    if (imageNode) {
        return new ImageData(_xml.@detailPath + imageNode.@name, imageNode.@attribution, imageNode.@link);
    } else {
        return null;
    }
}

Помните, один узел выглядит так:

1
<image name="flickr.jpg" attribution="Photo by Flickr, available under an attribution license" link="http://www.flickr.com" />

Этот метод на самом деле довольно прост. Мы берем переданный индекс и используем его для поиска подходящего узла в данных XML. Затем мы берем этот узел и извлекаем из него атрибуты. С этими значениями мы создаем ImageDataобъект и возвращаем его. Однако обратите внимание на проверку, чтобы увидеть, существует ли узел; если мы передали индекс за пределами допустимого диапазона, у нас не будет действительных данных в XML, поэтому мы не сможем вернуть действительный ImageDataобъект. Это простая проверка, которая может предотвратить ошибки в будущем, в редких случаях, когда они нам понадобятся.

Поместите этот метод в тест, используя его в ImageViewer. Начните с импорта ImageDataкласса:

1
2
3
4
import data.DataLoader;
import data.ImageData;
import events.ImageChangeEvent;
import ui.thumbs.ThumbnailCollection;

В onImageChangeметоде мы можем удалить трассировку и запросить DataLoaderсоответствующую информацию.

1
2
3
4
5
6
7
8
9
private function onImageChange(e:ImageChangeEvent):void {
    _currentImageIndex = e.value;
    updateImage(_currentImageIndex);
}
 
private function updateImage(index:uint):void {
    var img:ImageData = _imageData.getImageByIndex(index);
    trace("image name: " + img.source);
}

Мы также извлечем логику для того, чтобы что-то сделать с этим индексом, в новый метод. На данный момент это может показаться излишним, но нам нужно будет сделать это позже, поэтому я использую свой хрустальный шар и экономлю немного накладных расходов, выполняя метод сейчас.

Протестируйте фильм, и вы должны увидеть соответствующий след при нажатии на миниатюры.

Панель «Вывод» после нажатия на несколько миниатюр

Теперь давайте обратим наше внимание на область детализации изображения. Напомним, что целью здесь является загрузка изображения, отображение атрибута и переход по URL-адресу при нажатии на изображение. Достаточно просто, но давайте удостоверимся, что мы подходим к нему с объектной ориентацией.

Создайте новый текстовый файл и сохраните его как DetailImage.asв classes/ui/detail/. Вот код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package ui.detail {
 
    import flash.display.*;
    import flash.events.*;
    import flash.net.*;
    import flash.text.*;
 
    public class DetailImage {
 
        private var _container:Sprite;
        private var _attributionField:TextField;
        private var _link:String;
        private var _loader:Loader;
 
        public function DetailImage(container:Sprite) {
            _container = container;
 
            _attributionField = _container.getChildByName("attributionField") as TextField;
            _loader = new Loader();
            _container.addChildAt(_loader, _container.getChildIndex(_attributionField));
 
            _container.mouseChildren = false;
            _container.buttonMode = true;
 
            _container.addEventListener(MouseEvent.CLICK, onClick);
        }
 
        private function onClick(e:MouseEvent):void {
            navigateToURL(new URLRequest(_link), "_blank");
        }
 
        public function setImage(src:String, attribution:String, link:String):void {
            _loader.load(new URLRequest(src));
            _attributionField.text = attribution;
            _link = link;
        }
 
    }
 
}

Теперь большая часть этого зависит от того, что мы передадим конкретный фрагмент ролика в конструктор. Код, управляющий этим битом пользовательского интерфейса, работает в предположении, что с ним Spriteсвязано определенное свойство в вызываемом свойстве _container. А именно, он ожидает, что будет TextFieldгде-то внутри так Spriteназываемого «attributionField».

Вот краткий обзор класса: после импорта и примера мы имеем четыре свойства. Это _containerвышеупомянутое Sprite, которое передается. Это будет Spriteна этапе FLA, и мы вернемся к этому снова на следующем шаге. _attributionFieldЯвляется ссылкой на TextFieldвнутренней стороне _container. Будет просто удобно иметь возможность ссылаться на него через переменную, а не всегда через getChildByName. _loaderэто Loaderобъект, который мы создаем для загрузки изображений, и _linkпросто пункт назначения клика.

Конструктор делает большую часть тяжелой работы. Он устанавливает большинство свойств и настраивает все Spriteкак кнопку. Чтобы округлить класс, у нас есть onClickметод, который является CLICKобработчиком, и просто переходит к URL-адресу, который в данный момент хранится в _link. И тогда есть setImage; это берет информацию, которая нам нужна, чтобы отобразить DetailImageкак уникальное изображение, и выполняет соответствующие действия с информацией: загружает источник изображения в Loader, устанавливает текст в атрибуцию и сохраняет ссылку для последующего использования в onClickметоде.

Опять же, это не очень сложный класс. Мы можем попытаться упростить ситуацию, создав множество небольших, сфокусированных объектов.

Там нет ничего, чтобы проверить еще; сохраните это для следующего шага, когда мы настроим DetailImage.


Чтобы отобразить детальное изображение, мы сначала хотим создать DetailImageобъект в нашем классе документов. Итак, ImageViewerдобавьте выделенный код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package ui {
 
    import flash.display.*;
    import flash.events.*;
 
    import data.DataLoader;
    import data.ImageData;
    import events.ImageChangeEvent;
    import ui.detail.DetailImage;
    import ui.thumbs.ThumbnailCollection;
 
    public class ImageViewer extends Sprite {
 
        private var _imageData:DataLoader;
        private var _thumbnails:ThumbnailCollection;
        private var _detailImage:DetailImage;
 
        private var _currentImageIndex:int;
 
        public function ImageViewer() {
            _imageData = new DataLoader("data/images.xml");
            _imageData.addEventListener(Event.COMPLETE, onImageDataLoad);
 
            _thumbnails = new ThumbnailCollection();
            addChild(_thumbnails.container);
            _thumbnails.container.y = 300;
            _thumbnails.addEventListener(ImageChangeEvent.INDEX_CHANGE, onImageChange);
 
            _detailImage = new DetailImage(detailImage_mc);
        }
 
        private function onImageDataLoad(e:Event):void {
            var thumbPaths:Array = _imageData.getThumbnailPaths();
            for (var i:uint = 0; i < thumbPaths.length; i++) {
                _thumbnails.addThumbAt(thumbPaths[i], i);
            }
        }
 
        private function onImageChange(e:ImageChangeEvent):void {
            var index:int = e.value;
            updateImage(index);
        }
 
        private function updateImage(index:uint):void {
            var img:ImageData = _imageData.getImageByIndex(index);
            _detailImage.setImage(img.source, img.attribution, img.link);
        }
 
    }
 
}

Главное, что мы делаем, это вызываем setImageметод в нашем DetailImageслучае, и мы должны получить некоторую отдачу. Идите дальше и протестируйте проект, и посмотрите, не щелкает ли Thumbnailрезультат в чем-то происходящем DetailImage.

Получение изображения в области детализации

Обратите внимание, что четыре добавленные строки все о DetailImage. Мы расширили функциональность нашей программы, добавив один объект и внеся изменения в другой класс, который использует этот объект. Все остальные классы ( Thumbnailи ImageChangeEventт. Д.) Не нуждались в каких-либо обновлениях для внесения этого существенного изменения. Обратите также внимание на то, как выросла наша композиционная иерархия. Мы не только имеем нашу линию композиции с миниатюрами, но теперь у нас есть целая отдельная ветвь, включающая в себя DetailImage.

Составная иерархия до сих пор

Имея это в виду, теперь мы добавим последний фрагмент к этой загадке, систему нумерации страниц.


Наш пользовательский интерфейс будет состоять из двух кнопок, кнопок со стрелками влево и вправо в FLA. Создайте еще один новый текстовый файл, сохраненный как PaginationButtonв classes/ui/pagination/. Вот код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package ui.pagination {
 
    import flash.display.*;
    import flash.events.*;
 
    public class PaginationButton extends EventDispatcher {
 
        private var _clip:Sprite;
 
        public function PaginationButton(clip:Sprite) {
            _clip = clip;
            _clip.addEventListener(MouseEvent.CLICK, onClick);
            _clip.mouseChildren = false;
            _clip.buttonMode = true;
        }
 
        private function onClick(e:MouseEvent):void {
            dispatchEvent(e);
        }
 
        public function enable():void {
            _clip.alpha = 1;
            _clip.mouseEnabled = true;
        }
 
        public function disable():void {
            _clip.alpha = 0.5;
            _clip.mouseEnabled = false;
        }
 
    }
 
}

Основная задача этого класса, как представления кнопки, состоит в том, чтобы содержать Sprite(композицию) и предоставлять ей функциональность кнопки Sprite. Большая часть этого должна быть довольно очевидной, но мы также добавляем возможность enableи disableкнопку. Это дает объекту простой способ, как вы можете догадаться, включить или отключить кнопку. Функциональность заключена в капсулу и является ответственностью PaginationButtonобъекта. Обратите внимание, что мы не только управляем интерактивностью (с помощью _clip.mouseEnabled = true/false;), но и влияем на внешний вид, чтобы отключенная кнопка выглядела отключенной (с помощью _clip.alpha = 0.5;). Опять же, это инкапсуляция; простой вызов disableотключает кнопку, однако PaginationButtonобъект фактически выполняет эту задачу.

Это, вероятно, кажется довольно полным классом, но давайте добавим дополнительный слой.


У нас есть класс для отдельных кнопок, но мы бы хотели сгруппировать их в одну сущность. Мы создадим Paginationкласс, который будет составлять два PaginationButtonобъекта.

Создайте последний новый текстовый файл этого проекта (yay!), Вызовите его Pagination.asи сохраните в classes/ui/pagination/. Добавьте этот код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package ui.pagination {
 
    import flash.display.*;
    import flash.events.*;
 
    import events.ImageChangeEvent;
 
    public class Pagination extends EventDispatcher {
 
        private var _prevButton:PaginationButton;
        private var _nextButton:PaginationButton;
 
        public function Pagination(prevClip:Sprite, nextClip:Sprite) {
            _prevButton = new PaginationButton(prevClip);
            _nextButton = new PaginationButton(nextClip);
 
            _prevButton.addEventListener(MouseEvent.CLICK, onPrevClick);
            _nextButton.addEventListener(MouseEvent.CLICK, onNextClick);
        }
 
        private function onPrevClick(e:MouseEvent):void {
            dispatchEvent(new ImageChangeEvent(ImageChangeEvent.INDEX_OFFSET, -1));
        }
 
        private function onNextClick(e:MouseEvent):void {
            dispatchEvent(new ImageChangeEvent(ImageChangeEvent.INDEX_OFFSET, 1));
        }
 
        public function updateButtons(enablePrev:Boolean, enableNext:Boolean):void {
            if (enablePrev) {
                _prevButton.enable();
            } else {
                _prevButton.disable();
            }
 
            if (enableNext) {
                _nextButton.enable();
            } else {
                _nextButton.disable();
            }
 
        }
 
    }
 
}

Это действительно мало что делает, кроме того, что служит местом для сочинения двух PaginationButtonобъектов. Мы могли бы назвать этот объект менеджером, в значительной степени существующим, чтобы собирать и контролировать коллекцию других объектов. Это, однако, позволяет нам рассматривать эту коллекцию объектов как единое целое, тогда как PaginationButtonкласс уже является единым целым.

Тем не менее, небольшой ключ в нашей системе — это то, как справляться с тем, что происходит при нажатии кнопки. Вот что произойдет: во-первых, мы слушаем CLICKсобытия на наших двух кнопках. Затем мы отправляем новое событие в ответ. Событие будет ImageChangeEvent, но тип будет не INDEX_CHANGEновым, а новым типом. Затем мы будем использовать это valueсвойство, чтобы указать, в каком направлении нам нужно двигаться от текущего индекса изображения. Нам нужно изменить ImageChangeEventкласс, и это произойдет на следующем шаге.

Вы также заметите updateButtonsметод. Это даст нам возможность обновить состояние коллекции кнопок. Вы можете думать об этом, опять же, как о инкапсуляции более сложной задачи в менее сложную команду.

Прежде чем мы перейдем к вопросу ImageChangeEvent, вы можете удивиться, зачем вообще создавать этот класс. Или, возможно, зачем передавать эти две, Spriteа не создавать две PaginationButtonи передавать их. Ну, если честно, причина в том, что именно так я решил это сделать.

По моему мнению (и, пожалуйста, имейте в виду, что многое из этого — только мое мнение, изложенное в виде учебника), идея системы нумерации страниц заслуживает отдельного объекта. Когда я вижу отдельные элементы, такие как отдельные кнопки нумерации страниц, я вижу их как объекты. И если эти отдельные объекты могут быть сгруппированы, даже если это просто концептуально, то эта группа также представляется объектом. Это не в отличие от Thumbnailи ThumbnailCollectionклассов; Коллекция представлена ​​группой.

Это не единственный способ достичь нашей цели, но, как я обнаружил, он хорошо работает. Если вы хотите попробовать другую тактику, обязательно сделайте это. Один из лучших способов изучить объектно-ориентированное программирование — это попробовать идеи, а затем посмотреть, что сработало, а что нет. Конечно, для этого урока вам лучше всего следовать, внимательно следя.


Давайте сделаем обновления для ImageChangeEventкласса, который нам нужен. Это на самом деле равносильно добавлению нового INDEX_CHANGEтипа (выделено ниже).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package events {
 
    import flash.events.*;
 
    public class ImageChangeEvent extends Event {
 
        public static const INDEX_CHANGE:String = "indexChange";
        public static const INDEX_OFFSET:String = "indexOffset";
 
        private var _value:int;
 
        public function ImageChangeEvent(type:String, value:int, bubbles:Boolean=true, cancelable:Boolean=false) {
            _value = value;
            super(type, bubbles, cancelable);
        }
 
        override public function clone():Event {
            return new ImageChangeEvent(type, value, bubbles, cancelable);
        }
 
        override public function toString():String {
            return formatToString("ImageChangeEvent", "type", "value");
        }
 
        public function get value():int {
            return _value;
        }
 
    }
 
}

Это более или менее так. Если вы не заметили, мы будем использовать это valueсвойство как нечто более общее; ожидается, что, если это INDEX_CHANGEсобытие, то оно valueбудет содержать значение индекса назначения. Однако, если это INDEX_OFFSETсобытие, то оно valueбудет представлять значение смещения (то есть, насколько далеко от текущего значения индекса мы должны пройти).

Вот наша иерархия композиции на данный момент:

Составная иерархия до сих пор

Еще один шаг, и мы видим плоды этого труда.


У нас есть классы, у нас есть мувиклипы на сцене, нам просто нужно включить зажигание. В ImageViewer, нам нужно создать создать Paginationэкземпляр и прослушивать — и обрабатывать — его события. Добавьте следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package ui {
 
    import flash.display.*;
    import flash.events.*;
 
    import data.DataLoader;
    import data.ImageData;
    import events.ImageChangeEvent;
    import ui.detail.DetailImage;
    import ui.pagination.Pagination;
    import ui.thumbs.ThumbnailCollection;
 
    public class ImageViewer extends Sprite {
 
        private var _imageData:DataLoader;
        private var _thumbnails:ThumbnailCollection;
        private var _detailImage:DetailImage;
        private var _pagination:Pagination;
 
        private var _currentImageIndex:int;
 
        public function ImageViewer() {
            _imageData = new DataLoader("data/images.xml");
            _imageData.addEventListener(Event.COMPLETE, onImageDataLoad);
 
            _thumbnails = new ThumbnailCollection();
            addChild(_thumbnails.container);
            _thumbnails.container.y = 300;
            _thumbnails.addEventListener(ImageChangeEvent.INDEX_CHANGE, onImageChange);
 
            _detailImage = new DetailImage(detailImage_mc);
 
            _pagination = new Pagination(previous_mc, next_mc);
            _pagination.addEventListener(ImageChangeEvent.INDEX_OFFSET, onImageOffset);
        }
 
        private function onImageDataLoad(e:Event):void {
            var thumbPaths:Array = _imageData.getThumbnailPaths();
            for (var i:uint = 0; i < thumbPaths.length; i++) {
                _thumbnails.addThumbAt(thumbPaths[i], i);
            }
        }
 
        private function onImageChange(e:ImageChangeEvent):void {
            _currentImageIndex = e.value;
            updateImage(_currentImageIndex);
        }
 
        private function onImageOffset(e:ImageChangeEvent):void {
            _currentImageIndex += e.value;
            if (_currentImageIndex < 0) {
                _currentImageIndex = 0;
            } else if (_currentImageIndex >= _imageData.length) {
                _currentImageIndex = _imageData.length - 1;
            }
            updateImage(_currentImageIndex);
        }
 
        private function updateImage(index:uint):void {
            var img:ImageData = _imageData.getImageByIndex(index);
            _detailImage.setImage(img.source, img.attribution, img.link);
        }
 
    }
 
}

К этому моменту дополнения не должны быть слишком шокирующими. Сначала в строке 10 мы импортируем Paginationкласс, а затем создаем для него свойство в строке 18. В строках 33 и 3420 мы создаем экземпляр и прослушиваем INDEX_OFFSETсобытие. Наиболее существенным изменением является добавление onImageOffsetметода, который очень похож на onImageChangeметод, только нужно предварительно вычислить индекс от смещения valueиз ImageChangeEventи текущего индекса. Он также выполняет некоторую проверку границ, чтобы убедиться, что мы не увеличивали после конца и не уменьшали после начала. Иначе, это все еще вызывает updateImageс этим новым индексом.

Теперь зоркие из вас заметят использование _imageData.length, которое в настоящее время недопустимо для DataLoaderкласса. Ты прав; нам нужно добавить другой метод доступа к данным для DataLoader:

1
2
3
public function get length():uint {
    return _xml.image.length();
}

Это просто обеспечивает приятное и чистое свойство только для чтения, которое запрашивает данные XML на количество существующих узлов. С этим методом getter на месте, вперед и запустить фильм. Теперь вы сможете нажимать на кнопки со стрелками. Обратите внимание, что вы не можете пройти мимо начала или конца. Это по замыслу; Вы могли бы изменить логику, чтобы заставить ее зацикливаться на другом конце, но для целей этого урока я хочу иметь жесткие ограничения для целей следующего шага.


Наша последняя задача — убедиться, что пользовательский интерфейс обновляется каждый раз, когда на дисплее появляются какие-либо изменения. Например, щелчок по Thumbnailтекущему отображает соответствующее изображение. Тем не менее, он также должен нарисовать выделение вокруг Thumbnailнажатой кнопки, а также отключить следующую или предыдущую кнопку, если это необходимо. Соответственно, нажатие на одну из PaginationButtonкнопок должно не только отображать изображение, но также отключать или включать его PaginationButtonв зависимости от ситуации, а также рисовать выделение вокруг соответствия Thumbnail.

Вы можете заметить, что это тот случай, когда изменения в одной части пользовательского интерфейса влияют на другую часть пользовательского интерфейса (кроме более непосредственного эффекта изменения детального изображения). Искушение может состоять в том, чтобы, скажем, Thumbnailнарисовать свой собственный выбор при нажатии или PaginationButtons, чтобы включить или отключить себя при нажатии. Однако, если бы Thumbnailнарисовал свой собственный выбор, то как бы мне управлять выбором, когда мы изменили изображение с PaginationButtons? Это было бы возможно, но это означало бы, что мы будем выбирать Thumbnails из двух разных мест.

Вместо этого мы позволим иерархии композиции работать на нас и сохраним эту логику интерфейса в одном месте: в ImageViewer. Мы уже управляем изменением детализации изображения, так почему бы не управлять и другими обновлениями пользовательского интерфейса?

Обновите updateImageметод следующим образом:

1
2
3
4
5
6
private function updateImage(index:uint):void {
    var img:ImageData = _imageData.getImageByIndex(index);
    _detailImage.setImage(img.srouce, img.attribution, img.link);
    _thumbnails.selectThumbnail(index);
    _pagination.updateButtons(index > 0, index < _imageData.length-1);
}

Все, что мы делаем, — это расширяем работу, когда приходит время обновить изображение, поддерживая синхронизацию пользовательского интерфейса.

Теперь нам нужно написать selectThumbnailметод ThumbnailCollection. Во-первых, нам понадобится свойство для хранения выбранного в данный момент Thumbnailобъекта:

1
private var _currentThumb:Thumbnail;

И тогда мы можем написать selectThumbnailметод, который будет использовать это свойство:

1
2
3
4
5
6
7
public function selectThumbnail(index:int):void {
    if (_currentThumb) {
        _currentThumb.deselect();
    }
    _currentThumb = _thumbnails[index] as Thumbnail;
    _currentThumb.select();
}

Логика в этом методе просто гарантирует, что ранее сохраненный Thumbnail(если он есть; не первый раз, когда мы вызовем этот метод) будет отменен, в то время как новый выбран. Этот метод полностью восходит к самому первому учебнику AS3 101 по переменным , если вы хотите более глубокое обсуждение.

Вернуться в настоящее. Может показаться немного странным, что Thumbnail CLICKотправка осуществляется через ThumbnailCollectionобъект, затем к ImageViewerобъекту, который затем вызывает метод для ThumbnailCollectionобъекта, который затем вызывает метод для Thumbnailобъекта. Почему бы просто не Thumbnailактивировать каждый из них ? Две причины.

Во- первых, мы должны в идеале вернуться к нашему менеджеру класса ( ThumbnailCollection) для логики, потому что мы не только хотим , чтобы выбрать щелкнул Thumbnail, но мы также хотим де -выбор ранее выбранный Thumbnail. Как Thumbnailтаковой , каждый из них довольно автономен, и это именно то, что мы хотим (инкапсуляция), и ни один отдельный Thumbnailобъект не знает ни о каком другом Thumbnailобъекте. Таким образом, нам нужно ThumbnailCollectionвыполнить отмену выбора, по крайней мере, и имеет смысл выполнить выборку из того же места.

Во-вторых, как уже упоминалось, мы хотим иметь возможность обновлять выборку из других частей пользовательского интерфейса. И это будет намного более управляемым, если мы сохраняем логику для выполнения выбора в одном месте. Следовательно, для этого единственного места имеет смысл «наименьший общий знаменатель» пользовательского интерфейса; ImageViewerкласс. В нашей иерархии отдельные ветви мало знают друг о друге. Например, ветвь пагинации не знает о ветке thumbs, и поэтому изменение пагинации, которое влияет на большие пальцы, естественно будет обрабатываться в том месте, где соединяются ветви, то есть в ImageViewerклассе.

Достаточно теории; иди и попробуй фильм уже! Пользовательский интерфейс должен оставаться синхронизированным; нажатие на миниатюру влияет на состояние нумерации страниц, и наоборот. Хорошо идет!

Пользовательский интерфейс работает синхронно.  Обратите внимание, что выбран последний эскиз, а следующая кнопка отключена.

Последнее, что, вероятно, вас беспокоит, если вы ищете полезного. Мы должны действительно поднять начальное изображение в DetailImageпредставлении. Должно быть достаточно легко … но где следует реализовать изменения?

Мы можем передать DetailImageклассу начальное изображение для загрузки. Это, безусловно, будет отображать изображение, но не будет обновлять пользовательский интерфейс. PaginationButtonS не будет знать , чтобы отключить предыдущую кнопку (при условии , что изображение первого изображения), и мы не получили бы выбор на Thumbnail.

Мы можем ThumbnailCollectionавтоматически отправлять ImageChangeEvent.INDEX_CHANGEсобытие, когда оно заполнено данными, автоматически заполняя » 0» для value. Это решило бы проблему синхронизации пользовательского интерфейса. Но стоит ли ThumbnailCollectionбеспокоиться об этом? Я уже говорил, что если это работает, то это технически не так, но это лучшее решение? По моему мнению, выполнение этого ставит ответственность не в том месте. Это не до ThumbnailCollectionпервоначального выбора. Мы действительно будем делать это там для целей синхронизации пользовательского интерфейса, а это не то, для чего предназначен ThumbnailCollectionобъект.

Почему бы просто не обновить интерфейс так, как мы это установили: имейте ImageViewвызов updateImage. Таким образом, основной класс документа несет ответственность за решение, какое изображение отображать изначально, а также когда и вообще. Это также чрезвычайно просто сделать в этот момент. Просто добавьте одну строку в onImageDataLoadметод (выделено ниже):

1
2
3
4
5
6
7
private function onImageDataLoad(e:Event):void {
    var thumbPaths:Array = _imageData.getThumbnailPaths();
    for (var i:uint = 0; i < thumbPaths.length; i++) {
        _thumbnails.addThumbAt(thumbPaths[i], i);
    }
    updateImage(0);
}

Запустите фильм сейчас, и мы не только прекрасно настроили начальное состояние, но и еще раз изучили идею ответственности и инкапсуляции.


Мы могли бы использовать эту концепцию для синхронизации пользовательского интерфейса немного дальше, и если вам нужна практика, я бы порекомендовал вам сделать это. Однако этот урок уже длиннее среднего, и я должен оставить вас с этим вызовом:

Сделайте кнопку «слайд-шоу», которая также управляет отображением изображения. При щелчке он входит в режим воспроизведения слайд-шоу, который запускает a Timerи, следовательно, повторно отправляет TimerEvents как ImageChangeEvents INDEX_OFFSETтипа. Слайд-шоу должно быть приостановлено при повторном нажатии. Кроме того, он должен автоматически приостанавливаться, когда вы щелкаете по любому из миниатюр или нумерации страниц (предполагается, что пользователь заинтересован в возобновлении контроля над изображениями, а автоматическое продвижение слайд-шоу больше не требуется).

По своей сути, эта проблема — просто еще одна ветвь иерархии композиции, и та же модель отправки событий обратно ImageViewerобъекту поможет. Детали реализации, конечно, будут разными, но общая идея та же.

Надеемся, что это концептуальное обсуждение также выявило преимущество объектно-ориентированного программирования: когда объекты должным образом «запечатаны» и не зависят друг от друга (то есть инкапсулированы), добавление функций становится почти тривиальным. Вам не нужно охотиться за салатом кода, чтобы найти нужные вещи для обновления. Просто добавьте несколько новых объектов и создайте их экземпляры в нужном месте (да, это слишком упрощение. Я пытаюсь продать вас на ООП, верно?).

Теперь, как я уже сказал, у нас нет времени для реализации этой функции слайд-шоу, и поэтому я призываю вас попробовать свои силы в этом самостоятельно. Я, однако, включил версию проекта в пакет загрузки, который уже закончил. Если вам нужен совет или вам просто лень делать это самостоятельно, но вы хотите увидеть его в действии, взгляните на проект image_viewer_slideshow .


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

Иерархия композиции — это то, к чему нужно привыкнуть, но с практикой это становится довольно естественным, и вы сможете начать новый проект, взглянув на желаемый результат и разбив его на эту иерархию.

Если вы новичок в объектно-ориентированном программировании, то все это, вероятно, будет головокружительным. Пожалуйста, не расстраивайтесь. Это займет немало практики, чтобы освоиться с ООП. Единственный способ поправиться — это сделать это на самом деле. Вы, вероятно, напишете несколько довольно плохо управляемых классов в первые пару раз. Ничего страшного; просто узнайте, что вы можете из этого опыта, и выясните, что бы вы сделали по-другому, если бы вам пришлось делать это снова. Я думаю, что у меня ушло около года с того момента, когда я впервые узнал ООП, чтобы достичь того момента, когда я действительно почувствовал, что мне не нужно стыдиться написанного мной кода. Просто придерживайся этого. Практика делает совершенным, как говорится.

Сказав это, я рад, что вы до сих пор там сидели Эта серия уроков еще не закончена, но этот конкретный выпуск был довольно левиафанским по своей природе. Спасибо за чтение, и мы познакомимся с некоторыми еще более продвинутыми объектно-ориентированными методами в следующей статье.