13 May 2012

Drag and drop изображений во Flash в браузере

Website development
В ходе тестирования своего сервиса собралось достаточное количество фидбека, что олдскульный способ загрузки файлов в приложение не уносит. Люди хотели drag and drop и пытались перетащить картинки прямо с рабочего стола. Приложение у нас занимает весь экран браузера и написано на флеше, так что прямого способа решить задачу не нашлось.

Поразмыслив и погуглив решили реализовать D&D хотя бы для хрома вот так:
Когда пользователь переключается с вкладки с приложением, т.е. она теряет фокус, поверх флешки накладывается div на который навешано событие отлова дропнутых файлов.
Затем через ExternalInterface изображение в виде ByteArray передается во flash, где оно декодируется и отображается.


Отображение div поверх flash

#over_holder_full
{
  background-color: #eee;
  opacity: 0.6;
  position: absolute;
  width: 100%;
  height: 100%;
  z-index: 7;
  display: none;
}

#flash_conainer
{
  width: 100%;
  height: 100%;
  position: absolute;
  z-index: 0;
}

<div id="over_holder_full"></div>
<div id="flash_conainer">
<div id="myContent">
<h1>Alternative content</h1>			
</div>
</div>


Показываем #over_holder_full при потере фокуса, на который навешан drag and drop

window.addEventListener('blur', function() {
   fullDragArea.style.display = "block";
   console.log("not focused");
});

window.addEventListener('focus', function() {
  fullDragArea.style.display = "none";
  console.log("focus");
});


Отлов файла дропнутого на #over_holder_full

fullDragArea = $id("over_holder_full");
      
// is XHR2 available?
var xhr = new XMLHttpRequest();
if (xhr.upload) {
      fullDragArea.addEventListener("dragover", FileDragHover, false);
      fullDragArea.addEventListener("drop", FileSelectHandler, false); // <-- то что нас интересует
      
     //....
}


Передача изображения во flash

// file selection
function FileSelectHandler(e) {
   fullDragArea.style.display = "none";
   // fetch FileList object
   var files = e.target.files || e.dataTransfer.files;
   //set grey preview in flash
   setPreview(pageX, pageY);  
   // process all File objects
   setTimeout(function(){//<-- задерка нужна чтобы команда setPreview успела отправиться во флешку
      for (var i = 0, f; f = files[i]; i++) {
          ParseFile(f);
      }}, 100);
   }


// output file information
function ParseFile(file) {

var arrBuffer;
    
   if (file.type.indexOf("image") == 0) {
      var reader = new FileReader();
      reader.onload = function(e) {
         //console.log("arrBuffer + " + e.target.result.byteLength);
         var dataview = new DataView(e.target.result);
         var ints = new Int32Array(e.target.result.byteLength / 4); //<-- вручную преобразуем изображение в массив байтов 
         for (var i = 0; i < ints.length; i++) {
            ints[i] = dataview.getInt32(i * 4);//<-- теперь записываем по 4 байта (необходимо чтобы потом во флеше нормально преобразовать в ByteArray)
         }
        
          console.log("ints" + ints.length);
          // display an image
          setImage(pageX, pageY, ints);
      }
      reader.readAsArrayBuffer(file);
   }
}


Функции связывающие JavaScript и Flash

function setImage(pageX, pageY, int32Arr) {
   if (swfReady){
      var res = [];
      for (var i = 0; i < int32Arr.length; i++) {
         res[i] = int32Arr[i]; //<-- необходимое преобразование т.к. через externalinterface можно передавать только примитивные типы данных, коим Int32Array не является
      }
      getSWF("DefaultLauncher").setImage(pageX, pageY, res);// первые два параметры - координаты курсора
   }
}
  
function setPreview(pageX, pageY) {
   if (swfReady)	{
      console.log("setPreview");
      getSWF("DefaultLauncher").setPreview(pageX, pageY);
   }
}


Принимаем изображение на стороне флеша

var exManager:EXManager = new EXManager();
exManager.setImageCallback = function(pageX:Number, pageY:Number, listBytes:Array):void{
				
	var ba:ByteArray = new ByteArray();
	for(var i:int = 0; i<listBytes.length; i++){
		ba.writeInt(listBytes[i]); //преобразуем в ByteArray
	}
				
				
	var myDecoder:JPEGDecoder = new JPEGDecoder(); //пришлось воспользоваться сторонней библиотекой для декодирования картинки из ByteArray
	myDecoder.parse(ba);
	var colorComponents:uint = myDecoder.colorComponents;
	var numComponents:uint = myDecoder.numComponents;
	var pixels:Vector.<uint> = myDecoder.pixels;
				
	var width:uint = myDecoder.width;
	var height:uint = myDecoder.height;
	var bitmap:BitmapData = new BitmapData ( width, height, false );
	bitmap.setVector ( bitmap.rect, pixels );
				
	var bmp:Bitmap = new Bitmap(bitmap);
	var oldW:Number = bmp.width; 
	bmp.width = bmp.width > 500 ? 500 : bmp.width;
	bmp.height = bmp.height*(bmp.width / oldW);
	bmp.x = pageX;
	bmp.y = pageY;
	stage.addChild(bmp);
				
}
				
				
exManager.setPreviewCallback = function(pageX:Number, pageY:Number):void{
	var sp:Sprite = new Sprite();
	sp.graphics.beginFill(0x666666, 0.8);
	sp.graphics.drawRect(0,0,100,80);
	sp.x = pageX;
	sp.y = pageY;
	stage.addChild(sp);
}


