Рубрика: ActionScript 3
SapphireSteel: Amethyst Beta 2
16 января SapphireSteel выпустила вторую бету Аметиста с интеллисэнс для ActionScript 3 и MXML.
Апдейты внизу.
Пара слов.
Как писал Рост, Amethyst - еще один плагин для Microsoft Visual Studio. Замечу, что он бесплатный (Amethyst Personal Edition) и ставится на бесплатную версию VS Shell, то есть по факту, чтобы использовать Amethyst как альтернативный Flex IDE, со вкусом от Microsoft, нужно вложить 0 денег.
Как я уже упомянул, Amethyst может быть установлен либо на коммерческую версию VS 2008, либо на специальную версию Visual Studio Shell, которая исключает поддержку основных языков от Microsoft. Другими словами, вы получаете ту же функциональность что и в “Express” версии VS, но без поддержки C#, C++ и VB .NET. Из-за конструктивных особенностей VS Express не поддерживает интеграцию со сторонними инструментами, поэтому Amethyst там не доступен.
Установка
Для инсталляции вам необходимы:
- Shell(free) или любая коммерческая версия Visual Studio 2008.
- Flex SDK и Flash 9/10.
- Java с Microsoft C библиотекой msvcr71.dll.
- И сам Amethyst Beta 2 инсталлятор.
В настоящее время доступны лишь инструкции по инсталляции, но в этом году планируется выпуск более продвинутого инсталлятора для Amethyst с возможностью доставки Visual Studio Shell и Flex SDK и прочего.
В будущем, для Amethyst Professional Edition, обещают “high-end” дебаггер и некий design view, с возможность drag-and-drop (в Personal Edition также будет дебаггер, но видимо не такой “high-end”
.
Ну-с, попробуем..
Updated
Для инсталляции необходима Visual Studio 2008 Shell (integrated mode), либо можно скачать all-in-one инсталлятор для Ruby in Steel Personal Edition 2008, в нем уже включена VS 2008 Shell, скинуть ненужные галочки, инсталлировать, а затем доставить Amethyst.
Updated 2
В общем, впечатления никакие особо.
Парочка мелочей, вполне субъективных (может у кого не так?):
- Если сохранить Build-настройки проекта, то при следующем билде полезут ошибки mxml компилятора.
- Дебаггера не обещали пока, на на trace() я (напрасно)надеялся.
- HTML шаблон слегка нервирует, вынуждая все время разрешать отображение ActiveX в IE6.
- Работа интеллисэнс позабавила чутка. Хехе, у <mx:Button/> в mxml не предлагает событие “click” ![]()
Сыровата она (на то и бета).
List растягивающийся под контент. AutoResizeableList.
Дело было ранней зимой и я как обычно засыпал на работе у батареи. Потом я проснулся, умылся, взбодрился и сел писать. К сожалению, я не сильно имею представление на сколько это актуально и свежо подходом к решению проблемы, но, по крайне мере, один человек очень получал от этого много удовольствия ![]()
Кстати, перед тем как начать читать, тем кто мало знаком lifecycle компонентов во Flex, я бы посоветовал познакомиться с этим, вот этим и с тем.
Начнем-с.
Необходимо было сделать компонент List само(ра)стягивающимся под данные по высоте, то есть визуализовать все данные находящиеся в дата провайдере. Если упростить понятие данных, то в этом случае это простой текст, который надо отобразить в полном объеме. Другими словами, в качестве рендерера должен выступать любой контейнер содержащий любой UIComponent, способный отображать не обрезанный многострочный текст.
Листинг 1. Например это такой рендерер:
- <mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml"
- mouseChildren="false">
- <mx:Script>
- <![CDATA[
- ...
- ]]>
- </mx:Script>
- <mx:Canvas width="100%">
- <mx:Text id="tf" width="100%"/>
- </mx:Canvas>
- </mx:VBox>
Если же рендерер не содержит в себе особых компонентов и его размеры можно корректно высчитать за один проход валидации, то вот самый примитивный способ. Выставляем List#variableRowHeight=true (в случае если высота варьируется) и снаружи его можно обсчитать с помощью, например, List#measureHeightOfItems() и делать это на событие FlexEvent.UPDATE_COMPLETE.
Листинг 2. Примерно так, без напильников и предохранителей:
- <mx:Script>
- <![CDATA[
- private function setMeasuredHeight(list:List):void
- {
- var h:Number = list.measureHeightOfItems();
- list.height = h + list.getStyle("paddingTop") +
- list.getStyle("paddingBottom");
- }
- ]]>
- </mx:Script>
- <mx:List id="simpleList"
- width="400" variableRowHeight="true"
- itemRenderer="SimpleRenderer"
- dataProvider="{dp}"
- updateComplete="setMeasuredHeight(simpleList)"/>
Особые компоненты.
Что же такое “особые” компоненты и их особенное поведение? Есть стандартные компоненты, способные отображать многострочный текст на базе flash.text.TextField, но из-за странностей в реализации текстовых полей во FlashPlayer, они становятся не обычными. Для них не достаточно одного прохода валидации. Им нужно два, иначе после первого прохода мы получаем некорректный TextField#textHeight. И то, как такие компоненты с этим живут, можно увидеть, например, в mx.controls.Text#updateDisplayList().
Листинг 3. mx.controls.Text#updateDisplayList() строка 326.
- if (isSpecialCase()){
- var firstTime:Boolean = isNaN(lastUnscaledWidth) ||
- lastUnscaledWidth != unscaledWidth;
- lastUnscaledWidth = unscaledWidth;
- if (firstTime){
- invalidateSize();
- return;
- }
- }
Вкратце, если updateDisplayList компонента произошел в первый раз после апдейта данных (присваивание текста в текстовое поле), пинаем еще один measure и выходим из метода. И только во время следующего measure будет вычислен корректный размер и запущен второй updateDisplayList для коррекции визуального представления. Это незаметно снаружи, в случаях когда мы присваиваем текстовому полю какой-нибудь текст и сразу вызываем validateNow(), то мы получаем правильные размеры, но если заглянуть внутрь, то мы увидим, что measure() и updatedDisplayList() выполняются два раза и происходит это в одном кадре не без помощи LayoutManager-а.
Под проходом валидации я понимаю процесс, который затрагивает по крайней мере один из защищенных методов: commitProperties, measure и updateDisplayList. В нашем случае, нас больше итересуют два последних.
Пара слов о компоненте List.
Если List-у не задана высота напрямую пользователем и не жестко диктуется родительским контейнером, то инициализируясь он всегда вычисляет List#measuredHeight руководствуясь List#rowCount и List#rowHeight. Если они не заданы вручную, то List#measuredHeight будет всегда равен 140 px, потому как высота строки по умолчанию 20 и количество строк по умолчанию 7. В ситуации когда высота List-а задана напрямую (e.g List#height = 320) фаза measure будет пропущена. Если же нет, то мы получим высоту в 140 px независимо от количества данных в провайдере и количества данных в рендерерах, которые поместились в этих 140 px.
Еще немного занудных подробностей о lifecycle компонента List. В первый проход updateDisplayList List создаст и отвалидирует рендереры, но сделает это не корректно. Если остановить валидацию на первом проходе мы увидим примерно неопрятную картину.
Картинка 1.

В нормальных условиях особые рендереры, используя трюк с двойной валидацией, возбуждают List к повторному measure и updateDisplayList (см. Листинг 3). Ко второму updateDisplayList рендереры уже будут иметь корректные measured размеры и там List задаст им правильный лэйаут, то есть им будет выставлена правильная высота и тд.
Картинка 2.

Отмечу также, что мы не выставляем никаких значений для List#rowCount и List#rowHeight, так как они попросту не нужны в случае когда List#variableRowHeight=true. Вообще, фаза measure с вариативным рендерером (для стандартного List-a) на мой вкус довольно бесполезна, потому как уже говорилось выше List#measuredHeight всегда будет высчитана по умолчанию в 140 px. Мы постараемся внести в нее немного больше смысла, заставив List растягиваться под контент по высоте.
Итак. Скрытый рендерер.
Необходимо высчитать размер всех рендереров на базе данных и учесть стили. Так как, например, в самом начале при инициализации в фазе measure List не имеет никаких созданных рендереров, то нужда заставляет нас использовать стандартный внутренний механизм скрытого рендерера. Cкрытый рендерер - это рендерер созданный внутри самим List-ом на базе List#itemRenderer с visible=false. С его помощью List производит необходимые вычисления для лэйаута, например, это касается вертикального скролбара или вычисления List#rowHeight.
Листинг 4. Создание скрытого рендерера внутри компонента List. mx.controls.List#getMeasuringRenderer() строка 1732.
- if (!item) {
- item = createItemRenderer(data);
- item.owner = this;
- item.name = "hiddenItem";
- item.visible = false;
- item.styleName = listContent;
- listContent.addChild(DisplayObject(item));
- measuringObjects[factory] = item;
- }
Кульминация. Проблемные места.
Основная идея очевидна и проста. Необходимо пройти по всем данным пихая их в скрытый рендерер, валидируя и снимая с него measured размеры. Таким образом, складывая measured размеры одного и того же рендерера отвалидированного с разными данными в одну кучу, но с учетом паддингов и хрома, мы получаем общую measured высоту для List-a.
Листинг 5. com.riapriority.vertex.control.AutoResizableList#measure() строка 162.
- for (var i:int = 0; i < max; ++i) {
- // Reusing standart mechanism to get hidden renderer.
- renderer = getMeasuringRenderer(dp[i]);
- rect = validateAndMeasure(dp[i], renderer);
- h += rect.height + paddingTop + paddingBottom;
- }
- // We add top and bottom chrom + bottom offset
- measuredHeight = h + (viewMetrics.top + viewMetrics.bottom) + _bottomOffset;
- measuredMinHeight = measuredHeight;
- shouldBeUpdated = true;
На этом первый проход measure заканчивается и за ним следует updateDisplayList, в котором List по необходимости создает рендереры и выставляет им размеры и позиционирование. Но, как уже говорилось, если мы имеем дело с особыми компонентами, то в этот (первый) проход они отдадут неверные размеры, которые и будут им выставлены List-ом. Но эти рендереры сразу же попытаются возбудить List к повторному measure, который в свою очередь должен вызвать дополнительный updateDisplayList для корректировки лэйаута. В нашем случае с расширенным компонентом так и происходит, за исключением повторного вызова updateDisplayList и это не гут (на сколко это плохо мы наблюдали на картинке..). Почему? Все довольно просто, когда на List-е возбуждается вторая по счету всплывшая фаза measure (measure происходит снизу вверх) с подачи особых рендереров, то List снова обсчитывает все рендереры, снимая с них уже корректные размеры, делая это примерно также как я описал в предыдущем листинге и сравнивает результат со своими, а они были выставлены нами очень правильно в еще первый проход measure. В итоге, оба размера правильные и совпадают, а значит вызова updateDisplayList не произойдет вследствие отпимизационных введений. И это значит то, что в нашей ситуации ортодоксальное использование концепции жизненного цикла компонентов слегка хромает. Ибо основная концепция measure - вычисление предпочтительных размеров. Чтобы исправить положение мы воспользуемся ручным управлением.
Листинг 6.
com.riapriority.vertex.control.AutoResizableList#updateDisplayList() строка 204.
- if (shouldBeUpdated){
- shouldBeUpdated = false;
- callLater(function():void{
- invalidateDisplayListFlag = true;
- validateDisplayList();
- });
- }
Если попытаться пнуть это стандартным образом, например, используя List#invalidateDisplayList(), то фаза updateDisplayList не будет запущена по причине того, что мы в ней находимся в данный момент. Можно запихать List#invalidateDisplayList() в callLater и это будет работать, но мы проиграем в производительности, потому что окажемся в этой фазе только через два кадра, а жуть как хотелось бы на кадр пораньше.
Добавка о производительности.
С таким подходом мы удваиваем все вызовы связанные с валидацией, потому как сначала мы прогоняем валидацию сами, используя скрытый рендерер, а затем List, создавая визуальные рендереры, количество которых также соответствует данным в провайдере (так как мы выставили достаточную measuredHeight), прогоняет валидацию для них. Вообще, в рамках идеи “fit to content” производительность уже страдает, так как создаются все визуальные рендереры соответственно данным и если мы имеем очень много данных, то это опасный подход. В случае очень большого количества данных лучше размазывать процесс во времени и скорее всего не использовать List, но здесь все строго индивидуально. В этом примере, при количестве данных в ~500 единиц, мы получаем довольно ощутимую паузу перед рендерингом контрола на сцену, хотя, с моей точки зрения, даже так вполне бодро.
Другой подход?
Можно было бы избежать удваивание вызовов валидации, используя updateDisplayList для выставления явной высоты List-у и не используя measure вообще, но в это приведет к совсем плачевным результатам. Казалось бы, раз в updateDisplayList происходит создание рендереров для List-а, то вместо скрытого рендерера, для вычисление measured размеров, там можно использовать уже созданные. Но не тут то было. И я думаю, многим известно почему. List - достаточно оптимизированный компонент чтобы создавать ровно столько рендереров, сколько необходимо. И так как в measure высота вычисляется в 140 px, то и рендереров будет создано лимитированное количество. А именно, не больше 15 штук. То есть, при дата провайдере в 16 итемов List создаст 7 рендереров для 7 строк по умолчанию. Затем вычислит количество рендереров для буферизации сверху и снизу (16/2=8), но буферизацию для верха создавать не имеет смысла, так как очевидно, что при создании List-а мы и так будем находиться в крайнем верхнем положении с List#verticalScrollPosition=0.
Листинг 7.
mx.controls.listClasses.ListBase#makeRowsAndColumnsWithExtraRows() строка 1344.
- var desiredExtraRowsTop:int = offscreenExtraRows / 2;
- var desiredExtraRowsBottom:int = offscreenExtraRows / 2;
- offscreenExtraRowsTop = Math.min(desiredExtraRowsTop, verticalScrollPosition);
В следствии чего получаем 7+8=15. Соответственно, для провайдера длинной в 58 итемов, при проходе всех данных и снятии разсера через getExplicitOrMeasuredHeight, мы получим ексепшен на 36 рендерере, так как его не будет существовать. А значит, что этот подход нас не удовлетворяет и даже с выставленным List#offscreenExtraRowsOrColumns = List#dataProvider.length.
Пара нюансов.
При тестировании возникла пара проблем с drag-and-drop. Нельзя было перетащить итем в самый низ List-а. В нашем компоненте мы имеем свойство AutoResizableList#bottomOffset, которое позволяет задавать пространство снизу. Оно необходимо для возможности сделать drag-and-drop вниз List-a, а также предотвращает ошибку в случае перетаскивания в пустой List. Вы может продемонстрировать это себе сами, просто перетащите все из левого List-а в правый, а затем попытайтесь перетащить один из итемов обратно (эксепшен тихо проглатывается при отсутствии дебаг версии FlashPlayer). Хотя, этого эксепшена можно избежать перекрыв метод List#showDropFeedback() и пристрочив заплатку.
Наконец-то его можно потрогать и все остальное уже не важно:
Странно, но внутри тоже получилось много комментов.
Скачать пример отсюда. view source.
И наконец.
Eсли вам нет нужды использовать пряники компонента List, например, API по работе с данными и визуалом, drag-and-drop и etc, то тогда не надо заниматься извращениями, а надо просто использовать стандартный mx.controls.VBox и побыстрей забыть это стремное многобуквие ![]()
Ну а теперь самое главное.
С наступающим Первым днем года!
Горячо жму вас!
Пейджинг на лету
На одном из проектов возникла производственная необходимость отобразить данные в виде таблицы. Казалось бы проще репы, но избыточность данных бесстыдно ставила контрол на локти. Стоит отметить, что он имел 46 колонок и 100 строк, количество же данных могло исчисляться десятками тысяч. Упомяну, что интеграция происходила на базе веб-сервисов, так что LCDS нам только снился.
Очевидно, что в нашем случае, чтобы поднять производительность необходимо было лимитировать количество данных в провайдере. Таким образом, накопав в доках пару красноречивых намёков на паджинацию тут и тут , мы начали погружение.
Сверхзадачей было добиться относительной нативности скроллинга без скачков и излишнего напряжения (чтобы недостающие данные, по пришествии, не вставали в нулевой индекс, а аккуратно подлипали в конец).
Прольем же немного света на базовые механизмы паджинации поддерживаемые стандартными компонентами, являющими собой производные List-а, а именно HorizontalList, TileList, DataGrid, Menu, Tree, ну и List конечно. Техника базируется на использовании класса mx.collections.errors.ItemPendingError. Выбросив такую ошибку из метода getItemAt() коллекции, которая является дата-провайдером для List-based копонент, можно заставить контрол впасть в ожидание pendent данных. Абстрактный например:
- public class PagingCollection implements IList
- {
- override public function getItemAt(index:int, prefetch:int=0):Object
- {
- if (index < 0 || (_length != -1 && index >= _length))
- throw new RangeError();
- if(!cache.hasItem(index))
- throw new ItemPendingError("");
- return cache.getItemAt(index);
- }
- }
При этом контрол поймав эту ошибку подписывается к ней с помощью респондеров и ждет их срабатывания. Код в листинге ниже взят из метода scrollHandler() класса mx.controls.List (line 1451).
- try
- {
- if (!iteratorValid)
- iterator.seek(CursorBookmark.FIRST, pos);
- else
- iterator.seek(CursorBookmark.CURRENT, delta);
- if (!iteratorValid)
- {
- iteratorValid = true;
- lastSeekPending = null;
- }
- }
- catch(e:ItemPendingError)
- {
- lastSeekPending = new ListBaseSeekPending(CursorBookmark.FIRST, pos);
- e.addResponder(new ItemResponder( seekPendingResultHandler, seekPendingFailureHandler, lastSeekPending));
- iteratorValid = false;
- }
Когда данные готовы, дергаем за респондеры из нутра кастомной коллекции и контрол, в свою очередь, начинает дергать getItemAt() используя индексы paged данных. Абстрактный код ниже (в контексте кастомной коллекции) иллюстрирует обход всех респондеров, что фактически приводит к вызову обработчиков, добавленных контролом в момента отлова ItemPendingError (e.g. seekPendingResultHandler из предыдущего листинга).
- protected function onResult(val:PagingResult):void
- {
- cache.addItems(val.result);
- itemPendingError.responders.forEach(
- function(responder:IResponder, index:int, arr:Array):void
- {
- responder.result(val.result);
- });
- }
Интересно упомянуть, что начинается всё с обращения к геттеру length() коллекции. То есть при присвоения нового провайдера Листу, чтобы корректно отрисовать скролл, первым делом он пытается узнать его размер. Вначале, при отсутствии данных, возвращаем -1. Это важно чтобы отличить начальное состояние от возможно пустого массива в полученных данных в дальнейшем и делаем начальный запрос.
- override public function get length():int
- {
- if (_length == -1 && !lengthPending)
- {
- lengthPending = true;
- requestData();
- }
- return _length;
- }
Узнав общее колличество выставляем длину и пинаем контрол с помощью события CollectionEvent.COLLECTION_CHANGE.
- protected function onResult(val:PagingResult):void
- {
- cache.addItems(val.result);
- if(_length < 0)
- {
- _length = val.totalSize;
- lengthPending = false;
- var event:CollectionEvent = new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
- event.kind = CollectionEventKind.RESET;
- dispatchEvent(event);
- }
- }
Весьма отмечу также, что интенсивное таскание тумбы в режиме liveScrolling представляло из себя совершенно удручающее зрелище, даже с задержкой данных в 1 секунду, контрол выбрасывал ItemPendingError. Отнаследовавшись и перекрыв scrollHandler() c hot-plug мы вернули его к жизни:
- public class ListExtended extends List
- {
- public function ListExtended()
- {
- super();
- }
- override protected function scrollHandler(event:Event):void
- {
- /*
- value can be null and it
- results in throwing our custom
- PagingCollectionItemPendingError.
- In overridden method it is not
- wrapped in try/catch block.
- */
- if (iterator.bookmark.value)
- super.scrollHandler(event);
- }
- }
Творение сие не есть алмаз, а лишь фантазия воображения, но мы счастливы и живы, и солнце, по обыкновению, снова греет наши затылки.
Довольно живописную картину можно получить ознакомившись с приведенными выше ссылками и исходниками ниже.
Качать пример отсюда (некоторые комментарии в коде на англицком). view source.