Revision 9 as of 2010-10-27 04:14:24

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;

    /**
     * The logical density of the display.  This is a scaling factor for the
     * Density Independent Pixel unit, where one DIP is one pixel on an
     * approximately 160 dpi screen (for example a 240x320, 1.5"x2" screen), 
     * providing the baseline of the system's display. Thus on a 160dpi screen 
     * this density value will be 1; on a 120 dpi screen it would be .75; etc.
     *  
     * <p>This value does not exactly follow the real screen size (as given by 
     * {@link #xdpi} and {@link #ydpi}, but rather is used to scale the size of
     * the overall UI in steps based on gross changes in the display dpi.  For 
     * example, a 240x320 screen will have a density of 1 even if its width is 
     * 1.8", 1.3", etc. However, if the screen resolution is increased to 
     * 320x480 but the screen size remained 1.5"x2" then the density would be 
     * increased (probably to 1.5).
     *
     * @see #DENSITY_DEFAULT
     */
    public float density;
    /**
     * The screen density expressed as dots-per-inch.  May be either
     * {@link #DENSITY_LOW}, {@link #DENSITY_MEDIUM}, or {@link #DENSITY_HIGH}.
     */
    public int densityDpi;
    /**
     * A scaling factor for fonts displayed on the display.  This is the same
     * as {@link #density}, except that it may be adjusted in smaller
     * increments at runtime based on a user preference for the font size.
     */
    public float scaledDensity;
    /**
     * The exact physical pixels per inch of the screen in the X dimension.
     */
    public float xdpi;
    /**
     * The exact physical pixels per inch of the screen in the Y dimension.
     */
    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;
    }