2009-09-23

近況

会社に鍵と財布を忘れたその夜, 行き場を失った私は自宅の扉に拳を打ち付けた. 人生のロードマップを前倒す新宿中央公園デビューも頭をよぎったものの, 幸いその日のカバンにはケータイが入っていた. 近所の友達が在宅だったのもまた幸いだった. 懇願して寝床を, 脅迫して晩飯を得ることができた.

腹もふくれ我が家のようにくつろいでいると, Android devphone 1 の箱が目についた. そういえば前に買ったと言っていたっけ...などといいつつ勝手にいじっている私に気付いた友達が, 思い出したようにつぶやく - "そういえば, Android のブラウザはちょっと不思議でさあ..." というのは "ページの文章がいつも画面ぴったりに収まるんだよね." そういわれてみると, たしかに文章の幅がぴったりきている. 最初はたまたまかと思ったけれど, 設定画面をみると "Auto-fit pages: Format Web pages to fit the screen" なんて項目があった. API にも WebSetting.LayoutAlgorithm なる定数がある. やっぱりブラウザの機能らしい. なかなか便利. iPhone (iPod touch) の Safari にこんな機能はなかったから, Android 固有の機能なのかもしれない.

Fit Column To Screen

帰宅後に Android のツリー をチェックアウトしてみると, たしかに 改造 WebKit が入っていた. オリジナル ChangeLog ファイルの日付は今年の 2 月. 以降もツリーはそこそこ変更されており, セキュリティの修正なんかも手でぱちっているかんじ. 割と独自路線な様子.

件のレイアウト機能は FCTS(Fit Column To Screen) と呼ばれ, ANDROID_LAYOUT の ifdef で盛り込まれていた. この ifdef は何箇所かにあるけれど, bidi.cpp の使われ方がわかりやすい.

void RenderBlock::layoutInlineChildren(bool relayoutChildren, int& repaintTop, int& repaintBottom)
{
    ...
    if (firstChild()) {
#ifdef ANDROID_LAYOUT
        // if we are in fitColumnToScreen mode and viewport width is not device-width,
        // and the current object is not float:right in LTR or not float:left in RTL,
        // and text align is auto, or justify or left in LTR, or right in RTL, we
        // will wrap text around screen width so that it doesn't need to scroll
        // horizontally when reading a paragraph.
        const Settings* settings = document()->settings();
        bool doTextWrap = settings && settings->viewportWidth() != 0 &&
                settings->layoutAlgorithm() == Settings::kLayoutFitColumnToScreen;
        if (doTextWrap) {
            int ta = style()->textAlign();
            int dir = style()->direction();
            EFloat cssfloat = style()->floating();
            doTextWrap = ((dir == LTR && cssfloat != FRIGHT) ||
                    (dir == RTL && cssfloat != FLEFT)) &&
                    ((ta == TAAUTO) || (ta == JUSTIFY) ||
                    ((ta == LEFT || ta == WEBKIT_LEFT) && (dir == LTR)) ||
                    ((ta == RIGHT || ta == WEBKIT_RIGHT) && (dir == RTL)));
        }
        bool hasTextToWrap = false;
#endif
    }
    ...
#ifdef ANDROID_LAYOUT
                if (doTextWrap && !hasTextToWrap && o->isText()) {
                    Node* node = o->element();
                    // as it is very common for sites to use a serial of <a> or
                    // <li> as tabs, we don't force text to wrap if all the text
                    // are short and within an <a> or <li> tag, and only separated
                    // by short word like "|" or ";".
                    if (node && node->isTextNode() &&
                            !static_cast<Text*>(node)->containsOnlyWhitespace()) {
                        int length = static_cast<Text*>(node)->length();
                        // FIXME, need a magic number to decide it is too long to
                        // be a tab. Pick 25 for now as it covers around 160px
                        // (half of 320px) with the default font.
                        if (length > 25 || (length > 3 &&
                                (!node->parent()->hasTagName(HTMLNames::aTag) &&
                                !node->parent()->hasTagName(HTMLNames::liTag))))
                            hasTextToWrap = true;
                    }
                }
#endif
            }
    ...
#ifdef ANDROID_LAYOUT
        // try to make sure that inline text will not span wider than the
        // screen size unless the container has a fixed height,
        if (doTextWrap && hasTextToWrap) {
            // check all the nested containing blocks, unless it is table or
            // table-cell, to make sure there is no fixed height as it implies
            // fixed layout. If we constrain the text to fit screen, we may
            // cause text overlap with the block after.
            bool isConstrained = false;
            RenderObject* obj = this;
            while (obj) {
                if (obj->style()->height().isFixed() && (!obj->isTable() && !obj->isTableCell())) {
                    isConstrained = true;
                    break;
                }
                if (obj->isFloating() || obj->isPositioned()) {
                    // floating and absolute or fixed positioning are done out
                    // of normal flow. Don't need to worry about height any more.
                    break;
                }
                obj = obj->container();
            }
            if (!isConstrained) {
                int screenWidth = view()->frameView()->screenWidth(); // 画面サイズ
                if (screenWidth > 0 && width() > screenWidth) {
                    int maxWidth = screenWidth - 2 * ANDROID_FCTS_MARGIN_PADDING;
                    setWidth(min(width(), maxWidth));
                    m_minPrefWidth = min(m_minPrefWidth, maxWidth);
                    m_maxPrefWidth = min(m_maxPrefWidth, maxWidth);
                    m_overflowWidth = min(m_overflowWidth, maxWidth);
                }
            }
        }
#endif
     ...
}

要するに, 設定が有効で DOM のテキストノードの文字列長が一定を越えていたら inline 要素の折り返し幅を実際の画面幅でクリップしている. こんなことをするとレイアウトが激しく崩れそうだけれど, この時点で親の block 要素の幅は既に計算が済んでいる. inline 要素の幅だけが影響をうける.

Android のブラウザでは(Android に限らず最近のスマートフォンでは), 画面全体のレイアウトにある種の仮想スクリーンを使っているのを思いだしてほしい. 画面全体は十分な幅を持った仮想スクリーン上で PC 風にレイアウトさせ, inline 要素に詰まったテキストだけを 実際の画面幅で折り返している. おかげでページ全体のレイアウトはくずれないのに, テキストは横スクロールなしで読めるわけ. なかなかよくできている.

昔, ケータイ版の Opera には SSR(Small Screen Rendering) という機能があって(まだあるかも), ページ全体のレイアウトをを小さな画面にあわせ組替えてくれた. これはこれで便利だったけれど, 複数カラム構成のページが増えるにつれて段々と組替えるのが辛くなっていった. Android の FCTS はページ全体のレイアウトを維持したまま インライン要素の折り返しだけを制御することで, 今時の複数カラムなページも破綻なく扱えている. あたまいい. どうせなら Webkit 本家にもフィードバックして, Blackberry や Palm に塩を送ればいいのにね.

なお Android WebKit は FCTS 以外に SSR 風のレイアウトモードも実装しており, これは複数列のテーブルを一列に畳みこんだり, 画像を画面幅に縮小したりできるらしい. でも設定の GUI から選べないとろをみると, あまり良い出来ではないのかも.

というわけで, iPhone ユーザにコンパス付き Street View を自慢できなくなり悲しんでいる Android ユーザ諸兄は FCTS を自慢するとよいのではないでしょうか.