Исходники можно скачать тут

Замечание:
Данный пример работает только с изображениями с расширением JPG, но его можно легко расширить на любые другие форматы.

Из-за свойства wmode:opaque, которое необходимо чтобы поверх нее отображать div, не работает скролл во флешке. Если есть решения буду рад их услышать.

Чтобы запустить пример необходимо разместить его хотя бы на локальном сервере.

Update:
Как подсказали, чтобы побороть глюк с событием mouseWheel надо добавить в js следующее:

function wheel(event){
        var delta = 0;
        if (!event) event = window.event; // Событие IE.
        // Установим кроссбраузерную delta
        if (event.wheelDelta) { 
                // IE, Opera, safari, chrome - кратность дельта равна 120
                delta = event.wheelDelta/120;
        } else if (event.detail) { 
                // Mozilla, кратность дельта равна 3
                delta = -event.detail/3;
        }
        // Вспомогательня функция обработки mousewheel
        if (delta && typeof wheelHandle == 'function') {
                wheelHandle(delta);
                // Отменим текущее событие - событие поумолчанию (скролинг окна).
                if (event.preventDefault)
                        event.preventDefault();
                event.returnValue = false; // для IE
        }
  }

  function wheelHandle(delta){
    if (swfReady){
      getSWF("DefaultLauncher").externalMouseWheelHandler(delta);
    }      
  }


function pageInit() {
   jsReady = true;
		
   // Инициализация события mousewheel
   if (window.addEventListener) // mozilla, safari, chrome
    window.addEventListener('DOMMouseScroll', wheel, false);
   // IE, Opera.
   window.onmousewheel = document.onmousewheel = wheel;		
}


А так же добавить во flash:
private function externalMouseWheelHandler(delta:int):void {
    var globalPoint:Point = new Point(stage.mouseX, stage.mouseY);
    var objects:Array = stage.getObjectsUnderPoint(globalPoint);
    
    if (!objects || !objects.length) {
        return;
    }
            
    var target:DisplayObject = objects[objects.length - 1] as DisplayObject;
    if (!target) {
        return;
    }
    
    target = (target is InteractiveObject) ? target : target.parent;
    if (!target) {
        return;
    }
    
    var localPoint:Point = target.globalToLocal(globalPoint);
    var mouseEvent:MouseEvent = new MouseEvent(MouseEvent.MOUSE_WHEEL);
    mouseEvent.localX = localPoint.x;
    mouseEvent.localY = localPoint.y;
    mouseEvent.delta = delta;
    target.dispatchEvent(mouseEvent);
}


Update 2:
После некоторых эксперементов стало понятно что лучше передавать данные кусками, а так же считавыть из буфера не через getInt32 а через getInt8, т.к. длина файла не обязательна кратна 4-ем, как предполагается в первом случае.

JavaScript
function setImage(bufer) {
  if (swfReady){
    var dataview = new DataView(bufer);
    var byteLength = bufer.byteLength; // убрали деление на 4

    var numBytes = 10000;
    var startPos = 0;
    var finishPos;
    var i = 0;
    while(i < byteLength){
      finishPos = ((startPos + numBytes) > byteLength) ? byteLength : startPos + numBytes;
      var res = [];
      for (i = startPos; i < finishPos; i++) {
	res[i - startPos] = dataview.getInt8(i); // заменили getInt32 на getInt8
      }
      startPos = finishPos; 
      getSWF().sendData(res); 
    }

    getSWF().sendDataFinish(); 
  }
}
  
function setPreview(pageX, pageY, fileName) {
  if (swfReady){
    getSWF().sendDataStart(pageX, pageY, fileName);
  }
}


ActionScript
_exManager.sendDataStartCb = function(pageX:Number, pageY:Number, fileName:String):void {
	prevtime = getTimer();
};

_exManager.sendDataCb = function(listBytes:Array):void {
	for (var i:int = 0; i < listBytes.length; i++) {
		ba.writeByte(listBytes[i]);// соответственно так же побайтово считываем
	}
};

_exManager.sendDataFinishCb = function():void {
	loadFromByteArray(ba);
	ba.clear();
};
Tags:javascriptflashexternalinterfacedrag-and-drop
Hubs: Website development
+6
1.9k 15
Comments 5