Revision 11 as of 2010-10-27 04:46:25

Clear message
Locked History Actions

Android/Dimension

Dimension

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ソースコードの調査結果である。

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のメンバにアクセスするには、以下のようにオブジェクトを初期化すること。
 * <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;

    public DisplayMetrics() {
    }
    
    public void setTo(DisplayMetrics o) {
        widthPixels = o.widthPixels;
        heightPixels = o.heightPixels;
        density = o.density;
        densityDpi = o.densityDpi;
        scaledDensity = o.scaledDensity;
        xdpi = o.xdpi;
        ydpi = o.ydpi;
    }
    
    public void setToDefaults() {
        widthPixels = 0;
        heightPixels = 0;
        density = DENSITY_DEVICE / (float) DENSITY_DEFAULT;
        densityDpi = DENSITY_DEVICE;
        scaledDensity = density;
        xdpi = DENSITY_DEVICE;
        ydpi = DENSITY_DEVICE;
    }

    /**
     * Update the display metrics based on the compatibility info and orientation
     * NOTE: DO NOT EXPOSE THIS API!  It is introducing a circular dependency
     * with the higher-level android.res package.
     * {@hide}
     */
    public void updateMetrics(CompatibilityInfo compatibilityInfo, int orientation,
            int screenLayout) {
        boolean expandable = compatibilityInfo.isConfiguredExpandable();
        boolean largeScreens = compatibilityInfo.isConfiguredLargeScreens();
        
        // Note: this assume that configuration is updated before calling
        // updateMetrics method.
        if (!expandable) {
            if ((screenLayout&Configuration.SCREENLAYOUT_COMPAT_NEEDED) == 0) {
                expandable = true;
                // the current screen size is compatible with non-resizing apps.
                compatibilityInfo.setExpandable(true);
            } else {
                compatibilityInfo.setExpandable(false);
            }
        }
        if (!largeScreens) {
            if ((screenLayout&Configuration.SCREENLAYOUT_SIZE_MASK)
                    != Configuration.SCREENLAYOUT_SIZE_LARGE) {
                largeScreens = true;
                // the current screen size is not large.
                compatibilityInfo.setLargeScreens(true);
            } else {
                compatibilityInfo.setLargeScreens(false);
            }
        }
        
        if (!expandable || !largeScreens) {
            // This is a larger screen device and the app is not 
            // compatible with large screens, so diddle it.
            
            // Figure out the compatibility width and height of the screen.
            int defaultWidth;
            int defaultHeight;
            switch (orientation) {
                case Configuration.ORIENTATION_LANDSCAPE: {
                    defaultWidth = (int)(CompatibilityInfo.DEFAULT_PORTRAIT_HEIGHT * density +
                            0.5f);
                    defaultHeight = (int)(CompatibilityInfo.DEFAULT_PORTRAIT_WIDTH * density +
                            0.5f);
                    break;
                }
                case Configuration.ORIENTATION_PORTRAIT:
                case Configuration.ORIENTATION_SQUARE:
                default: {
                    defaultWidth = (int)(CompatibilityInfo.DEFAULT_PORTRAIT_WIDTH * density +
                            0.5f);
                    defaultHeight = (int)(CompatibilityInfo.DEFAULT_PORTRAIT_HEIGHT * density +
                            0.5f);
                    break;
                }
                case Configuration.ORIENTATION_UNDEFINED: {
                    // don't change
                    return;
                }
            }
            
            if (defaultWidth < widthPixels) {
                // content/window's x offset in original pixels
                widthPixels = defaultWidth;
            }
            if (defaultHeight < heightPixels) {
                heightPixels = defaultHeight;
            }
        }
        
        if (compatibilityInfo.isScalingRequired()) {
            float invertedRatio = compatibilityInfo.applicationInvertedScale;
            density *= invertedRatio;
            densityDpi = (int)((density*DisplayMetrics.DENSITY_DEFAULT)+.5f);
            scaledDensity *= invertedRatio;
            xdpi *= invertedRatio;
            ydpi *= invertedRatio;
            widthPixels = (int) (widthPixels * invertedRatio + 0.5f);
            heightPixels = (int) (heightPixels * invertedRatio + 0.5f);
        }
    }

    @Override
    public String toString() {
        return "DisplayMetrics{density=" + density + ", width=" + widthPixels +
            ", height=" + heightPixels + ", scaledDensity=" + scaledDensity +
            ", xdpi=" + xdpi + ", ydpi=" + ydpi + "}";
    }

    private static int getDeviceDensity() {
        // qemu.sf.lcd_density can be used to override ro.sf.lcd_density
        // when running in the emulator, allowing for dynamic configurations.
        // The reason for this is that ro.sf.lcd_density is write-once and is
        // set by the init process when it parses build.prop before anything else.
        return SystemProperties.getInt("qemu.sf.lcd_density",
                SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT));
    }
}

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

    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デバイスのピクセル形状は正方形でなければならないことが明らかである。

ところが、dpi値としては縦横別々にxdpi, ydpiと設定できるようになっている。ピクセル形状が同一でなければならないとしたら、それらは常に同一の値のはずである。

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