Adobe открывает RTMP протокол. Wow!
20-го января Adobe анонсировала планы по открытию своего проприетарного протокола RTMP (Real-Time Messaging Protocol) в рамках проекта Open Screen Project.
Спецификация ожидается в первой половине этого года и будет выложена на adobe.com/devnet/rtmp.
Обратный инжениринг со Sparx EA за пару кликов
Баян. Ооочень древний.
Я почти уверен, ведь вы уже давно знаете, что Sparx Enterprise Architect поддерживает ActionScript 2/3. Костя писал про это еще в неблизком 2006 году. Кроме всего навороченного, он умеет генерировать код из UML-схем. Но субъективно гораздо более интересной была фича “Reverse Engineering and Synchronizing", которая позволяет генерировать UML-схемы на базе существующих исходников, с поддержкой различных языков: ActionScript, C, C#, C++, Delphi, Java, PHP, Python, Visual Basic, Visual Basic.NET. Также, например, можно преобразовать ActionScript код в любой из поддерживаемых языков и vice versa, импортировать и генерировать WSDL, импортировать модель базы данных через ODBC и много всего не менее прекрасного.
А сейчас, как можно проще и быстрее, сгенерируем UML-схему на базе ActionScript классов. Качаем триал (порядка 29MB ) и запускаем.
В картинках.
Создаем новый проект и выбираем модель “Class".
step 1.

Можно, например, сразу создать новый package и импортировать туда папку с исходным кодом. Правый клик на package -> Code Engeneering -> Imoprt Source Directory, но сделаем же это пошагово. И не будем особо придираться к структуре папок по умолчанию, а оставим пока все как есть. Двойным щелчком открываем диаграмму System.
step 2.

Удаляем ненужный хлам и правым кликом выбираем импорт.
step 3.

После того как выбранные классы/интерфейсы импортировались и установлены зависимости, можно причесать схему автоматикой.
step 4.

При импорте папки с кодом целиком (Code Engeneering -> Imoprt Source Directory), возникает сложность с отображением всех схем на одной диаграмме, так как EA создает отдельную диаграмму со схемами для каждой вложенной папки (если в папке нет кода, то диаграмма будет пуста). Не искал как их объединить. Вдруг кто знает? Но можно напихать интересующие классы в один .as, импортировать и получить желаемый результат на одной диаграмме, ну или прочувствовать мощь step-а 3. ![]()
Стоит эта штука US $199 за одну Professional Edition.
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.