Pull to refresh

Загрузчик фотографий как vkontakte на Flex

IT-companies
Неделю назад мои знания action script ограничивались тем, как добавить событие onclick на баннер перед загрузкой в баннерную сеть. В качестве загрузчика файлов я использовал swfupload, и очень не хотел влезать внутрь swf-ника и разбираться в коде. Мне не нравится flash, я ни разу не дизайнер и теряюсь, когда вижу все эти слои, кадры, инструменты для рисования звездочек и motion guides.

Потом я наткнулся на эту эту потрясающе-красивую штуку, и узнал, что есть flex. И что flex — это круто, потому что даже такой супер-начинающий как я, с нуля за несколько дней смог написать загрузчик фотографий с предпросмотром, ресайзом на клиенте и upload-баром, примерно такой, какой используется на сайте vkontakte.ru.

Есть три причины, из-за которых я решил использовать flash для загрузки фотографий. Это FileReference, FileReferenceList и flash.display.Bitmap. В 10-й версии флеш плеера у FileReference появилась функция load(), с помощью которой можно просматривать выбранные фотографии в ролике локально без загрузки на сервер. FileReferenceList позволяет в файловом диалоге с помощью shift-а выбрать сразу несколько фотографий. Bitmap делает ресайз картинок перед отправкой на сервер. Все это нельзя сделать на чистом javascript-е.

Итак, пишем загрузчик фотографий как vkontakte на flex (пошаговое пособие для совсем начинающих).

Прежде всего нужно поставить flex builder 3 (здесь есть версия, которой можно пользоваться 60 дней) и обновить флеш плеер до 10-й версии. Создадим новый flex project. Тип приложения — web application, тип сервера — none. Сразу же нужно исправить компилятор проекта, для этого выбираем Project->Properties->Flex Compiler и меняем параметр «Require Flash Player version» на 10.0.0. Если это не сделать, функция load() у объекта FileReference не будет работать.

Сначала расположим все необходимые элементы на странице, а потом будем писать скрипт. Зададим фиксированные размеры рабочей области (тегу application), внутрь поместим panel, внутрь панели — TileList, ProgressBar и ControlBar с кнопками «Выбрать фотографии», «Начать загрузку» и «Очистить». После TileList добавим HBox c элементами ProgressBar и кнопкой «отмена». Вот что получится.
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="540" height="465">

<mx:Panel title="Загрузка фотографий"
paddingTop="10" paddingLeft="10" paddingBottom="10" paddingRight="10"
width="100%" height="100%">

<mx:TileList
alternatingItemColors="[#FFFFFF,#CCCCCC,#AAAAAA]"
verticalScrollPolicy="on"
columnWidth="120" columnCount="4" rowHeight="110" rowCount="3" />

<mx:HBox horizontalAlign="center" width="100%">
<mx:ProgressBar />
<mx:Button label="Отмена" />
</mx:HBox>

<mx:ControlBar horizontalAlign="right">
<mx:Button label="Выбрать фотографии" />
<mx:Button label="Начать загрузку" />
<mx:Button label="Очистить" />
</mx:ControlBar>
</mx:Panel>
</mx:Application>


* This source code was highlighted with Source Code Highlighter.


