Androidの解像度の違いを考慮したデザイン基礎知識

多様にある Android 端末の画面サイズと解像度

Android 端末は様々なメーカから発売されており、多種多様な端末があります。スマートフォンとタブレットも合わせるとかなりの数になります。

そんな Android のアプリデザインを作っていく上で避けて通れないのは、多様にある端末環境のことを考慮したUIデザイン設計です。端末の種類が限られている Apple の iPhone とは異なった事情があります。

考慮してデザイン制作と実装を進めていかないと、せっかくデザイナーさんが頑張ってイケてるデザインを作ったのに実装後に残念な見栄えになってしまったりします。

プログラマとデザイナーは密にやり取りをすべき

デザイナーが画像素材を用意してプログラマがそれを使って実装していくのが一般的ですが、出来ればどういう風に素材を用意して欲しいかお互い相談しながら制作を進めていくべきでしょう。

  • 画像スライスの仕方(png への切り出し方)
  • 用意する画像サイズ
  • 基本 UI パーツのデザインをそのまま使うかカスタムデザインにするか
  • クライアントやデザイナーがやりたいデザインをそもそも実現できるのか

また、相談する時はお互いに基礎知識を持っていたほうが話が早いですが、デザイナーが基礎知識を知っているとは限らないので、プログラマの人は必ず理解しておくべきだと思います。

現場によるかとは思いますが、「Web デザインしか経験のないデザイナーさんにアプリ素材を作ってもらうことになった」とか、割とよくある話だと思います。

主要な用語の意味

ここからの基礎知識の説明にあたり、色々と用語が登場するので、先に意味を書きます。

現実世界の物理的なサイズ単位(センチメートルとか)と、抽象的な論理サイズ単位(dip とか)があるので、ごっちゃにならないように区別して考えたほうが良さそうです。

ピクセル(px)とは

画像や文字を構成する最小単位の点(ドット)。800px は最小単位の点が800個あることを表す。

インチ(inch)とは

1インチ = 2.54cm の物理サイズ。端末の画面サイズを表す時に利用される。例えば7インチタブレットとは対角線の長さが 17.78cm の画面サイズのタブレットのことを指す。

dpi とは

Dot Per Inch の略。1インチ(=2.54cm)の幅にドット(画像や文字を構成する最小単位)がいくつあるか、その数を表す。この値が大きいほど1ドットの物理的な大きさが小さくなり、よりきめ細かくなる。

端末の解像度だけで dpi 値が決まるわけではなく、画面の物理サイズ(インチ)も影響する点に注意。

dip(dp)とは

Density Indipendent Pxcels の略で、画面密度に依存しない抽象的なピクセルを表す。Android で大きさを指定する時に使う単位。dip を省略してdpとも表記する。

Android 端末が 160dpi(mdpi)のときに 1dip は 1px で表示される。また、320dpi(xhdpi)のときは倍の密度なので 1dip は 2px で表示される。

dip でサイズ指定しておけば、画面密度が異なる端末でもほぼ同じ物理サイズで表示してくれる。48dip を指定すると物理的な大きさは 9mm 前後になる。

px で直接サイズ指定しちゃうと、画面密度が異なる端末で見えるサイズがもろに変わってしまうので、基本的に扱わない。

※ dip と dpi があってややこしいけど、別な単位なので注意。

画面密度に合わせた素材をそれぞれ用意する

Androidには画面密度(dpi)の大きさの分類がいくつかあります。その分類ごとに画像サイズを調整した素材をそれぞれ用意しておくことが出来ます。

画面密度(dpi)の分類一覧

画面密度分類 dpi px/dip シェア
(2012年)
シェア
(2016年)
備考
ldpi 120dpi 0.75px/dip 2.2% 2.7% ほぼ使われていません
mdpi 160dpi 1px/dip 18.0% 13.6% シェアは低下傾向です
基準となる分類です(baseline)
tvdpi 213dpi 1.33px/dip 2.4% Nexus7(2012)などが
該当します
hdpi 240dpi 1.5px/dip 51.1% 42.4% 主流です
xhdpi 320dpi 2px/dip 28.7% 24.1% 主流です
xxhdpi 480dpi 3px/dip 14.8% シェアを伸ばしています

※シェアのソース: Dashboards | Android Developers

