各種のピクセル密度のサポート

Android デバイスは、画面サイズ(スマートフォン、タブレット、TV など)だけでなく、画面のピクセルサイズもさまざまです。1 平方インチあたり 160 ピクセルのデバイスもあれば、480 ピクセルのデバイスもあります。ピクセル密度の違いを考慮しないと、画像が拡大してぼやけて表示されたり、まったく違うサイズで表示されたりすることがあります。

ここでは、解像度に依存しない測定単位を使用してピクセル密度ごとに代替ビットマップを提供することで、さまざまなピクセル密度をサポートするようにアプリを設計する方法を説明します。

これらの方法の概要については、次の動画をご覧ください。

実際のアイコン アセットのデザインについて詳しくは、マテリアル デザインのアイコンのガイドラインをご覧ください。

密度非依存ピクセルを使用する

まず避けなければならないのは、ピクセルを使用して距離やサイズを定義することです。寸法をピクセルで定義すると問題が発生します。画素密度は画面によって異なるため、デバイスが異なると同じピクセル数でも異なる物理サイズになる場合があるからです。

図 1. ピクセル数が異なる可能性のある同じサイズの 2 つの画面

密度が異なる画面で UI の表示サイズを維持するには、測定単位として密度非依存ピクセル(dp)を使用して UI をデザインする必要があります。1 dp は、中密度画面(160 dpi、「ベースライン」密度)の 1 ピクセルとほぼ等しい仮想ピクセル単位です。Android ではこの値が他の密度の実際のピクセル数に変換されます。

たとえば、図 1 つの 2 つのデバイスで考えてみましょう。ビューの幅を「100 px」として定義すると、左側のデバイスにはもっと大きく表示されます。このため、両方の画面で同じサイズになるように「100 dp」を使用する必要があります。

ただし、テキストサイズを定義するときは、代わりにスケーラブル ピクセル(sp)を単位として使用する必要があります(ただし、レイアウトサイズには sp を使用しないでください)。sp 単位のサイズはデフォルトでは dp と同じですが、ユーザーの使用するテキストサイズに基づいて変更されます。

たとえば 2 つのビューの間の間隔を指定するときは、dp を使用します。

    <Button android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/clickme"
        android:layout_marginTop="20dp" />
    

テキストサイズを指定するときは、常に sp を使用します。

    <TextView android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="20sp" />
    

dp 単位をピクセル単位に変換する

場合によっては、寸法を dp で表してからピクセルに変換する必要があります。dp 単位から画面ピクセルへの変換は簡単です。

px = dp * (dpi / 160)

ユーザーが指先を少なくとも 16 ピクセル分動かすと、スクロール操作またはフリング操作が認識されるアプリがあるとします。基準の画面でユーザーの操作が認識されるようにするには、16 pixels / 160 dpi、つまり 1 インチの 1/10(2.5 mm) だけ指先を動かす必要があります。また、高密度画面(240dpi)のデバイスでは 16 pixels / 240 dpi、つまり 1 インチの 1/15(1.7 mm)だけ動かす必要があります。高密度画面の場合、指先を動かす距離がより短くなるため、ユーザーの操作に対してアプリがより敏感になるように見えます。

この問題を解決するには、コードで操作のしきい値を dp で表してから実際のピクセルに変換します。たとえば、次のようになります。

Kotlin

    // The gesture threshold expressed in dp
    private const val GESTURE_THRESHOLD_DP = 16.0f
    ...
    private var mGestureThreshold: Int = 0
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Get the screen's density scale
        val scale: Float = resources.displayMetrics.density
        // Convert the dps to pixels, based on density scale
        mGestureThreshold = (GESTURE_THRESHOLD_DP * scale + 0.5f).toInt()

        // Use mGestureThreshold as a distance in pixels...
    }
    

Java

    // The gesture threshold expressed in dp
    private static final float GESTURE_THRESHOLD_DP = 16.0f;

    // Get the screen's density scale
    final float scale = getResources().getDisplayMetrics().density;
    // Convert the dps to pixels, based on density scale
    mGestureThreshold = (int) (GESTURE_THRESHOLD_DP * scale + 0.5f);

    // Use mGestureThreshold as a distance in pixels...
    

