Locked History Actions

Android/Dimension

Dimension

まとめ

※以下は実証のない単なる想像も含まれる

コンピュータのモニタに適切なサイズで描画を行うのは困難が伴なう。 何が適切なサイズであるのか不明確である上、そもそもアプリ側に通知されている解像度や画面サイズがそもそも正しいものである保証もない。

例えば、アプリ側には160dpiの解像度と通知されているから、160ピクセルの線を描画すれば1インチの線になるかといえばそうもいかない。 そもそも160piという値は、デバイスとは無関係にOS側で固定で通知されているのもかもしれない。

あるいはプロジェクタを使用している場合に、高解像度だからと言って、通常のモニタと同様に1インチの線を描画すればよいかと言えばそうではない。 この場合には、実際の長さより拡大して描画しなければ用をなすことができない。

このように、OS側から通知されている解像度が正しいとは限らないうえ、状況によっては「正しい」長さで描画しても無意味になる。

「適切な文字の大きさ」も状況によって変わることがある。同じデバイス、同じアプリを使用していてもある人にとっては適切だが、またある人にとっては不適切な場合もある。

。。。ということをAndroidの設計者は考えたと思われるが、それにしても説明が少なすぎる。このあとの(中途半端な)検証を元に、Androidの単位系を説明してみる。

まず、Androidにおける描画は基本的にピクセルベースで行うことになっている。つまり、何かを描画するときは、そのピクセルサイズを指定するのである。 ピクセルと言ってもデバイスの実ピクセルではない。これらはデバイスの解像度によってサイズが異なってしまう。

そうではなく、仮想的なピクセルである。実ピクセルの単位はpxであるが、仮想的なピクセルの単位をdpあるいはdipという。 そして、1dpあたりの実ピクセル数を密度(density)という。

例えば、densityが2の場合は、1つの仮想ピクセルの長さは2つの実ピクセルと等しくなる。 つまり、実際のデバイスの解像度はまちまちではあるものの、アプリ側としてはそれらを気にせずにdpという単位のピクセルベースで描画を行えばよいことになる。 そして、このdensityの値は端末製造者が適当なものを設定する模様である。

これは非常にうまい考え方ではある。従来的なアプリ作成では、実ピクセルかあるいは「ポイント」が使用されてきたが、前者は解像度等によってアプリ側が描画サイズを調整する必要のある場合があり、後者ではそもそも何を1ポイントとするのか混乱が見られる。

dpを使えば、決め打ちで5dpなどとしてしまっても、デバイスに関わらずそこそこ同様の表示になるというわけだ。 こんなことができるのはアンチエイリアシングがほとんどコスト無しに可能になったおかげもあると思われる。 アンチエイリアシング無しでこのようなことを行なおうとすれば、表示はギザギザ(ジャギー)だらけになってしまうだろう。 単純な考え方だが、技術の発展無しには実現できなかったのである。

ただし、これにはもちろんデメリットもあり、densityが1未満の場合には、1dpの幅の線を描画しようとしても何も描画されないということもありうる。

さて、通常の描画にはdpを使用すればよいのだが、先に述べたように文字サイズを変更された場合に文字のあいだの距離を指定するにはどうするか。 「文字サイズを変更された」とはいっても、アプリ側で文字の大きさを指定する場合ではなく、「システム全体の文字の大きさを変更する」という場合である。 アプリ側は基本的にはそれに従わなければならない。というよりも、自動的にデフォルトの文字サイズがその指定サイズになるようだ。

とは言っても、現在のAndroid(2.2)にはこの機能はなぜか実現されていないので基本的には検証できない(マーケットからダウンロードできる有料アプリによって可能になるという話ではある)。

さて、将来的にこのような機能が追加され、デフォルトの文字サイズを大きくされた場合には、例えばそれらの文字間を相変わらず3dpとしていては都合が悪い。 ということで、dpに文字大きさの補正を加えた単位をspとして定義したようだ。文字まわりの距離にはspを使えということである。

ただし、dpにしてもspにしてもどの程度の具体的な数値が文字間距離として適切なのか、あるいはアイコンサイズとして適切なのかなどはどこにも示されていない模様である。

そのほか、pxという実ピクセル単位も使用できるし、in,mm,pt等の「実際の距離」を表す単位も使用できるのだが、これらはかなり特殊な用途といえる。

つまり、デバイスのdpiが本当に正しいという前提のもとで、in, mm, pt等を使えば常に同じ大きさになるのだが、先にも示したように同じ大きさで表示できるということが、 常に適切なこととは言えないのである。