Скроем ProgressBar и будем показывать его только когда нажимаем на кнопку «начать загрузку (visible =»false"). Сделаем enabled=«false» у кнопок «начать загрузку» и «очистить» (они будут активны только когда список фотографий непуст). Добавим id-шники ко всем важным элементам.

Добавим тег <mx:Script> и будем в нем писать код. Подключим flash.net.FileReferenceList и mx.collections.ArrayCollection и заведем главную глобальную переменную photos типа ArrayCollection.
<mx:Script>
<![CDATA[

import flash.net.FileReferenceList;
import mx.collections.ArrayCollection;

[Bindable]
private var photos:ArrayCollection = new ArrayCollection;

]]>
</mx:Script>


* This source code was highlighted with Source Code Highlighter.

[Bindable] означает, что переменная photos может быть связана с другими элементами. Укажем ее в качестве dataProvider-а к TileList, и тогда все изменения в массиве photos будут автоматически отражаться в TileList.

Добавим глобальную переменную frList типа FileReferenceList. На клик по кнопке «Выбрать фотографии» добавим функцию selectPhotos. Для того, чтобы отлавливать событие, когда в frList.browse() в файловом диалоге завершается выбор фоток, у frList нужно указать addEventListener(Event.COMPLETE,addPhotos). Необязательным параметром функции frList.browse() служит массив объектов типа FileFilter, там можно задать маску для названий файлов. Наконец напишем функцию addPhotos, которая будет перебирать все файлы, выбранные в frList.fileList и добавлять их в массив photos. Вот что получится.
import mx.events.CollectionEvent;
import flash.net.FileReferenceList;
import mx.collections.ArrayCollection;

[Bindable]
private var photos:ArrayCollection = new ArrayCollection;
private var frList:FileReferenceList = new FileReferenceList;

private function init():void
{
frList.addEventListener(Event.SELECT,addPhotos);
}

private function selectPhotos():void
{
frList.browse([new FileFilter("Изображения jpeg","*.jpg;*.jpeg")]);
}

private function addPhotos():void
{
for (var i:uint = 0; i < frList.fileList.length; i++)
{
var elem:Object = new Object;
elem.fr:FileReference = FileReference(frList.fileList[i]);
photos.addItem(elem);
}
}

* This source code was highlighted with Source Code Highlighter.

Нужно, чтобы кнопки «Начать загрузку» и «Отмена» были активны только когда массив photos не пуст. В Функцию инициализации добавим обработчик:
photos.addEventListener(CollectionEvent.COLLECTION_CHANGE,function()
{
startUploadButton.enabled = (photos.length>0);
clearPhotosButton.enabled = (photos.length>0);
});

* This source code was highlighted with Source Code Highlighter.

Теперь напишем itemRenderer для нашего TileList. itemRenderer отвечает за то, каким образом показывать элемент массива photos в ячейке TileList. Вынесем код itemRenderer-а в отдельный MXML компонент. Сделаем File->New->MXML Component с именем photoThumb, based on Canvas width 120 height 110. В TileList добавим свойство itemRenderer со значением photoThumb.

Теперь будем писать код компонента. Сделаем, чтобы при наведении на каждую фотографию на ней возникала панель с управляющими кнопочками (у нас пока будет только одна кнопка «удалить»). Соответствующий элемент массива, к которому вызывается itemRenderer, находится в глобальной переменной data. У нас каждый такой элемент — это объект с единственным свойством fr типа FileReference. Соответственно, чтобы в ячейке указать имя файла, нужно написать <mx:Label text="{data.fr.name}"/>. Публичные функции родителя вызываются через parentDocument. Вот полный код itemRenderer-а, бекграунд у родительского Canvas-а задан для того, чтобы корректно обрабатывался rollOver.
<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml"
width="120" height="110"
backgroundColor="#FFFFFF" backgroundAlpha="0"
rollOver="{controls.visible=true}" rollOut="{controls.visible=false}">

<mx:VBox width="100%" height="75" y="5" horizontalAlign="center">
<mx:Image source="{data.fr.data}" maxWidth="100" maxHeight="75" horizontalAlign="center" verticalAlign="middle" />
</mx:VBox>
<mx:Label text="{data.fr.name}" width="100%" truncateToFit="true" bottom="0" textAlign="center" />

<mx:VBox id="controls" visible="false" y="65" right="10" horizontalAlign="right">
<mx:Button label="X" click="parentDocument.clearPhoto(data)" fontSize="6" width="30" height="15" />
</mx:VBox>
</mx:Canvas>


* This source code was highlighted with Source Code Highlighter.

Все работает кроме того, что при выборе больших фоток они не успевают загрузиться, и TileList их не показывает. Чтобы это исправить, добавим в функцию addPhotos обновление TileList после загрузки каждой фотографии:
private function addPhotos(e:Event):void
{
for (var i:uint = 0; i < frList.fileList.length; i++)
{
var elem:Object = new Object;
elem.fr = FileReference(frList.fileList[i]);
elem.fr.load();
elem.fr.addEventListener(Event.COMPLETE,refreshThumb);
photos.addItem(elem);
}
}

private function refreshThumb(e:Event):void
{
photosList.invalidateList();
}


* This source code was highlighted with Source Code Highlighter.

Добавим простые функции удаления всех фоток и удаления выбранной фотки, используя методы removeAll, removeItemAt и getItemIndex у класса ArrayCollection. При этом функция, которая вызывается из itemRender-а, должна быть публичной.
private function clearPhotos():void
{
photos.removeAll();
}

public function clearPhoto(data:Object):void
{
photos.removeItemAt(photos.getItemIndex(data));
}

* This source code was highlighted with Source Code Highlighter.

На данном этапе получено работающее приложение, которое показывает иконки файлов и работает локально. Мультиселект при выборе файлов тоже работает. Осталось добавить загрузку выбранных файлов на сервер и задействовать ProgressBar. Это просто. При нажатии на кнопку «Начать загрузку» будем брать первый элемент из photos, создавать URLRequest, вызывать у FileReference метод upload и добавим event listener-ы, которые будут управлять прогресс-баром и отслеживать, когда файл загрузится. После загрузки удаляем первый элемент из photos и запускаем все заново. Вот код:
private function startUpload():void
{
photosProgressContainer.visible = true;

var fr:FileReference = photos.getItemAt(0).fr;
fr.cancel();
fr.addEventListener(ProgressEvent.PROGRESS,uploadProgress);
fr.addEventListener(DataEvent.UPLOAD_COMPLETE_DATA,uploadComplete);
fr.upload(new URLRequest("http://ragneta.com/tests/flexupload/upload.php"));
}

private function uploadProgress(e:ProgressEvent):void
{
photosProgress.setProgress(e.bytesLoaded,e.bytesTotal);
}

private function uploadComplete(e:DataEvent):void
{
photos.removeItemAt(0);
if (photos.length > 0)
startUpload();
else
photosProgressContainer.visible = false;
}


* This source code was highlighted with Source Code Highlighter.

На этом все, здесь полный код: Uploader.mxml, photoThumb.mxml. Дальше в ролик нужно передавать какую-нибудь авторизацию, я передаю идентификатор сессии через flashvars, соответственно во flex переданные переменные находятся в массиве Application.application.parameters. Затем в инициализации делаю HTTPRequest, отправляю сессию и получаю имя юзера, его альбомы и все остальное. Также нужно отлавливать ошибки и исключения, разбирать ответ сервера итд. Здесь написан только необходимый минимум.

Если нет необходимости загружать исходники фотографий, можно делать ресайз до отправки на сервер. Делается с помощью flash.display.Bitmap, вот пример.

В процессе подготовки поста обнаружил странный баг. Прогресс-бар и событие FileReference.cancel() некорректно работают на WinVista + FF 3.5.7 + Flash Player 10.0.42, но на аналогичном WinXP + FF 3.5.7 + Flash Player 10.0.42 все хорошо.

Также пока не решил проблему с поворотом изображений. Во-первых не нашел, как делать анимированное преобразование через задание новой image.transform.matrix. Вторая проблема — мой Rotate применяется не к данным, которые передаются в itemRenderer, а к тегу самого itemRenderera. Если повернуть картинку, а потом ее удалить и загрузить на ее место новую, она окажется повернутой. При этом событие initialize каждой ячейки происходит только один раз. Приходится в itemRenderer на dataChange вешать поворот в 0, хотя хватило бы, если бы TileList умел полностью уничтожать ячейки при удалении данных, и потом проводить повторную инициализацию.
Tags:flexuploaderfilereferenceзагрузка картинок
Hubs: IT-companies
Total votes 92: ↑63 and ↓29 +34
Views6K

Comments 49

Only those users with full accounts are able to leave comments. Log in, please.

Popular right now

Top of the last 24 hours