iOS Safariでのclickとtouchendの挙動について


最近、業務でiPad用のウェブサイトを作っているのですが、タッチ時の処理を、clickだと挙動がワンテンポ遅れるということでtouchendを使っているところがあります。ところがありますというのは、touchendだと、うまくいかないところがあったためです。

というわけで、わかった範囲でiOSでのclickとtouchendの挙動をまとめてみます。

clickは反応に少し時間がかかる

最初に書いたように、iOSのclickのイベントはタッチして少し時間がたってから発生します。これは、ダブルタップなどのイベントが発生する可能性を考慮してのことのようです(参考:うのらぼ。 – 【JS】iPhoneでタップの反応が遅いと感じたら)。

その要素上でスクロールすると、clickだと反応しないが、touchendは反応してしまう

ある要素上に指をおいてスクロールすると、そのclickイベントは発動しませんが、touchendイベントは発動するようです。そりゃそうだろ、と思うかもしれませんが、touchendイベントで何か処理をする要素上でスクロールした場合はそのイベントを発動してほしくないといった場合の対処方法はちょっと面倒そうです(touchstartとtouchendの両方でウィンドウ位置を見て変わっていたら発動しない、というようなことをすれば実現できるかもしれません)。

clickはその要素上ではなく、その近辺でのタッチでも発動してしまう

どうやら、clickの判定範囲はiOSで大きめにとられているらしく、例えばclickでイベントが発動する要素のすぐ下の要素はtouchendでイベントが発動する要素。なんてものを作っていると、touchendイベントが起こるほうの上の方をタッチすると、両方のイベントが発動してしまいます。
click,touchendで背景変化
例えば、上記のサンプルページの最初のtouchendと書かれた青い要素の、”touchend”を書かれている部分をタッチすると、タッチした場所の背景が黒くなり、少し遅れて上の”click”と書かれた要素の背景も黒くなってしまいます。
ただ、これを、touchendのイベントを呼んだ時に、『event.preventDefault()』を呼んでイベント遷移を止めることで簡単に回避できました。

touchendした後すぐにclickイベントの要素が同じ位置に置かれると、そのclickイベントが発動する

ちょっと意味がわからないかもしれませんが、例えばある箇所をタップすることで、フィルターをクリックすると閉じるモーダルウィンドウが表示されるような状況があるとすると、touchendするとすぐにその上にフィルターが表示され、つづいてそのフィルターのクリックイベントが発動してモーダルウィンドウが閉じてしまうという状況になってしまいます。これも、『event.preventDefault()』を呼ぶと回避できます。
click,touchendでモーダルウィンドウ表示
なお、フィルターのイベントもclickではなくtouchendとすると、フィルターが閉じたところにclickでイベントが発動する要素があるとそのイベントが発動してしまいます。

touchendでアラートを表示するようなイベントを実行した後、touchendでイベントが実行する要素を触れるだけでまたアラートが表示されてしまう

これも意味がわからないかもしれません。簡潔に伝えたいことを伝えれる人間になりたいものです・・・。これは、バグだと自分は思っているのですが、touchendでアラートを表示するようにし、その要素をタップした後、再びその要素に触れるだけで再度アラートが表示されてしまうのです(触れているだけなので、touchendイベントはまだ起こらないはず)。自分自身ではなく、アラートを表示させた後、他のtouchendイベントを実行する要素にも触れただけでアラートが表示されてしまいます。どうも、touchendとalertが干渉しちゃってるようなのですが、原因はわからず。対処方法としては、setTimeoutで非同期にアラートを表示するようにすると、上記のような問題は起こらなくなります(参考:[iPhone]javascriptのtouchendイベントがおかしい。 WEBアプリケーション研究室 開発ノート)。
click,touchendでアラート表示
ちなみに、なぜか自分の使っているタッチパネル対応のWindows7のChromeで上記を試してみたところ、Chromeが落ちました。まるでブラウザを閉じる処理を実行したかのように普通に。