調査開始

※以下は調査の経過を書き連ねたものである。

Androidの長さの単位としては以下の種類が提供されている。

例によって、これで何のことかわかる人はいないと思われる。

これらの記述からわかることは、dp, mm, in, ptはスケールが異なるだけで本質的には同じものであるということ。どのようなデバイスであっても、「1なんちゃら」は物理的に同じサイズになる。。。はずなのだが、しかし、この記述をそのまま受け入れるわけにはいかない。

これらはPC用のプログラミングをさんざんやってきた者ならわかることなのだが、マシンや実行環境からスクリーンが「160dpi」であることを取得できたとしても、実際には違っている場合が多々あるからである。そりゃそうだろう、接続されているモニタがきちんと認識できていれば可能かもしれないが、モニタの種類がわからない場合も多々あるのである。15インチかもしれないし、プロジェクタかもしれない。

Androidの場合は外部モニタは想定されていないと思われるが、それにしてもメーカー側がきちんとこの値を用意してくれなければ、これらのサイズ表記はやはり絵に描いた餅に過ぎない。したがって、この単位系の前提条件として「正しく無い解像度申告を行う端末は認めない」ということでなければならない。もちろん、このような制限をかけるのはAndroidの場合には不可能であるから(つまり、勝手な端末を製造することを禁止することはできない)、結局のところこれらは全く信用ならないということになる。

dpについて

また、dpについては「The ratio of dp-to-pixel will change with the screen density, but not necessarily in direct proportion」としているが、これは完全に意味不明。どのように変化するのか、あるいはその理由は何なのか一切説明がない。疑問を持つ人がいて当然である。

http://groups.google.co.jp/group/android-developers/browse_thread/thread/978813b2998ef439

回答者はどうも、アスペクト比が1でない場合を想定しているらしい。 つまり、ピクセルの形状が正方形ではなく長方形の場合である。 だから、必ずしもdirect proportionではないと言いたいらしいのだが、しかしそうであるならdpはin,mm,pt等と全く同じ概念であり、何ら異なるところはない。 ならば、dpが特別扱いされている理由は何なのだろうか?

spについて

spは使えそうであるが、しかしこれについても具体的な算出式が示されていないため、「1」がどのような大きさになるのか一切わからない。

これについても上記のQ&Aで「It's a setting that isn't currently exposed in preferences, allowing you to make fonts larger or smaller. If you build Spare Parts from the 1.0 source tree you can install that and use it to change the font size preference. 」 と答えられているのだが、こんなものは全く答えになっていない。

答えたくない理由があるのではないかと勘ぐりたくもなるものだ。

これに関してまたQ&Aを発見した。

http://stackoverflow.com/questions/2000892/whats-the-relationship-between-pixels-and-scaled-pixels

ベストプラクティス

以下の文書があるが、あまり参考にはならない。

ソースコードを調べる

あいまいな記述に終始するドキュメントやQ&Aより、ソースコードを見た方がおそらく話が早いだろう。 以下は2010/10時点のAndroidソースコードの調査結果である。

TextView.setTextSize

android.widget.TextViewで文字サイズを指定する場合、TextView.setTextSize(int size, float size)が使用できるが、 これは以下のとおり、

    public void setTextSize(int unit, float size) {
        Context c = getContext();
        Resources r;

        if (c == null)
            r = Resources.getSystem();
        else
            r = c.getResources();

        setRawTextSize(TypedValue.applyDimension(
            unit, size, r.getDisplayMetrics()));
    }

setRawTextSize(float size)の引数が、何らかの統一的単位(おそらくピクセル)に変換された結果であろう。 r.getMetrics()はandroid.content.res.Resourcesクラスに定義されており、単にmMetricsフィールドを返すだけである。 これはandroid.util.DisplayMetricsというクラスである。このソースを省略しつつ、コメントを翻訳してみる。

DisplayMetricsクラス

/**
 * ディスプレイに関する一般的な情報を保持する構造体である。
 * 例えば、サイズ、密度、フォントスケーリングである。
 * DisplayMetricsのメンバにアクセスするには、以下のようにオブジェクトを初期化すること。
 * <pre> DisplayMetrics metrics = new DisplayMetrics();
 * getWindowManager().getDefaultDisplay().getMetrics(metrics);</pre>
 */
public class DisplayMetrics {
    /** 低密度スクリーンの標準的なquantize(?)されたDPI */
    public static final int DENSITY_LOW = 120;

