В этой мега-части нашего введения в объектно-ориентированное программирование в ActionScript 3 мы рассмотрим несколько выдающихся тем, а также соберем все воедино и организуем простой проект вокруг нескольких независимых, но совместимых объектов.
Если вы уже следили за серией AS3 101 , у вас не возникнет проблем с общими приемами, описанными в этом руководстве. Вы сосредоточитесь на том, как различные элементы всего ролика Flash взаимодействуют друг с другом, и изучите основы создания объектно-ориентированных приложений. Если вы обнаружите, что это немного над вашей головой, я бы посоветовал вам вернуться и освежить в памяти другие темы, затронутые в этой серии, а именно, касающиеся работы с XML , загрузки внешних изображений и первых двух частей этой серии ООП.
Шаг 1: Добро пожаловать обратно
Я собираюсь начать этот урок с обсуждения нескольких концепций, которые не вошли в предыдущие уроки ООП. Идея статических членов, пакетов, импортов, исходных путей и компоновки будет необходима как для проекта просмотра изображений, так и для общего образа жизни объектно-ориентированного программиста. Как и в большинстве моих учебных пособий по AS3 101, мы сначала сосредоточимся в основном на концепциях и теории, а затем будем использовать их более практичным образом.
Шаг 2: статическая привязка
Все свойства и методы, которые мы писали до сих пор, известны как свойства экземпляра (или методы экземпляра , в зависимости от ситуации). Что это обозначает? Что свойства и методы принадлежат экземпляру. Помните, как мы говорили, что все дома, построенные по одному и тому же проекту, имеют входную дверь, но характеристики двери могут быть уникальными для каждого дома? Да, это было очень давно, еще в начале первого урока ООП, но это то, что является свойством экземпляра. Каждый экземпляр может иметь свое уникальное значение, хранящееся в этой переменной. Можно присвоить одному экземпляру Button101
ярлык «Абракадабра», а другому — «Hocus Pocus». Поскольку каждый объект имеет свое собственное свойство, каждый экземпляр имеет свое собственное значение, хранящееся в нем.
Однако что, если вам просто нужно место для хранения значения, значения, которое не нужно изменять независимо с каждым экземпляром объекта? Что если у Button101
есть свойство углового радиуса, которое вы хотите сохранить согласованным во всех экземплярах кнопок, чтобы все кнопки имели одинаковый внешний вид?
Если это так, то зачем каждому экземпляру нужна своя копия того же значения?
Это не так. Если мы уверены, что все экземпляры смогут использовать одно и то же значение, то мы можем иметь все экземпляры на самом деле с одним и тем же свойством . Мы делаем это, объявляя свойство как статическое . Это означает, что свойство (или метод, если вы должны были сделать то же самое с методом) не принадлежит экземпляру, а классу. Это похоже на указание на план, что все дома должны иметь красную деревянную дверь.
На практике это выглядит так:
1
|
private static var cornerRadius:Number = 5;
|
Все, что мы сделали, это добавили ключевое слово static
. Это превращает его в свойство класса (или, опять же, метод класса ). Они также называются статическими свойствами (или … да, вы поняли).
Обратите внимание, что, как и ключевое слово override
, порядок между static
и public/private/internal/protected
имеет значения. Опять же, разумно выбрать стандарт и придерживаться его.
Если мы сделаем это (объявим свойство как статическое, а не выберем стандарт), тогда все экземпляры будут иметь одинаковое значение для cornerRadius
. Это не копия значения; на самом деле это ссылка на одно и то же значение во всех экземплярах.
Зачем это делать? Ну, например, есть места, где это просто имеет смысл. В сценарии, описанном на этом шаге, это имеет смысл, потому что у нас никогда не было изменений в этом конкретном значении. С другой стороны, объявление свойств или методов как статических может уменьшить использование памяти. Поскольку свойство указывает на одно значение в памяти, а не на уникальные экземпляры значений с одинаковыми именами, это оказывает меньшее влияние на память. Очевидно, что в нашем простом примере создание одной переменной Number
вместо двух вряд ли стоит рассматривать, но в тех случаях, когда объекты создаются несколько раз, а значение, содержащееся в свойстве, представляет собой существенную структуру данных, статические свойства могут создавать значимые значения. экономия.
Не упустите возможность обсудить, когда использовать статические методы и свойства в ближайшем будущем.
Для более глубокого изучения того, когда использовать статические методы и свойства, обратите внимание на краткий совет, который скоро будет опубликован, по Activetuts +.
Шаг 3: Пакеты
Другой остаток — понятие пакетов . До сих пор мы совершенно не знали о пакетах, но я думаю, что вы готовы с этим справиться. Пакеты — это просто способ организации вашей интро-группы кода и подгрупп. И это во многом связано с тем ключевым словом package
которое начинается с каждого файла класса.
Технически, мы все время использовали пакеты. Это невозможно не сделать; это ключевое слово package
определяет пакет, к которому принадлежит ваш класс. Тем не менее, мы использовали так называемый пакет верхнего уровня . Когда вы пишете это:
1
2
3
|
package {
// Class code here…
}
|
Вы помещаете свой класс в пакет верхнего уровня.
Однако, по разным причинам, вы можете начать размещать файлы классов в папках. Организация, вероятно, самая распространенная причина; Вы хотели бы объединить различные аспекты проекта и отделить их друг от друга, если это необходимо. Вы также можете повторно использовать имя класса. Если бы вы могли хранить Main
класс одного SWF-файла в отдельной папке от Main
класса другого SWF-файла, то вы могли бы использовать имя дважды.
Пакеты — это способ организации ваших классов в папки. Это двухэтапный процесс: сначала вам нужно создать файл в структуре папок, отражающей организацию, которую вы хотите. Затем вы отражаете структуру папок после ключевого слова package
в файле класса.
Шаг 4. Создание примера пакета
Мы создадим образец пакета прямо сейчас, чтобы проиллюстрировать, как это делается. Мы фактически модифицируем проект предыдущего урока, чтобы использовать пакеты. Если у вас нет этих файлов под рукой, его копию можно найти в пакете загрузки как «button_tutorial».
В вашей файловой системе создайте папку для размещения всего проекта.
Скопируйте файл Flash в эту папку
Создайте серию папок в этой папке. Они должны следовать этой структуре:
- [папка проекта] /
- as3101 /
- образец/
- щ /
- as3101 /
Теперь в as3101/sample/
скопируйте файл DocumentClass . Скопируйте класс as3101/ui/
папку as3101/ui/
.
Это первая часть; у нас есть организационная схема в нашей файловой системе. Теперь нам нужно отредактировать эти два файла, чтобы пакет соответствовал этой структуре папок.
Откройте DocumentClass и измените первую строку следующим образом:
1
|
package as3101.sample {
|
Аналогично, откройте Button101 и сделайте так, чтобы первая строка читалась:
1
|
package as3101.ui {
|
Другими словами, мы отредактировали файлы так, чтобы бит между package
и открывающей фигурной скобкой отражал структуру папок, ведущую к самому файлу класса, относительно файла Flash. Мы используем точки, чтобы отделить элементы папки, но не косую черту или обратную косую черту. Обратите внимание, что само имя класса не включено; пакет — это просто вложенные папки. Имя класса по-прежнему указывается после ключевого слова class
.
Как организационная техника, это открывает гораздо больше возможностей для структурирования ваших проектов. По мере того, как вы будете писать все больше и больше классов, вы захотите сгруппировать связанные классы вместе и создать иерархии группировок. Пакеты позволят вам сделать именно это.
Тем не менее, мы еще не закончили; если вы попытаетесь запустить проект прямо сейчас, вы ничего не получите; поскольку мы переместили DocumentClass, файл Flash больше не может его найти.
Шаг 5: Полностью квалифицированные имена классов
Один тонкий момент, который стоит иметь в виду, заключается в том, что как только вы используете пакет, имя вашего класса становится более сложным. Теперь, для всех намерений и целей, наши два класса по-прежнему называются « 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». В появившемся окне нажмите кнопку с галочкой рядом с входом «Класс документа:»:
Это приведет к следующему сообщению об ошибке:
Это способ Флэша сказать то же самое, что я только что сказал вам. Только Flash не скажет вам этого, если вы не предпримете какие-то действия, как мы только что сделали.
Чтобы все исправить, измените значение этого поля класса документа на as3101.sample.DocumentClass
. Когда вы нажимаете Return, ничего не должно происходить (другой вариант — получить похожее диалоговое окно с предупреждением). Когда вы нажмете на иконку карандаша, вы увидите, что класс открыт. Это означает, что мы успешно повторно связали наш Flash-файл с его недавно переименованным классом документов.
Однако, если вы тестируете сейчас, все равно не работает. С другой стороны, вы, по крайней мере, получите некоторые ошибки, объясняющие, почему что-то не работает:
По сути, мы столкнулись с той же проблемой с классом Button101
что и с DocumentClass
.
Шаг 6: Импорт
Как только вы начинаете помещать классы в пакеты, вам внезапно приходится указывать 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
не требуется, но его можно использовать, если хотите.
Шаг 7: Импорт подстановочных знаков
Теперь вы должны знать, что есть два способа импортировать класс. Одним из них является способ, который мы только что сделали: перечислить класс, используя его полное имя.
Однако вы можете использовать импорт с использованием подстановочных знаков . Это выглядит так:
1
|
import as3101.ui.*;
|
И он делает то, что вы, вероятно, ожидаете. Любой класс внутри пакета пользовательского ui
теперь доступен классу, выполняющему импорт по его короткому имени.
Есть несколько вещей, которые стоит отметить в этой технике.
- Вы не можете сделать это:
import as3101.*;
, Это вам ничего не даст. Подстановочные знаки импортируют только те классы, которые являются прямыми потомками
указанный пакет Поскольку Button101 и DocumentClass являются членами подпакетов as3101, мы ничего не добьемся
эта попытка. - Использование подстановочного знака автоматически не компилирует каждый класс в пакете в ваш SWF. Я повторяю: это не так .
Компилятор Flash достаточно умен, чтобы компилировать только те классы, которые он видит, используемые вашим SWF. Вам не нужно
беспокоиться о ненужном увеличении размера файла, используя подстановочный знак.
Зная это, использовать групповые символы или нет, должно стать личным предпочтением. Лично я использую их только на классах Flash Player (те, которые начинаются с flash.…
). В этих случаях я использую так много flash.net
или flash.display
что просто убедиться, что они все доступны, без необходимости возвращаться в раздел импорта и добавлять новый импорт. Тем не менее, с классами, которые я пишу, я считаю полезным специально перечислять импорт по отдельности, как форму документации. Вы можете посмотреть в разделе импорта и увидеть, на какие другие классы опирается данный класс. Однако выбор за вами, и я бы посоветовал вам определить свое соглашение и придерживаться его.
Я напишу краткий совет, который углубится в импорт. Это будет выпущено в ближайшее время, поэтому будьте внимательны, если хотите узнать больше о тонких деталях операторов импорта.
Для более глубокого обсуждения импорта с подстановочными знаками следите за появлением Activetuts + Quick Tip на эту тему.
Шаг 8: Исходные пути
Если мы собираемся поговорить об использовании пакетов для организации наших файлов классов, нам следует расширить обсуждение до путей к исходным текстам. Исходный путь — это каталог, в котором Flash будет искать классы для компиляции. По умолчанию каждый документ Flash знает, что нужно искать файлы классов в собственном каталоге документа Flash. Вот почему наши примеры работали до сих пор; наши классы всегда находились в той же папке, что и документ Flash (имейте в виду, что наше недавнее занятие по пакетам технически помещает файл класса в другую папку, но полное имя класса разрешает класс в серию папок плюс файл. И корневая папка наших пакетов находится в той же папке, что и документ Flash).
Теперь существует множество причин, по которым вы хотите хранить классы в каком-то другом каталоге. Два из наиболее распространенных являются:
- У вас есть многократно используемые служебные классы (возможно, TweenMax, Away3D или что-то полезное, что вы написали). Вы хотите использовать эти классы
во всех проектах, и не нужно копировать файлы из одного проекта в другой. - Вы хотели бы организовать свой проект так, чтобы по какой-то причине ваши Flash-файлы находились в другом каталоге, чем ваш класс
файлы. Это действительно имеет смысл, когда у вас большой проект, включающий много Flash-файлов. Просто чтобы сохранить структуру проекта
Проще говоря, вы можете решить оставить папку «classes» и папку «flas». Таким образом, Flash-файлы сгруппированы, но все еще могут
делиться классами, общими для всего проекта.
Естественно, вы можете выполнить эти задачи. Я бы не поднял их, если бы ты не смог. Я имею в виду, но не это значит.
И, конечно, у вас есть два варианта сделать это. Оба включают определение исходного пути . Вы можете определить их на уровне приложения (в этом случае исходный путь доступен для каждого документа Flash, который вы открываете на этом компьютере) или на уровне документа (в этом случае исходный путь доступен только для этого документа). В случае причины № 1 выше, путь источника уровня приложения имеет смысл. Что касается причины № 2, путь источника уровня документа будет лучше.
Чтобы определить исходный путь уровня приложения
Откройте настройки Flash (на Mac: Flash> Настройки; на ПК: «Правка»> «Настройки»). Нажмите на категорию ActionScript слева. В нижней части появившегося окна нажмите кнопку «Параметры ActionScript 3.0…».
В появившемся окне есть три области: исходные пути — верхняя.
Нажмите на кнопку «+» и введите свой путь. Или нажмите на кнопку папки, чтобы перейти к ней. Однако, если вы введете его, нажмите «ОК», пока не вернетесь к Flash. На данный момент вы можете свободно импортировать и использовать любой класс из этого каталога.
Чтобы определить исходный путь на уровне документа
Открыв документ, откройте «Параметры публикации» (выберите «Файл»> «Параметры публикации…»). Нажмите на вкладку «Flash». Нажмите кнопку «Настройки…» рядом с элементом управления версией ActionScript. Откроется новое окно, и оно будет очень похоже на окно, которое вы получите с настройками приложения. Его расположение немного отличается, но идея та же. Убедитесь, что вы находитесь на «Source Path» и введите свой путь. Нажмите «ОК» и помните, что это только для этого документа .
Между пакетами и исходными путями у вас есть много вариантов организации ваших классов.
Шаг 9: Композиция
В предыдущем уроке мы много внимания уделяли наследованию. Существует методика, которая может считаться противоположной наследственности. Это называется композиция .
Теперь нет ничего плохого в наследовании. Не то чтобы я сидел на троне лжи и кормил тебя злыми тарадидлами в этом последнем уроке. Но вы можете встретить фразу «предпочесть композицию наследованию» в вашем объектно-ориентированном путешествии. Если (когда) вы начнете изучать шаблоны проектирования, вы наверняка услышите это, и вы даже увидите, как это проявится, если будете достаточно долго работать с продвинутыми программистами. Недавно Big Spaceship (на момент написания этой статьи) опубликовал обсуждение методологии, которую они использовали при разработке пакета отображения своей библиотеки Github-ed ActionScript . В этом они объясняют свои причины, по существу, композиции вместо наследования.
Так что же это? Хорошей новостью является то, что вы уже знаете, как это сделать, и делали это все время. Композиция — это когда один объект сохраняет другой объект в свойстве. Если у вашего класса есть свойство myLoader
и в нем хранится объект Loader
, то говорят, что ваш класс составляет объект Loader. Это происходит все время; фактически сам Loader
имеет свойство contentLoaderInfo
которое само составляет объект LoaderInfo
, который предоставляет вам информацию о фактической загрузке.
Шаг 10: Тонкая разница
Композиция кажется далекой от наследства, не так ли? Эти два, вероятно, не кажутся связанными вообще. Но 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 «
Шаг 11: «Есть» и «Есть»
Итак, если и наследование, и композиция являются приемлемыми методами, как вы узнаете, когда и какие использовать? Ну, во-первых, есть упомянутый ранее архетипический ответ «предпочитаю композицию наследованию». Если вы чувствуете, что у вас есть возможность пойти с одним из них, возможно, вы захотите ошибиться на стороне композиции, хотя бы потому, что вы знаете какого-то лживого старого автора учебников, который сказал вам, что вы делаете это. Однако со всей серьезностью вы можете верить, что люди умнее вас (и умнее меня) придумали эту идиому предпочтения, и нет никаких оснований не поверить на это их словам.
Но как насчет более практического совета? Популярным методом оценки соответствия наследования и композиции является тест «Имеет» / «Есть».
Это идет так. Вы берете имена ваших двух классов (на данный момент мы представим, что это «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 может помочь вам.
Шаг 12: окончательное мнемоническое устройство
Если вам нужно более наглядное объяснение наследования и композиции, подумайте о следующем.
Наследование похоже на Робокопа . Он типичный человек расширенный с дополнительной функциональностью. Или, другими словами, он робот, который наследует основные функциональные возможности человека.
http://www.internationalhero.co.uk/r/robocop.jpg
Композиция похожа на Майкла Найта и Китта . Майкл Найт — типичный человек, и остается типичным человеком, только он владеет (создает) самосознающим автомобилем.
http://manggoblog.files.wordpress.com/2008/10/hoff-knight-rider-mustang.jpg
Оба полностью рад, но они достигли своей великолепности с помощью различных методов.
Но из этих двух только Хофф стал звездой в Baywatch. Поэтому предпочитайте композицию наследству.
http://stupidcelebrities.net/wp-content/hasselhoff2.jpg
(Я должен отметить, что я не придумал эту аналогию, но я затрудняюсь найти первоначальный источник. Если кто-нибудь знает, пожалуйста, сообщите нам об этом в комментариях)
Шаг 13: Общайтесь
Моя точка зрения при создании композиции больше связана со сборкой более крупной программы из объектов, а не с обсуждением более тонких моментов того, целесообразно ли это по сравнению с наследованием в различных ситуациях. Эти тонкие моменты важны, и вы будете сталкиваться с ними много раз по мере того, как вы будете расти как объектно-ориентированный разработчик, но сейчас нам нужно посмотреть, как мы применяем наши методы объектно-ориентированного программирования для создания более крупного приложения. ,
Наиболее распространенное разочарование и замешательство, которое я вижу у начинающих разработчиков ООП, связано с взаимодействием между различными объектами.Большинство людей на этом этапе своего образования понимают классы и предметы, а также то, как писать данный класс. Следующий шаг — начать использовать несколько классов и объектов для создания чего-то полезного (например, веб-сайта), но кажется, что естественная путаница возникает из-за того, что объекты должны быть инкапсулированы (помните это обсуждение? Это было рассмотрено в моем первом ООП). учебник ). Тогда как один объект взаимодействует с другим объектом, чтобы создать систему, из которой можно построить эту полезную программу?
Большинство первых попыток, которые я засвидетельствовал, связаны либо с ужасными, ужасными нарушениями принципа инкапсуляции, сферой ответственности для данного класса, либо с обоими. Или же попытки просто не работают. Я хотел бы выделить время (и пространство статьи), чтобы показать вам пример плохо написанной системы объектов.
Шаг 14: Код на этом этапе сомнителен, пожалуйста, не пытайтесь напечатать его для себя. Мы иллюстрируем менее чем идеальную технику.
Я надеюсь, что заголовок сделал точку.
В этом примере я представляю два класса 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
и theString
), а затем сохраняем их в свойствах экземпляра в следующих двух строках. - В строке 38 мы добавляем нашего собственного
CLICK
слушателя для внутренней обработки. - На линиях 55-57 у нас есть
CLICK
слушатель. Эти новые свойства используются для примененияString
к текстовому значениюTextField
.
Теперь, если это работает, почему это плохая идея? Обычно довольно субъективно считать правильность кода. С одной стороны, если это работает, то это не может быть так плохо. С другой стороны, если код должен поддерживаться , то удобство сопровождения кода так же важно, как и функциональность.
Код, представленный здесь, не подлежит обслуживанию. Самая большая проблема прямо здесь (в Button101BadIdea
):
1
|
_clickField.text = _text; |
Мы только что заблокировали наш класс кнопки для выполнения одной и только одной задачи: помещения текста в TextField
. Теперь, конечно, он настроен на использование любого текста и любого TextField
, но на самом деле эта кнопка имеет очень конкретное назначение. Если вам когда-нибудь понадобится кнопка, чтобы сделать что-то еще — что — нибудь еще — вы застряли бы с необходимостью создавать больше классов кнопок. Даже если внешний вид кнопки должен был быть таким же.
Шаг 15: Могу ли я лучше, Солушун?
Конечно, лучшим решением было то, что уже было представлено в предыдущих уроках (см. Проект «button_tutorial» в пакете загрузки, если вам нужно обновить). Но что делает это лучше?
Для начала, Button101
класс гораздо более пригоден для повторного использования, если он не предполагает, что щелчок должен привести к вводу текста в TextField
. При необходимости этот конечный результат все еще может быть достигнут, но может быть и любой другой результат. Но мы достигаем результата не в рамках Button101
объекта; вместо этого эта логика находится в DocumentClass
объекте. Или, в более общем смысле, эта логика находится в объекте, который «владеет» Button101
объектом.
Теперь объектом, который владеет Button101
объектом, может быть практически любой объект, для которого требуется кнопка. Затем за кнопкой следует конкретный конечный результат, необходимый при нажатии кнопки. И это ответственность за объект, которому нужна кнопка, а не сама кнопка.
Итак, в оригинальном и лучшем решении мы имеем один объект, который составляет другой объект. Составляемый объект является довольно автономным, но предоставляет определенные общедоступные свойства и методы, с помощью которых он может быть настроен в определенной степени. Объект, выполняющий компоновку, находится под контролем и манипулирует составным объектом через его общедоступные свойства и методы.
Эти отношения являются основным строительным блоком более крупных приложений и фокусом оставшейся части этого урока.
Шаг 16: Иерархия композиции
Чтобы обеспечить двустороннюю связь между классами, нам нужно начать думать с точки зрения иерархии композиции. Как правило, если объект A составляет объект B, то обратное явно не соответствует действительности. Я должен повторить, что, естественно, есть исключения из этого правила. Но эта конкретная методология хорошо работает в подавляющем большинстве случаев и хорошо вписывается в встроенную систему событий в ActionScript 3.
Подумай об этом так. Ваш SWF начинается с объекта документа. Этот объект, помимо прочего, будет состоять из других объектов, таких как MovieClip, или объектов, представляющих MovieClip. Может даже существовать объект, представляющий коллекцию миниатюр или сетку кнопок. Объект может «принадлежать» объекту документа («принадлежат», я имею в виду, что объект документа составляет объект коллекции большого пальца). Теперь объект коллекции большого пальца может составлять несколько объектов миниатюр, которые сами состоят из Loader
объекта и TextField
объекта.
Теперь, имеет ли смысл для объекта коллекции большого пальца составлять объект документа? Или для отдельных миниатюр для создания объекта коллекции большого пальца? Возможно ли даже для встроенного класса Loader
создать собственный класс Thumbnail
?
Иерархия композиции начинает становиться очевидной. Вы заметите, что существует не только структура того, как вещи составлены, но и сами объекты начинаются с самого грандиозного (объект документа) и прокладывают себе путь к более и более конкретным и сфокусированным объектам (таким как объект миниатюр, или основной строительный блок Loader
объекта).
Мы можем проиллюстрировать эту иерархию следующим образом:
На диаграмме выше объекты представлены прямоугольниками с именами классов в них. Один объект составляет другой, если соединительная линия заканчивается ромбовидной формой на конце композиции . То есть, на приведенном выше рисунке, DocumentClass
составляет Thumbnail
, и, Thumbnail
в свою очередь, составляет Loader
и TextField
.
Шаг 17: Охват событий
ActionScript 3 в значительной степени основан на идее диспетчеризации и прослушивания событий. Я не буду вдаваться в основные события здесь; Вы можете прочитать этот учебник ActiveTuts для получения дополнительной информации, если вам это нужно. События являются отличным способом обеспечения связи (это почти то, о чем они). Отправка события от одного объекта позволяет практически любому другому объекту действовать в ответ на него.
Однако обратите внимание на разницу между выполнением открытых методов и обработкой события . Например, представьте объект A и объект B. Объект A может выполнять любой из открытых методов объекта B, и это одна из форм связи. Если, с другой стороны, Объект A отправляет и Событие, для которого Объект B прослушивает, то это еще одна форма связи. Но обратите внимание на разницу. Первый активен : объект A напрямую манипулирует объектом B. Второй пассивен : объект A просто отправляет событие. Объект B может даже не прослушивать, и если это так, Объект A не имеет никакого контроля над тем, что Объект B может сделать в результате.
Шаг 18: Открытое общение
Итак, у нас есть иерархия композиции. Обычно объект документа составляет объект коллекции большого пальца. И из-за этой связи объект документа получает возможность выполнять публичные методы, непосредственно манипулируя / активно взаимодействуя с объектом коллекции большого пальца. Но коллекция большого пальца не знает об объекте, который «владеет» им. В идеале, он должен быть написан для повторного использования, чтобы позволить его использовать любому другому объекту, который хочет использовать коллекцию большого пальца. Поэтому мы не можем привязать его к какому-либо конкретному объекту документа.
Но мы хотим, чтобы объект документа удалил свой предварительный загрузчик, когда вся коллекция большого пальца загружена; как коллекция большого пальца взаимодействует с объектом документа, если он не составляет его? Как вы уже догадались, это происходит через события. Коллекция большого пальца связывается косвенно / пассивно с объектом документа. На самом деле, это немного вводит в заблуждение, говоря, что он «связывается с объектом документа». Это действительно просто говорит в пустоту: «Мои изображения загружены, на случай, если кому-то все равно». И, к счастью, есть кто-то, кто слушает в пустоте, и им все равно, потому что теперь они уберут свой предзагрузчик.
Другими словами, двусторонняя связь часто достигается с помощью открытого метода в одном направлении и посредством рассылки событий в другом.
Теперь, пожалуй, наиболее распространенной формой двусторонней связи, которая не является только что описанным методом, является способность публичного метода возвращать значение. Я бы сказал, что это не столько двусторонняя связь, сколько способ извлечения информации из другого объекта. То есть, если Объект A вызывает метод для Объекта B и получает возвращаемое значение, тогда Объекту B может быть сказано что-то сделать, но в действительности объект A не должен делать что-либо в ответ на получение значения обратно. Это раскалывает волосы, и хотя этот маневр ценен, мы здесь не фокусируемся.
Мы на мгновение разберем это в более практичном подходе, но помните об этой концепции при создании нашего средства просмотра изображений.
Шаг 19: Построение просмотра изображений
Давайте засучить наши рукава. Перво-наперво, давайте рассмотрим наш проект. Последний пример показан в верхней части статьи, поэтому, если вы забудете, вернитесь туда на мгновение. Этот шаг будет о определении наших потребностей и разделении наших объектов.
Во-первых, давайте запишем цели
- Показать группу миниатюр изображений
- При нажатии на уменьшенное изображение отобразится увеличенная версия изображения.
- Это также приведет к появлению «выбранного» состояния на миниатюре
- Нажатие на кнопку «следующий» или «предыдущий» приведет к отображению соответствующего изображения в большей области, а также к выбору соответствующего эскиза.
- Если пользователь находится на первом изображении в последовательности, кнопка «предыдущий» будет отключена и будет отображаться как таковая. Аналогичное действие со «следующей кнопкой» в конце последовательности.
- Нажатие на увеличенное изображение приводит к ссылке на исходное изображение на Flickr
- Управляйте отображаемыми изображениями и связанными данными с помощью документа XML, загружаемого во время выполнения.
Вы можете сказать, но я рисовал этот конкретный проект в последние несколько шагов с моими примерами композиционных структур. Назовите это предзнаменованием. Поэтому следующий список необходимых классов не должен вызывать удивления:
Класс документов
«Работает» в SWF. Собирает все вместе в рабочее приложение
ThumbnailCollection
Управляет группой Thumbnail
объектов. Отвечает за создание и верстку отдельных миниатюр на основе входящих данных. Также отвечает за выбор миниатюр.
Миниатюра
Индивидуальный эскиз. Действует как кнопка и загружает и отображает изображение. Также может отображать выбранное состояние, которое можно включить или отключить с помощью открытых методов.
DetailImage
Загружает увеличенное изображение. Можно щелкнуть и отправить это событие.
Пагинация
Контролирует их PaginationButtons
, повторно отправляя их CLICK
события и управляя, когда они включены и отключены.
PaginationButton Отдельная
кнопка «следующий» или «предыдущий» вариант. В основном просто кнопка, хотя их можно включать и отключать, и обновлять их внешний вид соответственно.
DataLoader
Все остальные классы, упомянутые до сих пор, были визуальными; этого нет. В его обязанности входит загрузка XML-файла, его анализ и извлечение соответствующих данных по запросу. Этот вид класса обычно кажется более чуждым начинающим объектно-ориентированным программистам, но он не более и не менее полезен, чем визуальная сторона вещей. Это включение здесь частично иллюстрирует этот момент.
ImageData
Объект-значение, единственная цель которого — переносить биты данных, относящиеся к одному изображению («Объект-значение» — это просто объект с, как правило, набором свойств и несколькими, если таковые имеются, методами. Цель состоит в том, чтобы собрать несколько битов информация — ценности — в единый объект).
ImageChangeEvent
Нам понадобится собственный класс события, чтобы упаковать некоторую полезную информацию вместе с событием. Событие, в частности, будет, когда изображение должно измениться из-за какого-то взаимодействия.
Имея это в виду, давайте начнем.
Шаг 20: Создайте проект
У нас есть хорошее представление о вовлеченных классах, поэтому мы можем успешно создать проект. Я подробно опишу процесс, используя файловую систему и ОС компьютера, но если вам удобен хороший проектно-ориентированный редактор кода, такой как 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/
|
Окончательная структура папок должна выглядеть следующим образом:
Шаг 21: Настройте FLA
Теперь создайте файл Flash (конечно, ActionScript 3.0) и сохраните его в папке src \ flas . Я звоню моему ImageViewer.fla . Кроме того, вы можете начать с FLA-файла, предоставленного в пакете загрузки, в image_viewier_start . В нем есть графика для различных элементов, поэтому вам не нужно тратить время на рисование кнопок и тому подобное.
Даже со стартовым FLA нам нужно настроить несколько вещей.
Выберите пункт меню « Файл»> «Параметры публикации» . Убедитесь, что выбрана вкладка « Форматы », и выберите имя для своего SWF. Более важно, однако, это установить путь. Мы хотим пойти в мусорное ведро , поэтому введите ../../bin/ImageViewer.swf
.
Затем нажмите на вкладку Flash , а затем на кнопку « Настройки…» рядом с версией ActionScript. Здесь убедитесь, что выбрана вкладка « Исходный путь », и нажмите кнопку « +» .
В полученной записи введите ../classes/
.
Установка относительного пути для путей публикации и исходных путей документов делает проект более переносимым. Не поддавайтесь искушению нажать на кнопку «цель», так как это приведет к абсолютному пути к папке классов. Это значительно затрудняет перемещение проекта в новое место на жестком диске или передачу кому-то другому для работы на другом компьютере.
Шаг 22: Создайте и установите класс документа
Создайте новый текстовый файл и сохраните его как 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. Скоро мы добавим больше классов, но сначала давайте подготовим наши загружаемые ресурсы.
Шаг 23: Добавьте изображения
Вы можете найти несколько изображений, готовых к использованию в пакете загрузки. Вы найдете их в image_viewer / bin / images / . Вы можете просмотреть файлы, если хотите, и вы, вероятно, найдете их предсказуемо организованными. В одной папке находятся изображения размером с миниатюру, а в другой папке — более крупные версии тех же изображений. Поместите маленькие изображения в вашем проекте bin/images/thumbs/
и большие изображения в bin/images/detail/
.
Все изображения взяты из Flickr и лицензированы Creative Commons. Атрибуты можно найти на следующем шаге, а также в окончательном проекте.
Если вы хотите использовать свои собственные изображения, увеличьте размер миниатюр до 100×100, а детали — до 400×300 и просто поместите их в соответствующие папки.
Шаг 24: Создайте файл XML
Теперь мы будем каталогизировать эти изображения в 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/" > |
Что это делает в три раза:
- Нам не нужно будет указывать полный путь к изображению для каждого изображения, потому что мы предполагаем, что все изображения находятся в одном каталоге.
- Нам не нужно указывать отдельные пути для миниатюры и деталей, и вместо этого мы полагаемся на две версии, имеющие
одинаковое имя файла, просто находящиеся в разных каталогах. - При желании мы можем легко настроить расположение папок с изображениями.
Вот полный документ 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>
|
Шаг 25. Создайте класс DataLoader
Мы начинаем прямо с нашего первого класса без документов, и это также не визуальный класс. Начните с создания нового текстового документа и сохранения его как 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 в случае чего-то плохого.
Шаг 26: Интегрировать DataLoader в приложение
Создав наш 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." ); }
}
}
|
Протестируйте его, и вы увидите единственную трассировку, указывающую, что данные закончили загрузку.
Шаг 27: Анализ DataLoader
Теперь давайте посмотрим на выбор, который мы сделали при разработке вещей до сих пор.
Во-первых, мы решили, чтобы 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
объекты.
Шаг 28: добавь метод доступа к данным
Давайте дадим 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 и квалифицирован для ее анализа; Итак, давайте разместим логику там, где она принадлежит.
Шаг 29: Используйте метод доступа к данным
Вернемся в ImageViewer , давайте использовать тот getThumbnailPaths
метод, который мы только что создали. Измените onImageDataLoad
метод так, чтобы он выглядел так:
1
2
3
4
|
private function onImageDataLoad(e:Event): void { var thumbPaths: Array = _imageData.getThumbnailPaths(); trace (thumbPaths.join( "\n" )); }
|
Пока это не добавляет много функциональности, но распечатывает что-то более интересное, чем раньше. Вы должны увидеть список полных путей к миниатюрным изображениям:
Это показывает, что наш getThumbnailPaths
метод работает, а также что мы используем составной объект, напрямую вызывая методы для него.
Шаг 30: создай класс миниатюр
Нашим следующим шагом будет передача этих 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
класс.
Шаг 31: Написать класс 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
.
Шаг 32: Используйте объект ThumbnailCollection
Давайте сложим все это вместе и посмотрим, что мы можем сделать. Вернувшись в 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. Вы также должны иметь возможность взаимодействовать с миниатюрами, чтобы подсветка отображалась при наведении мыши.
Шаг 33: Пользовательское событие
Мы собираемся обработать щелчок на 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
собственности. Это обеспечивает свойство только для чтения. Но это свойство будет чрезвычайно полезным в нашей программе.
Здесь нечего проверять, поэтому давайте перейдем к фактическому использованию этого объекта события.
Шаг 34: Нажав на изображение
До сих пор мы хорошо справились, просто написали целые классы и заставили их работать вместе. Наша следующая задача включает небольшие изменения в нескольких классах, поэтому обратите внимание.
Мы начнем с того места 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; }
}
}
|
Шаг 35: инкапсулируйте данные изображения в объект значения
Для того, чтобы получить данные изображения из 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
класс, однако, имеет только три допустимых свойства, каждое из которых имеет свой собственный тип данных. Компилятор будет ловить непреднамеренные злоупотребления объектом, вызывая ошибки.
Не только это, но и философски, мы можем мыслить с точки зрения объектов во всем приложении. Даже такая простая вещь, как «данные об изображении», может быть представлена как объект, и, таким образом, мы можем создать класс именно для этой цели.
Шаг 36: Запрос данных для получения подробной информации
Ну, у нас есть индекс. Нам удалось получить индекс того, на 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); }
|
Мы также извлечем логику для того, чтобы что-то сделать с этим индексом, в новый метод. На данный момент это может показаться излишним, но нам нужно будет сделать это позже, поэтому я использую свой хрустальный шар и экономлю немного накладных расходов, выполняя метод сейчас.
Протестируйте фильм, и вы должны увидеть соответствующий след при нажатии на миниатюры.
Шаг 37: Детальное изображение
Теперь давайте обратим наше внимание на область детализации изображения. Напомним, что целью здесь является загрузка изображения, отображение атрибута и переход по 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
.
Шаг 38: Показать детальное изображение
Чтобы отобразить детальное изображение, мы сначала хотим создать 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
.
Имея это в виду, теперь мы добавим последний фрагмент к этой загадке, систему нумерации страниц.
Шаг 39: Кнопки нумерации страниц
Наш пользовательский интерфейс будет состоять из двух кнопок, кнопок со стрелками влево и вправо в 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
объект фактически выполняет эту задачу.
Это, вероятно, кажется довольно полным классом, но давайте добавим дополнительный слой.
Шаг 40: Добавление нумерации страниц
У нас есть класс для отдельных кнопок, но мы бы хотели сгруппировать их в одну сущность. Мы создадим 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
классов; Коллекция представлена группой.
Это не единственный способ достичь нашей цели, но, как я обнаружил, он хорошо работает. Если вы хотите попробовать другую тактику, обязательно сделайте это. Один из лучших способов изучить объектно-ориентированное программирование — это попробовать идеи, а затем посмотреть, что сработало, а что нет. Конечно, для этого урока вам лучше всего следовать, внимательно следя.
Шаг 41: Обновите ImageChangeEvent
Давайте сделаем обновления для 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
будет представлять значение смещения (то есть, насколько далеко от текущего значения индекса мы должны пройти).
Вот наша иерархия композиции на данный момент:
Еще один шаг, и мы видим плоды этого труда.
Шаг 42: Создать систему нумерации страниц
У нас есть классы, у нас есть мувиклипы на сцене, нам просто нужно включить зажигание. В 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 на месте, вперед и запустить фильм. Теперь вы сможете нажимать на кнопки со стрелками. Обратите внимание, что вы не можете пройти мимо начала или конца. Это по замыслу; Вы могли бы изменить логику, чтобы заставить ее зацикливаться на другом конце, но для целей этого урока я хочу иметь жесткие ограничения для целей следующего шага.
Шаг 43: синхронизируйте интерфейс
Наша последняя задача — убедиться, что пользовательский интерфейс обновляется каждый раз, когда на дисплее появляются какие-либо изменения. Например, щелчок по Thumbnail
текущему отображает соответствующее изображение. Тем не менее, он также должен нарисовать выделение вокруг Thumbnail
нажатой кнопки, а также отключить следующую или предыдущую кнопку, если это необходимо. Соответственно, нажатие на одну из PaginationButton
кнопок должно не только отображать изображение, но также отключать или включать его PaginationButton
в зависимости от ситуации, а также рисовать выделение вокруг соответствия Thumbnail
.
Вы можете заметить, что это тот случай, когда изменения в одной части пользовательского интерфейса влияют на другую часть пользовательского интерфейса (кроме более непосредственного эффекта изменения детального изображения). Искушение может состоять в том, чтобы, скажем, Thumbnail
нарисовать свой собственный выбор при нажатии или PaginationButton
s, чтобы включить или отключить себя при нажатии. Однако, если бы Thumbnail
нарисовал свой собственный выбор, то как бы мне управлять выбором, когда мы изменили изображение с PaginationButton
s? Это было бы возможно, но это означало бы, что мы будем выбирать Thumbnail
s из двух разных мест.
Вместо этого мы позволим иерархии композиции работать на нас и сохраним эту логику интерфейса в одном месте: в 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
классе.
Достаточно теории; иди и попробуй фильм уже! Пользовательский интерфейс должен оставаться синхронизированным; нажатие на миниатюру влияет на состояние нумерации страниц, и наоборот. Хорошо идет!
Шаг 44: последнее задание
Последнее, что, вероятно, вас беспокоит, если вы ищете полезного. Мы должны действительно поднять начальное изображение в DetailImage
представлении. Должно быть достаточно легко … но где следует реализовать изменения?
Мы можем передать DetailImage
классу начальное изображение для загрузки. Это, безусловно, будет отображать изображение, но не будет обновлять пользовательский интерфейс. PaginationButton
S не будет знать , чтобы отключить предыдущую кнопку (при условии , что изображение первого изображения), и мы не получили бы выбор на 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 ); }
|
Запустите фильм сейчас, и мы не только прекрасно настроили начальное состояние, но и еще раз изучили идею ответственности и инкапсуляции.
Шаг 45: для дальнейших экспериментов
Мы могли бы использовать эту концепцию для синхронизации пользовательского интерфейса немного дальше, и если вам нужна практика, я бы порекомендовал вам сделать это. Однако этот урок уже длиннее среднего, и я должен оставить вас с этим вызовом:
Сделайте кнопку «слайд-шоу», которая также управляет отображением изображения. При щелчке он входит в режим воспроизведения слайд-шоу, который запускает a Timer
и, следовательно, повторно отправляет TimerEvent
s как ImageChangeEvent
s INDEX_OFFSET
типа. Слайд-шоу должно быть приостановлено при повторном нажатии. Кроме того, он должен автоматически приостанавливаться, когда вы щелкаете по любому из миниатюр или нумерации страниц (предполагается, что пользователь заинтересован в возобновлении контроля над изображениями, а автоматическое продвижение слайд-шоу больше не требуется).
По своей сути, эта проблема — просто еще одна ветвь иерархии композиции, и та же модель отправки событий обратно ImageViewer
объекту поможет. Детали реализации, конечно, будут разными, но общая идея та же.
Надеемся, что это концептуальное обсуждение также выявило преимущество объектно-ориентированного программирования: когда объекты должным образом «запечатаны» и не зависят друг от друга (то есть инкапсулированы), добавление функций становится почти тривиальным. Вам не нужно охотиться за салатом кода, чтобы найти нужные вещи для обновления. Просто добавьте несколько новых объектов и создайте их экземпляры в нужном месте (да, это слишком упрощение. Я пытаюсь продать вас на ООП, верно?).
Теперь, как я уже сказал, у нас нет времени для реализации этой функции слайд-шоу, и поэтому я призываю вас попробовать свои силы в этом самостоятельно. Я, однако, включил версию проекта в пакет загрузки, который уже закончил. Если вам нужен совет или вам просто лень делать это самостоятельно, но вы хотите увидеть его в действии, взгляните на проект image_viewer_slideshow .
Резюме
В этом уроке мы рассмотрели создание простого проекта с использованием объектно-ориентированных методов. Сам проект был (надеюсь) тем, что вы, вероятно, могли бы обрабатывать самостоятельно без объектов, но наша цель состояла в том, чтобы продемонстрировать, как проект может быть построен из нескольких объектов, а не просто объединены в один сценарий.
Иерархия композиции — это то, к чему нужно привыкнуть, но с практикой это становится довольно естественным, и вы сможете начать новый проект, взглянув на желаемый результат и разбив его на эту иерархию.
Если вы новичок в объектно-ориентированном программировании, то все это, вероятно, будет головокружительным. Пожалуйста, не расстраивайтесь. Это займет немало практики, чтобы освоиться с ООП. Единственный способ поправиться — это сделать это на самом деле. Вы, вероятно, напишете несколько довольно плохо управляемых классов в первые пару раз. Ничего страшного; просто узнайте, что вы можете из этого опыта, и выясните, что бы вы сделали по-другому, если бы вам пришлось делать это снова. Я думаю, что у меня ушло около года с того момента, когда я впервые узнал ООП, чтобы достичь того момента, когда я действительно почувствовал, что мне не нужно стыдиться написанного мной кода. Просто придерживайся этого. Практика делает совершенным, как говорится.
Сказав это, я рад, что вы до сих пор там сидели Эта серия уроков еще не закончена, но этот конкретный выпуск был довольно левиафанским по своей природе. Спасибо за чтение, и мы познакомимся с некоторыми еще более продвинутыми объектно-ориентированными методами в следующей статье.