こうやって見ていくと、だいたいはtouchendでも対処方法はありそうですね。実は今回、ブログに書く上でいろいろ調べて対処方法があると気づいたところもあるので、clickとしていたところをtouchendに変えてみようと思います。

Bootstrap3のAffixとScrollspyを試してみた


就職してもうすぐ3年になりますが、初めて病欠しました。17日の木曜日から熱と下痢でダウンしており、丸々二日パソコンさえ触らないという状況でした。

それはともかく、Bootstrapの公式サイトにもあるサイドバーのメニュー。
affix

スクロールして一番上まで来たら追従するようになり、さらにヘッダーまでくるとその位置でストップするような動きになっています。そして、そのメニューはメインの内容に対応して現在の位置がどこか示してくれます。前者はAffix、後者はScrollSpyという方法を使うと実装できるようです。

というわけで作ってみたのそのメモ。

HTML

CSS

JavaScript

3~19行目がAffixの記述(といっても、console.logをやっているだけのところは必要ないですが)、21行目がScrollSpyの記述です。

実行サンプル:Bootstrap3のAffixとScrollspyを試してみた
ブラウザの高さを狭くして、一番下までスクロールしたらフッターの上でメニューの位置がストップするのがわかると思います(とはいっても、Chromeでしか動作確認してないのですが・・・)。

以下、作成する際に手こずったこと。

はじめ、スクロールを一番下までやった後にスクロールを上に戻すと、なぜかフッターの直前でメニューがストップしたままになってしまっていました(一番上までやるともとに戻る)。とりあえず、Affixにはいろいろイベントが用意されているようなので、affix.bs.affix(affixが発生する直前に発生)、affixed.bs.affix(affixが発生した直後に発生)、affix-bottom.bs.affix(affixが最下部に到達した時に発生)、affixed-bottom.bs.affix(affixが最下部に到達した直後に発生)というイベントを呼ぶとそれぞれのイベント名をコンソールに吐き出すようにしてみることに。すると、一番下までスクロールした時に『affix-bottom.bs.affix』『affixed-bottom.bs.affix』という順番に呼ばれた後、もう一度上にスクロールすると『affix.bs.affix』『affixed.bs.affix』が呼ばれるのだけれども、またそのすぐ後に『affix-bottom.bs.affix』『affixed-bottom.bs.affix』が呼ばれているのを確認。この中間の『affix.bs.affix』『affixed.bs.affix』が呼ばれた時点でのメニューの属性を確認してみるとstyleのtopがaffix-bottomの時のままになっていました。いろいろ調べて原因が分からなかったのでとりあえず『affix.bs.affix』が呼ばれた時点でtopを0にするようにしています(上記JavaScriptのコードの10行目)。でも、こんなことやってるところなさそうなので、やっぱり自分の何かが間違っているのだと思います。

ちなみに、『affix.bs.affix』とかのイベントは古いBootstrap3のバージョンでは動いてくれませんでした。今回のサンプルは現在最新のバージョン3.1.1で試しています。後、CSSの指定も意外と重要っぽいです。クラスがaffix-topの時の指定、クラスがaffixの時の指定、クラスがaffix-bottomの時の指定はちゃんと書いたほうがよさそうでした(上記CSSのコードの16行目から25行目)。

ScrollSpyのほうは、たいして手こずることはなかったのですが、1つ手こずったのがメニューのクラスに”nav”をつけなければいけないということに最初気づかず、迷いました。なんでnavクラスだけにしてるんだろう・・・。

余談ですが、Affixの機能は一般的にスティッキーサイドバーと呼ばれています。予想できるかもしれませんが、メニューではなくヘッダー追従型だと、スティッキーヘッダーという名前になります。ScrollSpyもBootstrap独自の名前っぽいので、一般的な呼び名というのがあるのかもしれません。

canvasのお絵描きツールに拡大機能をつけてみる


久々にcanvasのお絵描きツールに機能を追加してみた。今回追加した機能は拡大機能。