    /** 中密度スクリーンの標準的なquantize(?)されたDPI */
    public static final int DENSITY_MEDIUM = 160;

    /** 高密度スクリーンの標準的なquantize(?)されたDPI */
    public static final int DENSITY_HIGH = 240;

    /** システム全体を通して使用される「リファレンスな」密度 */
    public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;

    /**
     * このデバイスの密度
     * @hide 最終的には、この値は実行中に変更できるようにすべきであるため、
     * これは定数にはすべきではない。 
     *
     * 訳注:現在のコードでは、このクラスがロードされた時に値が決定され、
     * これ移行変更されることは絶対に無い。
     */
    public static final int DENSITY_DEVICE = getDeviceDensity();

    /** ディスプレイの、ピクセルで表した絶対的な幅
     *
     * 訳注:どっち方向の?portraitそれともlandscape?
     */
    public int widthPixels;

    /** ディスプレイの、ピクセルで表した絶対的な高さ */
    public int heightPixels;

    /**
     * ディスプレイの論理的な密度。これは密度非依存のピクセル単位のスケーリングファクタになる。
     *
     * つまり、だいたい(approximately)160dpiのスクリーン(例えば240x320ピクセル, 1.5x2インチ)
     * で1dipが1ピクセルになり、これがシステムのディスプレイのベースラインとなる。
     *
     * このように160dpiのスクリーンでは密度値は1であり、120dpiスクリーンでは0.75になるという
     * あんばいだ。
     *
     * この値は必ずしも実際のスクリーンサイズに従っているわけではなく(as given by 
     * {@link #xdpi} and {@link #ydpi})、ディスプレイdpiにおける全体的な変化に基づき
     * UI全体サイズのスケールを行うのに使用される(訳注:よくわからんが、一つのデバイスを
     * 使用中に解像度が変わることを想定している?)。
     *
     * 例えば、240x320(訳注:ピクセル?)のサイズのスクリーンは、たとえその幅が1.8インチ、1.3インチ
     * あるいは他の値であっても、密度は1である。
     *
     * その一方で、スクリーン解像度が320x480に増加したにも関わらず、スクリーンのサイズが1.5x2の
     * ままであれば、密度は増加する(おそらく1.5になる)。  
    *
     *
     * @see #DENSITY_DEFAULT
     */
    public float density;

    /**
     * 1インチあたりのドット数(訳注:ピクセル数?)で表されたスクリーン密度。
     * おそらく、
     * {@link #DENSITY_LOW}, {@link #DENSITY_MEDIUM}, or {@link #DENSITY_HIGH}
     * のいずれかになる。
     */
    public int densityDpi;

    /**
     * ディスプレイ上に表示されるフォントのスケーリングファクタ。
     * これは{@link #density}に同一であるが、ただし、実行時のフォントサイズについてのユーザプリファレンス
     * を元として若干の増加補正が行われる。
     */
    public float scaledDensity;

    /** スクリーンのX方向(ってどっちだよ)の正確(ほんとかね?)な1インチあたりのピクセル数 */
    public float xdpi;

    /** スクリーンのY方向の正確な1インチあたりのピクセル数 */
    public float ydpi;

    // 以下略

densityの意味

上記のdensityフィールドのコメントはとてもわかりづらい。 要するに、densityとは「仮想的なピクセル一つが物理ピクセルを占有する大きさ」と思えばよいようだ。

基本的には「ピクセル」なのでdpiとは無関係である。 例えば一つの240x320という物理ピクセル画面があるとする。

これが様々な大きさで表示された場合にはそのdpiは変化してしまうが、 densityは変わらない。density=2の場合には、1dpの長さ(仮想ピクセル)は常に2つの物理ピクセルの長さになる。

ところが、この仮想的なピクセル密度はどのように決めたらよいのだろうか? 「仮想的なピクセル」であるから、何らかの標準的なマシンのピクセルと同一にしておいた方が扱いやすいだろう。

ということで、標準的な160dpiのマシンにおいて仮想ピクセル1つが物理ピクセル1つに対応するよう、density=1と決めたというわけ。 例えば、この同じ大きさのスクリーンを持つマシンが解像度だけ縦横二倍に増えた場合は、一般的にはdensity=2とするはず。

しかしこれは、おそらく端末製造者に任されていると思われる。 スクリーンの大きさが同じで解像度が増え、さらに「常に虫眼鏡で見る」という機能を追加場合には、density=1となるかもしれない。 以前のマシンに比較して作業領域は縦横二倍になるからである。

つまり、densityとは仮想的なピクセルサイズであり、その値がどのように定義されているかは物理的な画面サイズや解像度からは一概には言えない。 これらは、おそらく端末製造者が適当な値を決定しているであろうからである。

TypedValue.applyDimension

次にTypedValue.applyDimensionを調べてみる。