DisplayMetrics.density フィールドには、現在のピクセル密度に応じて dp 単位をピクセルに変換するときに使用する必要のあるスケール係数を指定します。DisplayMetrics.density は中密度画面では 1.0、高密度画面では 1.5、超高密度画面では 2.0、低密度画面では 0.75 となります。この数値は、現在の画面での実際のピクセル数を取得するために dp 単位に掛ける必要のある係数です。

事前スケーリングされた設定値を使用する

ViewConfiguration クラスを使用すると、Android システムで使用される一般的な距離、速度、時間にアクセスできます。たとえば、フレームワークでスクロールのしきい値として使用されるピクセル単位の距離は、getScaledTouchSlop() を使って取得できます。

Kotlin

    private val GESTURE_THRESHOLD_DP = ViewConfiguration.get(myContext).scaledTouchSlop
    

Java

    private static final int GESTURE_THRESHOLD_DP = ViewConfiguration.get(myContext).getScaledTouchSlop();
    

getScaled 接頭辞で始まる ViewConfiguration のメソッドは、現在のピクセル密度に関係なく適切に表示されるピクセル単位の値を確実に返します。

別のビットマップを提供する

ピクセル密度が異なるデバイスで優れたグラフィック品質を実現するには、対応する解像度で、アプリのビットマップごとに複数のバージョン(密度バケットごとに 1 つ)を用意する必要があります。そうしないと、各画面で表示スペースが同じになるようにビットマップが拡大されて不鮮明になるなどスケーリングの乱れが発生します。

図 2. 異なる密度サイズでのビットマップの相対サイズ

アプリで使用できる密度バケットはいくつかあります。表 1 では、使用できるさまざまな設定修飾子とそれらが適用される画面タイプについて説明します。

表 1. さまざまなピクセル密度の設定修飾子。

密度修飾子 説明
ldpi 低密度(ldpi)画面(~120dpi)に適用するリソース
mdpi 中密度(mdpi)の画面(~160dpi)に適用するリソース(基準密度)。
hdpi 高密度(hdpi)画面(~240dpi)に適用するリソース
xhdpi 超高密度(xhdpi)画面(~320dpi)に適用するリソース
xxhdpi 超超高密度(xxhdpi)画面(~480dpi)に適用するリソース
xxxhdpi 超超超高密度(xxxhdpi)の画面(~640dpi)用のリソース
nodpi すべての密度に適用するリソース(密度非依存リソース)。そのときの画面の密度に関係なく、この修飾子の付いたリソースはスケーリングされません。
tvdpi mdpi と hdpi の間(およそ 213dpi)に該当する画面に適用するリソース。これは「メイン」の密度グループとしては認識されません。主にテレビが念頭に置かれており、ほとんどのアプリでは必要ありません。通常のアプリでは mdpi と hdpi のリソースを用意すれば十分であり、システムによって適宜拡大縮小されます。tvdpi リソースを指定する必要がある場合は、1.33*mdpi の係数でサイズを指定する必要があります。たとえば、mdpi 画面で 100px x 100px の画像は、tvdpi では 133px x 133px になります。

各種密度に応じた代替描画可能ビットマップを作成するには、6 つの密度分類の間に 3:4:6:8:12:16 のスケーリング比を適用する必要があります。たとえば、中密度画面向けの 48x48 ピクセルのビットマップ ドローアブルがある場合、各画面向けのサイズは次のようになります。

  • 低密度(ldpi): 36x36(0.75 倍)
  • 中密度(mdpi): 48x48(1.0 倍、基準)
  • 高密度(hdpi): 72x72(1.5 倍)
  • 超高密度(xhdpi): 96x96(2.0 倍)
  • 超超高密度(xxhdpi): 144x144(3.0 倍)
  • 超超超高密度(xxxhdpi): 192x192(4.0 倍)

生成した画像ファイルを res/ の適切なサブディレクトリに置くと、アプリが実行されているデバイスのピクセル密度に基づいて正しいファイルが自動的に選択されます。

    res/
      drawable-xxxhdpi/
        awesome-image.png
      drawable-xxhdpi/
        awesome-image.png
      drawable-xhdpi/
        awesome-image.png
      drawable-hdpi/
        awesome-image.png
      drawable-mdpi/
        awesome-image.png
    