大きさの比率: 1(mdpi) : 1.33(tvdpi) : 1.5(hdpi) : 2(xhdpi) : 3(xxhdpi)

なので例えば、xhdpi 用の素材を75%のサイズにしたものが hdpi 用素材となります(2*0.75=1.5)

大は小を兼ねるから xxhdpi 用の素材だけ用意すればいいんじゃないの?

その通りとも言えます。大きい dpi 向けの大きい素材だけ用意しておけば勝手に縮小して表示してくれます。

ですが、小さい dpi 向け素材を用意しておかないと以下の様なデメリットも発生しますので、シェアの多い dpi 向けの素材は用意しておいた方が無難かもしれません。

Android:xhdpiリソースしか持たないアプリが抱える問題

全ての dpi 種別向けに素材を用意していると当然アプリサイズ(容量)も大きくなるのでケースバイケースで考えましょう。

デザイナー目線から見た UI 設計の仕方

  • 多様な画面サイズがあることを見越した UI 設計
  • リキッドレイアウトや 9-patch などを活用して解像度が異なっても大丈夫なデザインにする

結局のところデザイナーは何の解像度を基準に素材を用意すべきか

横幅: 320dip 〜 360dip を目処に
縦幅: 480dip 〜 540dip を目処に。480dip で収まるようにしておけば大体の機種で可視領域に収まるはず

※必ずしも上記 dip 範囲に収まるわけではありません。

縦幅は様々な種類があるので、高さが違っても大丈夫なデザインにしときましょう。
横幅は 320dip と 360dip の2タイプが主流です。

仮に320x480dipを基準画面サイズと考え、これを px に直すと以下になります。

mdpi向け素材: 320×480(px)
hdpi向け素材: 480×720(px)
xhdpi向け素材: 640×960(px)

タブレットの場合はもっと大きいpx(dip)が想定されます。

デザイナーはどういう手順で用意すべきか

まずは市場に出回っている最も大きい dpi を基準に素材を用意して、最後に小さい dpi 向けにサイズだけ小さくした素材をまとめて作成するといいです。

最近やった事例だと、まず xhdpi 用の素材を作成し、最後にそれを75%サイズに縮小した hdpi 用素材もまとめて作成という手順で用意しています。

dip という大きさの感覚

スマートフォンの画面の横幅は 320dip というのを一つの目安に覚えておくと良さそうです。
タブレットの場合はもっと大きいです。例えば Nexus7 の場合は 600dip(800px * 0.751density)もあります。

エンジニア目線のレイアウトの組み方

  • フォントのサイズは sp 、それ以外のサイズは dip が基本
  • MATCH_PARENT(FILL_PARENT)や weight 指定を活用したリキッドレイアウトを活用する
  • 大きさを指定する単位に直接ピクセルを扱うことは避け、dip を利用する
  • AbsoluteLayout は避ける
  • ゲームなどでアスペクト比をきっちり維持して表示させなくてなならない場合は、上下に黒帯を入れる方法などを検討する

ImageView のサイズを WRAP_CONTENT にすると表示ピクセル数はどうなる?

例えば100×150ピクセルの画像を WRAP_CONTENT で表示した場合、基本的には以下のように調整されて表示されると思われます。

  • mdpi 環境の機種では100×150ピクセルで表示される
  • xhdpi 環境の機種では200×300ピクセル(2倍)で表示される(なので xhdpi 用素材が必要という理屈)

それ以外にも、様々な要因でサイズは調整されます。
参考: ImageViewの表示サイズの決まり方(リライト)

参考サイト

編集履歴

  • 2013/03/13追記: 一部 dpi 分類名に誤りがあったので修正しました。
  • 2016/02/23追記: Densities ごとのシェア情報に2016年1月の情報を加えました。リンク切れを修正しました。

Androidのアドレス帳を読み書きするContactsAPIとは何者か暴く

Android Contacts APIとは、どんなAPIか その可能性

AndroidSDKは端末内部のアドレス帳へアクセスするためのインターフェースとなるContactsAPIを用意しています。

端末内連絡先という重要なリソースへのアクセスを担うContactsAPIはアプリ開発の幅を広げるためには重要なAPIではないでしょうか。