最初は、拡大画像用のcanvasを新たに作成してそこにdrawImageメソッドを使って、拡大した部分を描画し、終わったらその拡大画像用のcanvasを逆にdrawImageメソッドで元のcanvasに描画したらいけるかなぁ・・・。なんて思っていたのですが、どうにもそのやり方に少し抵抗があってなかなか手につけられずにいました。『canvas 拡大』と検索するとcanvasのtransformメソッドについてでてきましたが、何か違う気がします。あくまで今から描画する図形に対しての指定みたいなので。

ただ、transformという単語をみてふと思いました。そういえば、CSS3にtransformというプロパティがあったよな・・・と(参考:transform – CSS | MDN)。

結論からいうと、このtransformプロパティを使ってお絵描きツールの拡大機能を実装することができました。というわけで、以下そのソースコードの説明。

とりあえず、今までcanvas部分にはボーダーをつけていたのですが、transformで拡大するとボーダーまで拡大してしまうので、divタグで囲んでそのdivタグにボーダーをつけることに。

HTML

CSS

また、canvasを拡大するとdivタグからはみ出して表示されてしまうので、overflowプロパティをhiddenにして拡大してはみ出した部分は非表示とすることに。そして、transform-originプロパティで拡大するときの原点を要素の左上に指定しています(参考:transform-origin – CSS | MDN)。

つづいてメインの拡大機能の実装部分。拡大機能にはラジオボタン選択ではなく、Hammer.jsを使ってダブルクリック(もしくはダブルタップ)すると2倍に拡大することにしました。

JavaScript

これで拡大はうまくいきました。scaleFactorという変数が拡大率を表しています。もう一度ダブルクリックすると元の大きさに戻るようにもしています。

ただ、これだと拡大した後に描画しようとすると、ペン先の位置が右下にズレてしまうので、描画のx座標とy座標をscaleFactorで除算することに。

以下、動作サンプル。
canvasを使ったお絵描き投稿システム

拡大するときには『塗りつぶし』ではなく、『ペン』を選ぶようにしてください。塗りつぶしを選んでいる状態だとおかしな動きになりました(いまさらだけど、やっぱり普通にラジオボタンで『拡大』を増やしてそこで拡大するかどうかを判定すればよかったかもしれない)。

今回作った機能はcanvasの拡大だけではなく、画像の拡大なんかにも利用できそうです。縮小した画像を表示させといて、ダブルクリックするとその位置を拡大するというような動きにしたり(試してませんが)。

ExifのOrientationを見たうえでcanvasに画像を表示できるJSライブラリ


先日、画像を縮小して表示するページを作成してこのブログで紹介しました(JavaScriptで縮小画像の作成 | while(isプログラマ))。

ただ、このページからiPadやiPod touchで開いたうえで、ファイルの選択から『写真を撮る』を選び、縦向き(縦長)にして写真を撮ってみると、縮小した写真は横向きに表示されてしまいました。これは、写真が標準で縦表示になっているわけではなく、Exif情報のOrientation(向き情報)によって縦方向の写真となっているためにおこります。HTML5のcanvasではどうやらExif情報を考慮してくれないようで、そのため縦に撮った写真が横に表示されてしまったというわけです(それどころか、Orientationによって縦表示とされている写真をimgタグで普通に表示させても、PCのブラウザからは横表示になってしまうという)。

ただ、これでは少し困るのでOrientationを見たうえで実際の方向通りにcanvasに描画する方法はないものかと思って探してみたところ、『JavaScript Load Image』というJavaScriptライブラリを見つけました。相変わらず英語は苦手なので全部わかったわけではないのですが、どうもExif情報を見るライブラリらしく、Exif情報を見たうえで画像をcanvasに表示することができるライブラリだそう。サンプルを使ってみると確かにOrientationで縦向きとなっている画像が、縦向きになってcanvasに描画されました。これはすごいよさそうです。

というわけで、上記ライブラリを用いたうえで、先日の画像縮小ページのJavaScript部分を下記のように変更してみました。

以下、実行サンプル。
画像を縮小して表示

Base64形式のURI Dataの画像をいれても画像は表示されるようなのですが、Exif情報が取得できませんでした(そのため、わざわざ画像をロードしたうえで、その幅を取得している)。

