Pull to refresh

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

Reading time9 min
Views11K
Неделю назад мои знания 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:
Hubs:
+34
Comments49

Articles