ですが、このAPIの仕様は煩雑で癖が強いです。直感的に欲しいデータにアクセスできませんし、書き込みもできません。

根本的なContactsAPIの概念的な部分を分かっていないとちょっとイライラすると思います。

また、似ているアドレス情報の連絡先を勝手に集約してまとめるような仕組みがAndroidOSレベルで用意されていたりするため開発者の混乱を招く原因になっていたりします。

また、データにアクセスするためのキー項目の特徴を知っていないと思わぬことが起きたりします。

ネット上に散らばっているサンプルソースを取り敢えず動かしてみて「何かよく分からないけど欲しいデータが取れた!」「でもホントはこういうデータを取り出したいんだけどどう書けばいいか分からない」というケースが結構想定されるAPIなのではないかなーと思います。

ContactsAPIについての抑えるべきポイントと概要を説明していきます。

Androidではアドレス帳をどういった形態で保持しているか

Android内部ではSQLiteに連絡先データが保存されているようです。
ただ、そのSQLiteデータベースに直接アクセスすることはできません。危険すぎます。

そこでアクセスインターフェースとしてContactsAPIが用意されています。
ContactsAPIもかなりSQLite側を意識した作りになっているため、
アクセスの仕方自体はほぼ変わりはないです。

アドレス帳データのSQLiteのテーブル設計はどうなっているか

Android内部のDB設計が分かっていないばかりに以前苦労した記憶があります。

まさに「何かよく分からないけど欲しいデータが取れた」状態で使っていましたが、テーブル構成が分かった瞬間にかなり道がひらけました。

これをもっと早く知っていればよかった…と思いましたが、同じ思いをする人が一人でも減ってくれることを願いER図を用意しました。

AndroidContactsAPIのER図

※緑のフィールドは外部キーです。主要なフィールドのみ載せています。

SQLiteは全てのテーブルに_idという一意のサロゲートキーを必ず持ちますが、ContactsAPIにおいてもSQLiteシステムの_idを主キーとして利用しています。

全てのテーブルに_idがあるという点が一つのポイントとなります。

それではテーブルごとに概要を説明していきます。

dataテーブル

上記ER図中では最小単位となるテーブルで、行ごとに電話番号やEメールアドレスといった実データが保存されています。ER図の関係の通り一つのraw_contactsレコードに所属するデータが複数あり、データの数だけdataレコード沢山があります。

そのデータ種別を区別するためにMIMEタイプというフィールドが用意されています。種類についてはContactsContract.CommonDataKindsのリファレンスを参照してください。

また、実データはdata1, data2といったフィールドに格納されています。MIMEタイプごとにそれぞれのdataが何を表すかということが決められています。

例えば電話番号の場合はdata1に電話番号、data2に種別(自宅や勤務先)、data3にラベルが格納されています。MIMEタイプごとのdataフィールド群の対応については、先ほどのContactsContract.CommonDataKindsのリファレンスからたどって調べれば分かると思います。

raw_contactsテーブル

dataテーブルにある一人分の名前や電話番号のデータレコードを一つにまとめます。1レコードで一人分のデータを表現します。直訳で生のコンタクトテーブルになり、後述のコンタクトテーブルよりもより直接的なデータとなります。

アカウント名やアカウントタイプといったフィールドもありますが、これは外部のクラウドストレージサービスの情報になります。例えば自分のGoogleアカウントやDocomoアカウントと紐付けて保存されていたりします。

contactsテーブル

このテーブルがクセモノです。いくつかのraw_contactsテーブルのレコードを更にまとめます。

どういうことかというとAndroidには似ている連絡先情報を自動で集約する仕組みが備わっており、ある基準で似ている連絡先と判断された連絡先を一つにまとめるためにこのテーブルは用意されています。

つまり、いくつかの似ているrow_contactsレコードをひとまとめにしてcontactsレコードを持つことがあるということです。ただ、この集約は恒久的なものではなく自動的に集約が解除されたりもします。

何を持って似ているかという判断基準はOSでちゃんと定められています。例えば姓名がそれぞれ一致したとか、姓だけ一致したら更にうんにゃらとか。興味がある人は調べてみるとよいでしょう。

こういった仕組みが用意されている意味としては、raw_contactsテーブルでは複数のアカウントの情報を持つ関係上、クラウドの連絡先と同期をとった時に似たような同一人物のデータが複数端末内にできてしまうため可能な限りそれを一括りにまとめようというような目的のようです。

