Как стать автором
Обновить

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

Время на прочтение9 мин
Количество просмотров11K
Неделю назад мои знания 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 умел полностью уничтожать ячейки при удалении данных, и потом проводить повторную инициализацию.
Теги:
Хабы:
+34
Комментарии49

Публикации

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн