世の中、ブラウザはオープンソースの時代です( ー`дー´)キリッ ので、タイーホとならない程度に、書きたいことを書いてみようと思います。
そもそもなぜブラウザをカスタマイズするのか?
へたなカスタマイズはフラグメンテーション(改造によって互換性のないめちゃくちゃなものに・・・)のもとです。なので、やるべきではありません。
それなのに、なぜカスタマイズをするのか。
そこにはユーザの小さな小さな不満があるからです。
・タップしてからページが表示されるまでの時間が遅い。早くしたい。
・タッチスクロールの指に吸い付く度合いがヘボい。もっと指に気持ちよくついてきてほしい。
・電池めっちゃくう。ずっとブラウザ使ってても電池くわないようにしてほしい。
ほんとうに地味なことですが、こういうちょっとしたことがユーザの感覚に響くのです。
なくても困らないのだけども、あったほうが俄然いい
これがブラウザカスタマイズのモットーです。
で、ごちゃごちゃ論じるのはこのブログの趣旨には合いませんね。
オープンソースのブラウザコードを少しだけ読み解いてみましょう。
カスタマイズの説明の前に。
「ブラウザって、どうせタッチスクロールしたぶんだけ画面を動かしてるんでしょ?」というイメージをお持ちの方に、
ちょっとだけ認識を改めてほしいので、ブラウザのタッチスクロールの処理の仕組みを簡単に説明します。
まぁたしかにタッチスクロールした分だけ動かしてることには間違いないんですけど、登場人物が2人いて、それをここで明確にしておきたいのです。
ちょっとだけ認識を改めてほしいので、ブラウザのタッチスクロールの処理の仕組みを簡単に説明します。
まぁたしかにタッチスクロールした分だけ動かしてることには間違いないんですけど、登場人物が2人いて、それをここで明確にしておきたいのです。
登場人物1:タッチをひたすら監視する小人さん
Androidには、タッチイベントをリアルタイムに監視して、
「いまスクロールされたぞ!」「いまタップされたぞ!」って
都度言ってきてくれる小人さんがいます。
登場人物2:WebKit
WebKitの一番の役割は、
HTMLというフォーマットで書かれた文字列から
要求に応じてビットマップ画像を作り出すことです。
AndroidのWebViewは
前述のタッチイベント監視人さんとWebKitをうまく組み合わせて、
ユーザがタッチスクロールしたら画面を書きなおして
あたかも画面がスクロールしたかのように見せています。
ブラウザの「タッチスクロール」をすこしだけ改善してみよう
用意するもの:
・Android4.4.2か4.4.4の端末(Nexus 5)
・ページ内検索が速いPCブラウザ(IEはダメ、Google Chrome推奨)
・Androidのビルド環境
現在のAndroidのブラウザの多くに使われている"WebView"という部品は、じつは厳密にはスクロールした分だけ画面が移動してくれていません。
実際にスクロールされるのは、タッチスクロール量から数ミリメートルだけ引かれたぶんが、スクロールされます。
え?うそでしょ?とおもったあなた。実際にAndroid端末で以下のように動作を見てみてください。
このブログをAndroidで見た時のスクリーンショットで説明してますが、YahooでもGoogleでもどんなページでも同じです。
どんなに頑張ってスクロールしても最終的に指の場所についてきてくれませんね?
今回はこれを、ちゃんと指についてきてくれるよう改善してみたいと思います。
・指に吸い付かない理由
正解から言ってしまうと、タッチイベントを監視している小人さんが"躊躇している"タイミングがあるからです。
専門用語で言うと、touch slopとよばれるもので、
「タップ」なのか「タッチスクロール」なのかを見分けるための、しきい値です。
ソースコードを見る前に、絵で説明します。
①は誰が見てもタップですね。これをスクロールだって言うと、手が震えがちな人はタップできませんね。
そして、③は誰が見てもスクロールですね。これをタップだって言われると、スクロールさせるにはどんだけ手を動かさんとダメなの?と思ってしまいますね。
じゃあ、②はどうでしょう?これは微妙ですね。
ただ、タッチ監視をしている小人さんは「微妙」という回答はできません。ユーザが画面に触れて何かアクションをした以上は、かならず「タップ」か「スクロール」かを決めてあげなければなりません。
そこでAndroidは1つバシッと指標を決めていて、
・8dp以上動いていない場合はタップ
・8dp以上動いている場合はタッチスクロール
としています。
ここでピンときたひとがいるかもしれません。
タッチスクロールしても8dp動いてない場合はタップと認識される。
タッチスクロールと認識され始めるのは、8dp目のスクロール部分から。
そう、まさにこの8dp捨てられているのがAndroidのWebViewの動作なのです。
さて、ソースを見てましょう。
http://tools.oesf.biz/android-4.4w_r1.0/xref/external/chromium_org/content/public/android/java/src/org/chromium/content/browser/third_party/GestureDetector.java
これがタッチを監視してる小人です。
・onDown()→onScroll(),onScroll(),onScroll(),onScroll(),・・・
・onDown()→onSingleTapUp()
mListenerへのコールバックのされ方が2通りあります。
(いうまでもなく、前者はmTouchSlopを超えたタッチスクロール操作で、後者はmTouchSlopを超えないタップ操作。)
Nexus 7 (2013)とかは8dpではなく、ちょっと大きめの12dpが指定されていますね。
# タッチパネルの精度が悪いからでしょうか…
ちなみに、ここまでのところ、「8dp捨てられている」という実装はありません。
タッチスクロール時、GestureDetector自体は
のように忠実に、判断結果と座標をmListenerへコールバックしています。
じゃあ、誰がonDown~初回onScrollのスクロール量を捨てているかというと、mListenerを使っている人です。
つまり、小人さんに監視を任せている、その司令塔が、捨てているのです。
http://tools.oesf.biz/android-4.4w_r1.0/xref/external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentViewGestureHandler.java
司令塔はこいつです。
そこには、このように書かれています。
初回にスクロールイベントを判定した時点で一気にばっこーーん!と8dpスクロールとんでスクロールされるのは不自然なので、滑らかに見せるために、8dp捨ててスクロール量を計算しているようです。
・指に吸い付かせる
まずは、馬鹿になって、AndroidのWebViewの心遣い(ばっこーーーん!とスクロールが飛んでしまうのを防ぐために)を無視してしまいましょう。
(一気にステップとばしすぎ!と思われても仕方がない書き方w ビルドの説明まで書いてるとキリがないので・・・)
で・・・
こんなのが製品として成り立つわけがありませんね。
実際に製品にする人々はどういう工夫をしているかというと・・・(以下略
F-06Fのオープンソースを覗いてみてください。
歯切れ悪いですが、以上です(笑)
もっといい方法があるよ!という意見がありましたら、どしどしコメントください。
実際にスクロールされるのは、タッチスクロール量から数ミリメートルだけ引かれたぶんが、スクロールされます。
え?うそでしょ?とおもったあなた。実際にAndroid端末で以下のように動作を見てみてください。
"指に吸い付かないスクロール"を体感しよう
このブログをAndroidで見た時のスクリーンショットで説明してますが、YahooでもGoogleでもどんなページでも同じです。
どんなに頑張ってスクロールしても最終的に指の場所についてきてくれませんね?
今回はこれを、ちゃんと指についてきてくれるよう改善してみたいと思います。
・指に吸い付かない理由
正解から言ってしまうと、タッチイベントを監視している小人さんが"躊躇している"タイミングがあるからです。
専門用語で言うと、touch slopとよばれるもので、
「タップ」なのか「タッチスクロール」なのかを見分けるための、しきい値です。
どこまでがタップでどこからがスクロールか
ソースコードを見る前に、絵で説明します。
①は誰が見てもタップですね。これをスクロールだって言うと、手が震えがちな人はタップできませんね。
そして、③は誰が見てもスクロールですね。これをタップだって言われると、スクロールさせるにはどんだけ手を動かさんとダメなの?と思ってしまいますね。
じゃあ、②はどうでしょう?これは微妙ですね。
ただ、タッチ監視をしている小人さんは「微妙」という回答はできません。ユーザが画面に触れて何かアクションをした以上は、かならず「タップ」か「スクロール」かを決めてあげなければなりません。
そこでAndroidは1つバシッと指標を決めていて、
・8dp以上動いていない場合はタップ
・8dp以上動いている場合はタッチスクロール
としています。
ここでピンときたひとがいるかもしれません。
タッチスクロールしても8dp動いてない場合はタップと認識される。
タッチスクロールと認識され始めるのは、8dp目のスクロール部分から。
そう、まさにこの8dp捨てられているのがAndroidのWebViewの動作なのです。
さて、ソースを見てましょう。
http://tools.oesf.biz/android-4.4w_r1.0/xref/external/chromium_org/content/public/android/java/src/org/chromium/content/browser/third_party/GestureDetector.java
これがタッチを監視してる小人です。
461 /** 462 * Analyzes the given motion event and if applicable triggers the 463 * appropriate callbacks on the {@link OnGestureListener} supplied. 464 * 465 * @param ev The current motion event. 466 * @return true if the {@link OnGestureListener} consumed the event, 467 * else false. 468 */ 469 public boolean onTouchEvent(MotionEvent ev) { 535 case MotionEvent.ACTION_DOWN: 553 mDownFocusX = mLastFocusX = focusX; 554 mDownFocusY = mLastFocusY = focusY; 558 mCurrentDownEvent = MotionEvent.obtain(ev); 571 handled |= mListener.onDown(ev); 572 break; 574 case MotionEvent.ACTION_MOVE: 578 final float scrollX = mLastFocusX - focusX; 579 final float scrollY = mLastFocusY - focusY; 583 } else if (mAlwaysInTapRegion) { 584 final int deltaX = (int) (focusX - mDownFocusX); 585 final int deltaY = (int) (focusY - mDownFocusY); 586 int distance = (deltaX * deltaX) + (deltaY * deltaY); 587 if (distance > mTouchSlopSquare) { 588 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); 589 mLastFocusX = focusX; 590 mLastFocusY = focusY; 591 mAlwaysInTapRegion = false; 595 } 599 } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) { 600 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); 601 mLastFocusX = focusX; 602 mLastFocusY = focusY; 603 } 604 break; 606 case MotionEvent.ACTION_UP: 615 } else if (mAlwaysInTapRegion) { 616 handled = mListener.onSingleTapUp(ev);スクロール量の平方が、mTouchSlopSquareという値を超えているかどうかによって、
・onDown()→onScroll(),onScroll(),onScroll(),onScroll(),・・・
・onDown()→onSingleTapUp()
mListenerへのコールバックのされ方が2通りあります。
(いうまでもなく、前者はmTouchSlopを超えたタッチスクロール操作で、後者はmTouchSlopを超えないタップ操作。)
413 final ViewConfiguration configuration = ViewConfiguration.get(context); 414 touchSlop = configuration.getScaledTouchSlop(); 425 mTouchSlopSquare = touchSlop * touchSlop;mTouchSlopSquareは↑で定義されていて、元をたどると http://tools.oesf.biz/android-4.4w_r1.0/search?q=config_viewConfigurationTouchSlop このへんから値を持ってきています。
Nexus 7 (2013)とかは8dpではなく、ちょっと大きめの12dpが指定されていますね。
# タッチパネルの精度が悪いからでしょうか…
ちなみに、ここまでのところ、「8dp捨てられている」という実装はありません。
タッチスクロール時、GestureDetector自体は
のように忠実に、判断結果と座標をmListenerへコールバックしています。
じゃあ、誰がonDown~初回onScrollのスクロール量を捨てているかというと、mListenerを使っている人です。
つまり、小人さんに監視を任せている、その司令塔が、捨てているのです。
http://tools.oesf.biz/android-4.4w_r1.0/xref/external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentViewGestureHandler.java
司令塔はこいつです。
30 class ContentViewGestureHandler implements LongPressDelegate { 31 69 private GestureDetector mGestureDetector;このとおり、タッチ監視の小人であるGestureDetectorを配下に持っています。
297 private void initGestureDetectors(final Context context) {のなかに小人による監視結果をさばくための実装があります。
そこには、このように書かれています。
304 @Override 305 public boolean onDown(MotionEvent e) { 308 mTouchScrolling = false; 309 mSeenFirstScrollEvent = false; 316 if (sendMotionEventAsGesture(GESTURE_TAP_DOWN, e, null)) { 318 } 319 // Return true to indicate that we want to handle touch 320 return true; 321 } 323 @Override 324 public boolean onScroll(MotionEvent e1, MotionEvent e2, 325 float distanceX, float distanceY) { 327 if (!mSeenFirstScrollEvent) { 328 // Remove the touch slop region from the first scroll event to avoid a 329 // jump. 330 mSeenFirstScrollEvent = true; 331 double distance = Math.sqrt( 332 distanceX * distanceX + distanceY * distanceY); 333 double epsilon = 1e-3; 334 if (distance > epsilon) { 335 double ratio = Math.max(0, distance - scaledTouchSlop) / distance ; 336 distanceX *= ratio; 337 distanceY *= ratio; 338 } 339 } 359 // distanceX and distanceY is the scrolling offset since last onScroll. 360 // Because we are passing integers to webkit, this could introduce 361 // rounding errors. The rounding errors will accumulate overtime. 362 // To solve this, we should be adding back the rounding errors each time 363 // when we calculate the new offset. 364 int x = (int) e2.getX(); 365 int y = (int) e2.getY(); 366 int dx = (int) (distanceX + mAccumulatedScrollErrorX); 367 int dy = (int) (distanceY + mAccumulatedScrollErrorY); 368 mAccumulatedScrollErrorX = distanceX + mAccumulatedScrollErrorX - dx; 369 mAccumulatedScrollErrorY = distanceY + mAccumulatedScrollErrorY - dy; 370 371 mExtraParamBundleScroll.putInt(DISTANCE_X, dx); 372 mExtraParamBundleScroll.putInt(DISTANCE_Y, dy); 373 assert mExtraParamBundleScroll.size() == 2; 374 375 if ((dx | dy) != 0) { 376 sendGesture(GESTURE_SCROLL_BY, 377 e2.getEventTime(), x, y, mExtraParamBundleScroll); 378 }上のコードででっかく太字にしたところが、「8dp捨てられている」実際のコードです。
初回にスクロールイベントを判定した時点で一気にばっこーーん!と8dpスクロールとんでスクロールされるのは不自然なので、滑らかに見せるために、8dp捨ててスクロール量を計算しているようです。
・指に吸い付かせる
まずは、馬鹿になって、AndroidのWebViewの心遣い(ばっこーーーん!とスクロールが飛んでしまうのを防ぐために)を無視してしまいましょう。
335 double ratio = Math.max(0, distance - 0) / distance;これで
mmm external/chromium_org/android_webview && mmm frameworks/webview/chromium/ cd out/target/product/hammerhead/ adb root adb remount adb shell stop adb sync system adb reboot端末を焼き帰ると、ぱっこーーーーん!と飛びながらも追従するブラウザが出来上がりましたね?
(一気にステップとばしすぎ!と思われても仕方がない書き方w ビルドの説明まで書いてるとキリがないので・・・)
で・・・
こんなのが製品として成り立つわけがありませんね。
実際に製品にする人々はどういう工夫をしているかというと・・・(以下略
F-06Fのオープンソースを覗いてみてください。
歯切れ悪いですが、以上です(笑)
もっといい方法があるよ!という意見がありましたら、どしどしコメントください。