また、@drawable/awesomeimage を参照すると画面の dpi に基づいて適切なビットマップが必ず選択されます。密度に対応する密度固有のリソースを指定しない場合は、次に適したものが選択され、画面に合わせてスケーリングされます。

ヒント: スケーリングしてはいけないドローアブル リソースがある場合(実行時にご自身で画像を調整する場合など)は、nodpi 設定修飾子が付いたディレクトリにそのリソースを配置する必要があります。この修飾子の付いたリソースは密度非依存と見なされ、スケーリングされません。

その他の設定修飾子と、現在の画面設定に適したリソースがどのように選択されるかについて詳しくは、リソースを指定するをご覧ください。

アプリアイコンを mipmap ディレクトリに配置する

他のすべてのビットマップ アセットと同様、アプリアイコンの密度固有のバージョンを指定する必要があります。ただし、アプリアイコンは、アプリ ランチャーによっては、デバイスの密度バケットで求められているものより 25% 大きく表示されます。

たとえば、デバイスの密度バケットが xxhdpi で、提供するアプリの最大サイズが drawable-xxhdpi の場合、このアイコンは拡大されて不鮮明になります。したがって、mipmap-xxxhdpi ディレクトリにはより密度の高いランチャー アイコンを配置する必要があります。これにより、ランチャーが xxxhdpi アセットを使用できるようになります。

このようにアプリアイコンは拡大される可能性があるため、すべてのアプリアイコンを drawable ディレクトリではなく mipmap ディレクトリに配置する必要があります。drawable ディレクトリとは異なり、密度固有の APK を構築しても mipmap ディレクトリはすべて APK に保持されます。これにより、ランチャー アプリが最適な解像度のアイコンを選択してホーム画面に表示できるようになります。

    res/
      mipmap-xxxhdpi/
        launcher-icon.png
      mipmap-xxhdpi/
        launcher-icon.png
      mipmap-xhdpi/
        launcher-icon.png
      mipmap-hdpi/
        launcher-icon.png
      mipmap-mdpi/
        launcher-icon.png
    

アイコンのデザインのガイドラインについては、アイコンのマテリアル ガイドをご覧ください。

アプリアイコンの作成については、Image Asset Studio を使用してアプリアイコンを作成するをご覧ください。

ベクター グラフィックを使用する

複数の密度固有の画像を作成する代わりに、ベクター グラフィックを 1 つ作成することもできます。ベクター グラフィックでは、XML を使ってパスと色を定義して画像が作成されます。その際、ピクセル ビットマップは使用されません。このため、ベクター グラフィックはスケーリングの乱れを発生させることなくどのサイズにでも拡大縮小できますが、通常は写真ではなくアイコンなどのイラストにおすすめします。

多くの場合、ベクター グラフィックは SVG(Scalable Vector Graphics)ファイルとして提供されますが、Android ではこの形式がサポートされないため、SVG ファイルは Android のベクター ドローアブル形式に変換する必要があります。

Vector Asset Studio を使うと、次のように簡単に SVG を Android Studio からベクター ドローアブルに変換できます。

  1. [Project] ウィンドウで res ディレクトリを右クリックし、[New] > [Vector Asset] を選択します。
  2. [Local file (SVG, PSD)] を選択します。
  3. インポートするファイルを見つけて、調整します。

    図 3. Android Studio を使って SVG をインポートする

    [Asset Studio] ウィンドウに、ベクター ドローアブルでサポートされないファイル プロパティが存在することを示すエラーがいくつか表示される場合がありますが、これがインポートが妨げられる原因となることはありません。サポートされていないプロパティは無視されます。

  4. [次へ] をタップします。

  5. 次の画面で、プロジェクトで必要なファイルがあるソースセットを確認し、[Finish] をクリックします。

    すべてのピクセル密度で 1 つのベクター ドローアブルを使用できるため、このファイルはデフォルトのドローアブル ディレクトリに保存されます(密度固有のディレクトリを使用する必要はありません)。

        res/
          drawable/
            ic_android_launcher.xml
        

ベクター グラフィックスの作成について詳しくは、ベクター ドローアブルのドキュメントをご覧ください。

一般的には見られない密度の問題に関するアドバイス