Exif情報が取得できた場合、開発者ツールのコンソール画面に一覧して表示するようにしてみました。PCにExif情報のついた写真がある場合はそれをアップしてみて確認してください。Exif情報と一言でいっても、いろんな情報があるものですね。自分でも試してみましたが、ほとんどundefinedでした(表示している一覧はundefinedとなった情報は省いています)。

HTMLでフリックキーボードを作ろうとして、mouseupとtouchendの挙動の違いを知った


最近、会社でiPad用の業務用ウェブサイトを作っているのですが、ここは数値しか入力しないから数値しか入力できないようにしてほしいとか、ここは英数字とハイフンか入力しないからそれしか入力できないようにしてほしいとかいろいろ要望をいわれるため、HTMLでソフトウェアキーボードを自作してなんとか対応しています(まあ確かに、iPadのソフトウェアキーボードは使いにくい気はする)。
ちなみに、そういったキーボードを作成するのに参考にしたのは下記のサイト。
javascript、大きなテンキーのサンプル: 赤須Muのブログ
jQueryとCSSでソフトウェアキーボードをつくるチュートリアル | IDEA*IDEA
ここからいらないキーを排除したり、必要なキー(全て消すキー等)を入れたり、大きいボタンのレイアウトに調子したりして対応しました。最初、クリックイベントで作ってたんですが、どうもクリックイベントだと挙動がワンテンポ遅れるのでtouchendで実装しました(preventDefault()を使ってダブルタップで拡大しないようにもしている)。

まあそれはともかくとして、そういったキーボードを作っているうちにふと、テンキー入力のキーボードだってやろうと思えばHTMLで作れるんじゃね? と思って簡単なものを作ってみようと思いました。
で、作ってみたので、以下ソースコードと実行サンプル。

実行サンプル:フリックキーボード

61行目でタッチ対応のデバイスか確認し、62行目と63行目でタッチ対応端末と非対応の端末でイベントを分けています。mouseupに対応するのはtouchendだといろんなところで書いてあるので、マウスではmouseupとするところをタッチ対応端末ではtouchendとしています。

動作としては、『あ』と書かれたところをタッチ(もしくはマウスのボタンを押した時)に周りに『い』『う』『え』『お』と表示され、その部分に指を動かして手を離す(もしくはマウスのボタンを離す)と、その文字がテキストボックスに表示されるという動作にしようと思って作ってみました。

まずはマウスで試してみました。ちゃんと想定した動きになっています。

続いて、タッチで試してみました。CSSでhoverの指定をしたところがうまく動かないのはある程度予想していました。が、肝心のフリック入力までうまく動きません。『あ』のところをタッチすると『い』『う』『え』『お』が表示されるものの、そちらに指を動かして指を話しても、テキストボックスに入力されるのは『あ』。『い』のほうに指を動かして話しても『あ』。『お』のほうに指を動かして話しても『あ』。iPod touchのSafariでやっても、AndroidのChromeで試しても、PCのChromeで試してもそうなります(自分が使っているWindows7はタッチ対応です)。なんじゃこりゃ。

どうも、マウスイベントと違ったタッチイベントは、タッチした要素から外にでてもその要素上でタッチしたままと考えられているらしい(参考:androidとWebアプリケーション | 日記というか備忘録というか)。
W3Cのサイトにある勧告文もそんな感じのことが書いてあった(Touch Events)。touchmoveイベントの2つ目の段落にそれっぽいことが書かれているような気はする。ただ、英語は苦手なので本当にそう書いてあるかどうかはわからない。

ということで、簡単にはフリックキーボードは実装できなさそう。やるとしたら、touchendが発生した座標がどの位置にあたるかを見ればできるかもしれない。

ただ、フリックキーボード自体は思いつきで作ろうとしただけであり、必要にせまられて作ろうとしたわけではないので、実際に作るかどうかはわからないです。

2014年5月22日追記:
作ってみました:JavaScriptでフリックキーボードを作ってみた(再挑戦) | while(isプログラマ)