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

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 ディレクトリと異なり、すべての mipmap ディレクトリは、密度固有の APK をビルドしても 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 のベクター ドローアブル形式に変換する必要があります。

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

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

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

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

  4. [Next] をクリックします。

  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 データセンターのデバイスにアクセスできます。