事件の経緯
運営しているサイトでWeb上で画像をトリミングさせる機能(Twitterのプロフィールアイコン設定のときのアレみたいなやつ)を実装しているのですが、
ある日サイトをご利用いただいているユーザ様から
「トリミングしようと思って画像をタッチして移動させようにも動かせないです。 in Android」
というご連絡をいただきました。
はて、手持ちのスマホ(iPhone)で検証してみるも問題なく動かせる・・・何なのか。。
お洒落なサイトが印象的ですね。
www.croppic.net
自宅のテスト用Android実機で試してみる
「確かに動かせない。」
フロント側のメモリ不足あたりを疑いつつ、
なぜだろうなぜだろうと色々試行錯誤の上調べていった結果(プラグインごと乗り換えようともしたので丸3日かかりましたorz)、1件の記事にたどり着きました。
qiita.com
まさかねー!
だってほら、すごくお洒落なサイトだしブランド感も漂ってるし、新卒社会人みたいなコーディングになんてなってないよねー!(かく言う私も上の記事を見るまで意識してできてなかったですがw)
まさかでした。
重厚なmousemoveリスナ内のロジックがそこには広がってました、はい。
そこそこ潤沢なメモリを搭載した媒体なら耐えられそうですが、Android機とかそこらになると固まりそうなメモリ食いしん坊がそこにいました。
修正案
上記のサイトを参考にしつつ、
・レンダリング用にフレームレートを設ける
・スクロール系リスナ内ではイベントの登録のみにする
ように修正してみました。(なお、修正したのはドラッグ系ロジックのみです。多分ここだけの修正で大丈夫だとは思っているのですが・・・お気づきの点があれば教えてくださいm(__)m)
修正箇所 #454あたり
initDrag: function() {
var that = this;
that.img.on("mousedown touchstart", function(e) {
var pageX;
var pageY;
var userAgent = window.navigator.userAgent;
if (userAgent.match(/iPad/i) || userAgent.match(/iPhone/i) || userAgent.match(/android/i) || (e.pageY && e.pageX) == undefined) {
pageX = e.originalEvent.touches[0].pageX;
pageY = e.originalEvent.touches[0].pageY;
} else {
pageX = e.pageX;
pageY = e.pageY;
}
var z_idx = that.img.css('z-index'),
drg_h = that.img.outerHeight(),
drg_w = that.img.outerWidth(),
pos_y = that.img.offset().top + drg_h - pageY,
pos_x = that.img.offset().left + drg_w - pageX;
that.img.css('z-index', 1000).on("mousemove touchmove", function(e) {
var imgTop;
var imgLeft;
if (userAgent.match(/iPad/i) || userAgent.match(/iPhone/i) || userAgent.match(/android/i) || (e.pageY && e.pageX) == undefined) {
imgTop = e.originalEvent.touches[0].pageY + pos_y - drg_h;
imgLeft = e.originalEvent.touches[0].pageX + pos_x - drg_w;
} else {
imgTop = e.pageY + pos_y - drg_h;
imgLeft = e.pageX + pos_x - drg_w;
}
that.img.offset({
top:imgTop,
left:imgLeft
}).on("mouseup", function() {
$(this).removeClass('draggable').css('z-index', z_idx);
});
if(that.options.imgEyecandy){ that.imgEyecandy.offset({ top:imgTop, left:imgLeft }); }
if (that.objH < that.imgH) {
if (parseInt(that.img.css('top')) > 0) { that.img.css('top', 0); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', 0);}}
var maxTop = -( that.imgH - that.objH); if (parseInt(that.img.css('top')) < maxTop) { that.img.css('top', maxTop); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', maxTop); }}
}else{
if (parseInt(that.img.css('top')) < 0) { that.img.css('top', 0); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', 0); }}
var maxTop = that.objH - that.imgH; if (parseInt(that.img.css('top')) > maxTop) { that.img.css('top', maxTop);if (that.options.imgEyecandy) {that.imgEyecandy.css('top', maxTop); }}
}
if (that.objW < that.imgW) {
if( parseInt( that.img.css('left')) > 0 ){ that.img.css('left',0); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', 0); }}
var maxLeft = -( that.imgW-that.objW); if( parseInt( that.img.css('left')) < maxLeft){ that.img.css('left', maxLeft); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', maxLeft); } }
}else{
if( parseInt( that.img.css('left')) < 0 ){ that.img.css('left',0); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', 0); }}
var maxLeft = ( that.objW - that.imgW); if( parseInt( that.img.css('left')) > maxLeft){ that.img.css('left', maxLeft); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', maxLeft); } }
}
if (that.options.onImgDrag) that.options.onImgDrag.call(that);
});
}).on("mouseup", function() {
that.img.off("mousemove");
}).on("mouseout", function() {
that.img.off("mousemove");
}).on("touchend", function() {
that.img.off("touchmove");
});
}
修正後
initDrag: function() {
var that = this;
var fps = 30;
var frameTime = 1000 / fps;
var isAnimated = false;
var lastTouchInfo = null;
var userAgent = window.navigator.userAgent;
var z_idx = null,
drg_h = null,
drg_w = null,
pos_y = null,
pos_x = null;
that.img.on("mousedown touchstart", function(e) {
e.preventDefault();
if (userAgent.match(/iPad/i) || userAgent.match(/iPhone/i) || userAgent.match(/android/i) || (e.pageY && e.pageX) == undefined) {
lastTouchInfo = e.originalEvent.touches[0];
}
else{
lastTouchInfo = e;
}
isAnimated = true;
pageX = lastTouchInfo.pageX;
pageY = lastTouchInfo.pageY;
z_idx = that.img.css('z-index');
drg_h = that.img.outerHeight();
drg_w = that.img.outerWidth();
pos_y = that.img.offset().top + drg_h - pageY;
pos_x = that.img.offset().left + drg_w - pageX;
dragAnimation();
});
that.img.on("mousemove touchmove", function(e) {
if (userAgent.match(/iPad/i) || userAgent.match(/iPhone/i) || userAgent.match(/android/i) || (e.pageY && e.pageX) == undefined) {
lastTouchInfo = e.originalEvent.touches[0];
}
else{
lastTouchInfo = e;
}
});
that.img.on("mouseup touchend touchcancel", function() {
isAnimated = false;
lastTouchInfo = null;
});
function dragAnimation(){
if (!isAnimated) {
return;
}
var imgTop = lastTouchInfo.pageY + pos_y - drg_h;
var imgLeft = lastTouchInfo.pageX + pos_x - drg_w;
that.img.offset({
top:imgTop,
left:imgLeft
}).on("mouseup", function() {
$(this).removeClass('draggable').css('z-index', z_idx);
});
if(that.options.imgEyecandy){ that.imgEyecandy.offset({ top:imgTop, left:imgLeft }); }
if (that.objH < that.imgH) {
if (parseInt(that.img.css('top')) > 0) { that.img.css('top', 0); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', 0);}}
var maxTop = -( that.imgH - that.objH); if (parseInt(that.img.css('top')) < maxTop) { that.img.css('top', maxTop); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', maxTop); }}
}else{
if (parseInt(that.img.css('top')) < 0) { that.img.css('top', 0); if (that.options.imgEyecandy) { that.imgEyecandy.css('top', 0); }}
var maxTop = that.objH - that.imgH; if (parseInt(that.img.css('top')) > maxTop) { that.img.css('top', maxTop);if (that.options.imgEyecandy) {that.imgEyecandy.css('top', maxTop); }}
}
if (that.objW < that.imgW) {
if( parseInt( that.img.css('left')) > 0 ){ that.img.css('left',0); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', 0); }}
var maxLeft = -( that.imgW-that.objW); if( parseInt( that.img.css('left')) < maxLeft){ that.img.css('left', maxLeft); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', maxLeft); } }
}else{
if( parseInt( that.img.css('left')) < 0 ){ that.img.css('left',0); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', 0); }}
var maxLeft = ( that.objW - that.imgW); if( parseInt( that.img.css('left')) > maxLeft){ that.img.css('left', maxLeft); if(that.options.imgEyecandy){ that.imgEyecandy.css('left', maxLeft); } }
}
if (that.options.onImgDrag) that.options.onImgDrag.call(that);
setTimeout(dragAnimation, frameTime);
}
}
どなたかのお役に立てば光栄ですm(__)m
【追記:2019/2/28】
Android機でスクロールできない問題の根本原因違いました。。
どうやら
that.img.on("mousedown touchstart", function(e) {...
...
that.img.css('z-index', 1000).on("mousemove touchmove", function(e) {...
といった「that.img.on(...」というイベント登録のところに原因があるようです。
アプリケーションのつくりにもよると思いますが、レンダリングする画像データが重かったりすると、that.imgに正常にイベントが登録されないようです。
よって、私の場合は一つ上の上位divにイベントを登録するように修正することで、この問題を回避することができました。