    /**
     * Converts an unpacked complex data value holding a dimension to its final floating 
     * point value. The two parameters <var>unit</var> and <var>value</var>
     * are as in {@link #TYPE_DIMENSION}.
     *  
     * @param unit The unit to convert from.
     * @param value The value to apply the unit to.
     * @param metrics Current display metrics to use in the conversion -- 
     *                supplies display density and scaling information.
     * 
     * @return The complex floating point value multiplied by the appropriate 
     * metrics depending on its unit. 
     */
    public static float applyDimension(int unit, float value,
                                       DisplayMetrics metrics)
    {
        switch (unit) {
        case COMPLEX_UNIT_PX:
            return value;
        case COMPLEX_UNIT_DIP:
            return value * metrics.density;
        case COMPLEX_UNIT_SP:
            return value * metrics.scaledDensity;
        case COMPLEX_UNIT_PT:
            return value * metrics.xdpi * (1.0f/72);
        case COMPLEX_UNIT_IN:
            return value * metrics.xdpi;
        case COMPLEX_UNIT_MM:
            return value * metrics.xdpi * (1.0f/25.4f);
        }
        return 0;
    }

ここからわかることは以下の通り、

  • 単位がpxの場合には変換されていないことから、戻り値はピクセルである。
  • dipの場合はmetrics.densityでスケーリングされている。
  • spの場合はmetrics.scaledDensityでスケーリングされている。
  • pt, in, mmはx方向(ってどちらの場合?portrait?landscape?)のdpi値でしかスケーリングされていない。

このメソッドは縦横方向ともに(もちろん斜め方向もだろう)同じものが使用されると思われるが、ひどいことに一つの値でしかスケーリングされていない。 つまり、Androidデバイスのピクセル形状は正方形でなければならないことが明らかである。そうでない場合には、dipを使おうが、mmを使おうが、1という線は縦横方向で異なる長さになる。

ところがこれに反して、dpi値としては縦横別々にxdpi, ydpiと設定できるようになっている。ピクセル形状が正方形でなければならないという前提であるとしたら、それらは常に同一の値のはずであり、縦横別々の値を保持する必要はない。

このような一貫性の無いちぐはぐな設計であるため、pt, in, mmのスケーリングでは、なぜかxdpiのみを使用してydpiは無視されており、またその理由も一切の記述が無い。

Resources

density, scaledDensityの値はどのように決定されるのだろうか?DisplayMetricsクラスの中では(例外はあるものの基本的には)この値は操作されていない。

先のTextView.setTextSize内では基本的にandroid.content.res.ResourcesクラスのgetDisplayMetrics()を使ってDisplayMetricsを取得している。 ここを追ってみる。

。。。ここを見ても無駄であった。densityの値はそもそもandroid.view.Display#mDensityというフィールドがそもそもの「起源」であり、この値は 「Following fields are initialized from native code」との記述がある。

mDensityと同様にネイティブコードで値が格納されるであろうフィールドには「mDisplay mPixelFormat mRefreshRate mDensity mDpiX mDpiY」がある。 これらはおそらく端末製造者が設定するものと思われる。

Platform Developer

端末製造者(?)向け文書に以下があるが、

http://source.android.com/compatibility/android-2.1-cdd.pdf

これには次のようにある。

8.1.2. Non-Standard Display Configurations
Display configurations that do not match one of the standard configurations listed in
Section 8.1.1 require additional consideration and work to be compatible. Device
implementers MUST contact Android Compatibility Team as provided for in Section 12
to obtain classifications for screen-size bucket, density, and scaling factor. When
provided with this information, device implementations MUST implement them as
specified.
Note that some display configurations (such as very large or very small screens, and
some aspect ratios) are fundamentally incompatible with Android 2.1; therefore
device implementers are encouraged to contact Android Compatibility Team as early
as possible in the development process.

現在のところの結論としては、端末のピクセルが正方形であり、かつ先のパラメータが製造者によって正しく設定されているのであれば、 dip, mm, in, ptのいずれを使おうが大差はない。

逆に、ピクセルが正方形でなかったり、製造者による設定値が正しくなければ、dipを使おうが何を使おうが期待する結果にはならない。