一般的にユーザーに連絡先データを表示する場合はcontactsテーブルのレベルで見せるべきです。そのためのビューも提供されています。

自動集約の弊害

普通に考えて一人を特定するためのキーとしてはcontactsテーブルの_id(contacts_id)があればよいこととなります、が、集約機能のための弊害があります。

自動集約でレコードがまとめられる時にcontacts_idが変わってしまうことがあります。なのでcontacts_idをユーザーを特定するための恒久的なキーとして利用すべきではありません。

では、恒久的に1ユーザーを特定するためにはどうするのか。そのためにルックアップキーというものが用意されています。ユーザーへの参照をアプリ内で持っておきたい場合はこのキーを保持しておくのがよいでしょう。ただ、このキーにもデメリットがありますので、次の項の中で解説します。

プログラムコードサンプル

実際にコードサンプルを紹介していきます。

アドレス帳内の全コンタクトにループ処理でアクセスする

ループで全ユーザーにアクセスします。上記ER図でいうところのcontactsテーブルへのアクセスになります。

Cursor c = contentResolver.query(Contacts.CONTENT_URI, new String[] {Contacts._ID, Contacts.LOOKUP_KEY, Contacts.DISPLAY_NAME },null, null, null);
c.moveToPosition(-1);
while (c.moveToNext()) {
    // カーソルからデータ取り出し
    int contactsId = c.getInt(0);
    String lookupKey = c.getString(1);
    String displayName = c.getString(2);
    // ダンプ
    Log.d("contacts", "contactsId" + contactsId);
    Log.d("lookupKey", "lookupKey" + lookupKey);
    Log.d("displayName", "displayName" + displayName);
}

ルックアップキーで特定のユーザーの表示名を取得するコード例

ルックアップキーは基本的に固定の検索キーで、ユーザーを特定するためのキーとしては最適ですが検索キーとしての高速性に欠けます。

以下がルックアップキーのみでUriを作成し名前を取得する例です。ループで大量に処理をしたりしないのであればこの方法でも問題ないでしょう。

Uri lookupUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);

Cursor c = getContentResolver().query(lookupUri, new String[]{Contacts.DISPLAY_NAME}, ...);
try {
    c.moveToFirst();
    String displayName = c.getString(0);
} finally {
    c.close();
}

ルックアップキーとコンタクトIDを組み合わせたユーザー検索

contacts_idによる高速な検索とルックアップキーによる確実な検索の2つのメリットを合わせたユーザー検索方法が予め用意されています。

まずcontacts_idでの検索を試みて、失敗した場合はルックアップキーを利用した検索を行なってくれます。

大量にデータを処理する場合はこの方法でUriを作成すべきです。

Uri lookupUri = Contacts.getLookupUri(contactId, lookupKey);

Dataテーブルの実データの取得

contacts_idを元に電話番号を取得してみます。一人に対して複数の電話番号が設定されている場合があるのでカーソルループで取得します。

Dataテーブルには電話番号以外のデータも沢山入っているため、Where句でMIME TYPEで絞り込んでいます。

Cursor cursor = null;
try {
    cursor = contentResolver.query(Data.CONTENT_URI, null,
        Data.CONTACT_ID + " = ? AND " + Data.MIMETYPE + " = ?",
        new String[] { contactsId, Phone.CONTENT_ITEM_TYPE }, null);
    // 電話番号ループ
    cursor.moveToPosition(-1);
    while (cursor.moveToNext()) {
        String tel = cursor.getString(cursor.getColumnIndex(Data.DATA1));
        Log.d("tel", tel);
    }
} finally {
    cursor.close();
}

ContactsAPIまとめ

テーブル構成と自動集約に伴う罠はありますが、逆にいうとそこら辺さえ抑えればさほど難しくないと思います。

テーブル構成さえ分かっていれば上記で載せたサンプルコード以外にも色々と応用したアドレス帳アクセスもできるかと思います。

あと今回はサンプルコードを載せませんでしたが、プロフィール画像についてもDataテーブル内にBlobで直接格納されています。ちょっと探せばサンプルあるはずです。

それではよいContactsAPIライフを。

参考サイト