ここでは、Android でさまざまなピクセル密度でビットマップのスケーリングを実行する方法や、さまざま密度でのビットマップ描画をより細かく制御する方法について詳しく説明します。アプリでグラフィックを操作しない場合や、さまざまなピクセル密度で問題なく実行できる場合は、読まなくてもかまいません。

実行時に複数の密度でグラフィックを操作できるようにサポートする方法をよりよく理解するには、まずシステムが以下の方法でビットマップのスケーリングをサポートしていることを理解する必要があります。

  1. リソース(描画可能ビットマップなど)の事前スケーリング

    システムは使用時の画面の密度に応じたアプリの密度固有のリソースを使用します。該当する密度のリソースがない場合は、デフォルトのリソースが読み込まれ、必要に応じてスケールアップまたはスケールダウンされます。デフォルトのリソース(設定修飾子が付いていないディレクトリのリソース)は基準のピクセル密度(mdpi)に合わせて設計されていると見なされ、ビットマップのサイズはそのときのピクセル密度に適したサイズに変更されます。

    事前にスケーリングされたリソースの寸法をリクエストすると、スケーリングを実行した後の寸法を表す値が返されます。たとえば、mdpi 画面用に設計された 50x50 ピクセルのビットマップは hdpi 画面では 75x75 ピクセルにスケーリングされ(hdpi 用の代替リソースがない場合)、このサイズが返されます。

    場合によっては、Android でリソースを事前にスケーリングする必要がないことがあります。事前スケーリングが行われないようにする最も簡単な方法は、nodpi 設定修飾子が付いたリソース ディレクトリにリソースを配置することです。次に例を示します。

    res/drawable-nodpi/icon.png

    このフォルダ内の icon.png ビットマップは、使用時のデバイスの密度に基づいてスケーリングされません。

  2. 寸法と座標のピクセル値の自動スケーリング

    寸法および画像の事前スケーリングを無効にするには、マニフェストで android:anyDensity"false" に設定するか、またはプログラムを使用して Bitmap に対する inScaled"false" に設定します。こうすると、描画時にピクセルの絶対座標値とピクセルの寸法値が自動的にスケーリングされ、ピクセルで定義された画面要素が基準の画面密度(mdpi)で表示される場合とほぼ同じ物理サイズで引き続き表示されます。このスケーリングはアプリに対して透過的に処理され、アプリには、ピクセルの物理的な寸法ではなくスケーリングされたピクセルの寸法が返されます。

    たとえば、デバイスが従来の HVGA 画面とほぼ同じ 480x800 のサイズの WVGA 高密度画面を備えているとします。このデバイスでは、事前スケーリングを無効にしているアプリが実行されています。この場合、システムは画面寸法を照会する際にアプリに「嘘」をついて 320x533(画面密度のおおよその mdpi 変換)を返します。次に、アプリが (10,10) から (100,100) の四角形を無効にするなど描画操作を実行する際に、システムは座標は適切にスケーリングして変換し、(15,15) から (150,150) の領域を実際に無効化します。アプリがスケーリングされたビットマップを直接操作する場合は、この不一致により予期しない動作が発生する可能性がありますが、これは、アプリのパフォーマンスを可能な限り良好に維持するための妥当なトレードオフであると見なされます。この状況が発生した場合は、dp 単位からピクセル単位に変換するをご覧ください。

    通常は、事前スケーリングを無効にしないでください。複数の画面をサポートするには、このドキュメントで説明した基本的な手法を活用することをおすすめします。

アプリでビットマップを操作する場合、またはその他の方法で画面上のピクセルを直接操作する場合は、さまざまなピクセル密度をサポートするために、追加の手順を実行しなければならないことがあります。たとえば、指でなぞったピクセルの数を数えることでタッチ操作に応答する場合は、実際のピクセルではなく適切な密度非依存ピクセルの値を使用する必要がありますが、dp と px の値は簡単に変換できます。

すべてのピクセル密度でテストする

UI が正しくスケーリングされるように、複数のデバイスを使ってさまざまなピクセル密度でアプリをテストすることが重要です。物理デバイスでのテストは簡単ですが、さまざまなピクセル密度に対応する物理デバイスにアクセスできない場合も Android Emulator でテストできます。

デバイスを購入することなく物理デバイスでテストしたい場合は、Firebase Test Lab で Google データセンターのデバイスにアクセスできます。