Android

9-patch入門

More than 3 years have passed since last update.


9-patchとは

9-patchとは、Androidで利用できる一部分のみ拡縮可能になった画像のことです。

9-patch画像を適切に利用することで、下図のように▼や罫線の太さを保ったまま画像を拡大することができるようになります。

Android、今さら人に聞けない9パッチグラフィック | dotproof

[Android アプリの UI デザイン] 9-patch の作りかたのまとめと Tips | Developers.IO

チュートリアル:9patchで画像を作る « Tech Booster


9-patch画像の構成

9-patch画像は通常の画像の外周に1ピクセルの指定領域に画像の拡大処理に関する設定を描き加え、拡張子を9.pngに変更したものです。

Android Sdkに付属しているDraw 9patchツール(Android Studioから起動できます)を利用して編集することができます。

また、任意の画像から各dpi向けの9patchを作成できるSimple Nine-patch Generatorも便利です。

以下、AndroidSdkに含まれているbtn_default_normal_holo_light.9.pngを元に9-patch画像の要素について解説していきます。

ANDROID_SDK/platforms/android-23/data/res/drawable-xxhdpi/btn_default_normal_holo_light.9.png


Stretchable Patches

Stretchable Patchesは9-patch画像をより大きな領域に描画する際、拡大して描画される領域です。

通常の画像と異なり、9-patch画像ではStretchable Patches以外の部分が拡大されないため、角丸や影を崩さずにボタン画像などを描画することができます。

Stretchable Patchesは9-patch画像外周の上辺および左辺に黒色の線で指定します。

図中の緑色の部分が1方向に拡大されるStretchable Patches、ピンクの部分が2方向に拡大されるStretchable Patchesです。

20160110231042_7d8720d7006253f9d499021ba726dc04e6e09e90.png


Contents Area

Contents Areaは描画する際にコンテンツ(テキストなど)を表示する領域です。

Contents Areaがある9patch画像をViewの背景として指定した場合、Contents Area以外の領域がViewのpaddingに設定され、コンテンツはContents Areaに収まるように描画されます。

Contents Areaの指定は9-patch画像の右辺および下辺に黒色の線で指定します。

下図の紫色の領域がContents Areaです。描画サイズに応じてContents Areaは変わっていきますが、外側の枠(padding)は維持されています。

20160110232331_b0c6f70e842ffe885b575f1395d843245a8e838a.png


Optical Bounds

Optical BoundsはAPI レベル18で追加されたOptical bounds layoutに対応した要素です。

Optical Boundsを利用することで影や余白などの要素をViewのレイアウト領域から除外することができます。

Android 4.3 APIs | Android Developers

下表が実際にOptical Boundsを有効にして描画した例です。

左側のAndroid 5.1.0端末では9-patch画像でボタン周囲にあった余白部分がViewのレイアウト領域から除外され、TextViewの描画領域と重なっているのがわかると思います。

TextViewに何も指定しなくても自動的にボタンの余白と同じ幅で余白が取られていますが、これもOptical Boundsの機能です。

このようにマージン指定などの記述を減らして見た目の統一感を持たせることができますOptical Boundsですが、残念ながらAndroid 4.3未満の端末では有効になりません…(下表右側)。

Android 5.1.0
Android 4.1.1

20160111135817_62d37acaae743a6bf6ebd43e78857dd63a51162c.png
20160111135838_b1e96176cc4a23a3c71c7bb04e286a639fe7fb8c.png


layout.xml

<LinearLayout

android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layoutMode="opticalBounds"
android:background="@android:color/holo_orange_light"
android:orientation="vertical">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loremipsum" />

<TextView
android:background="@drawable/btn_default_normal_holo_light"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
</LinearLayout>


Optical Boundsは9-patch画像の右辺および下辺に赤色で指定します。

MacではDraw 9-patchツールでCtrl+クリック(またはドラッグ)すると赤色の線が引けます。

線を消す時はShift+Ctrl+クリック(またはドラッグ)です。


9-patch アンチパターン


大きすぎる9-patch画像

9-patch画像は基本的に縮小が下手です。

描画領域よりも大きい9-patchを背景に指定した場合、Stretchable Areaしか縮小されないため思った通り表示されない場合があります。


Stretchable Patchesが小さい

他のdrawable画像と同様、端末の画面密度向けリソースに同名の9-patch画像が提供されていない場合には、9-patch画像も描画前に画素密度に応じてリサイズされます。

この時にStretchable Patchesが小さすぎる場合、複数のStretchable Patchesのサイズ比に不整合が生じたり、Stretchable Patchesが消失してしまったりします。

これはNexus6で文字の左右が均等に拡大されるように作った9-patchがうまく描画されない不具合の原因のひとつです。

例として、以下のような左右2ドットずつのStretchable Patchesを持った画像をdrawable-xhdpiにのみ配置するとします。

ic_image_filter_frames.9.png

上記の画像をNexus6で表示すると、吹き出し部分がわずかに左にずれます。

これはxhdpi(320dpi)からNxus6の560dpiに変換する際、右側のStretchable Patchesが左側よりも1ドット大きくなり、その後で9-patchとして解釈されたためと考えられます。

20160111165318_19cabcc7cc284c06ec0b8dd7b81c0c7740c4839a.png

左右のStretchable Patchesの幅を4ドットずつにしてみると、このズレは発生しなくなります。

(幅を広げるほどズレが発生しにくくなるだけで、確実にズレを防ぐことはできません)

20160111165344_78d07d2e8e05372d2abea45eb61a8bdf0c1e0004.png

Android DevelopersではStretchable Patchesは最低2x2以上で定義することとなっていますが、上記から見ても4x4以上のサイズで定義したほうがDraw 9-patchツール上での見やすさからもサイズ計算上でも安心だと思います。


特定のdpi向けにしか9-patch画像を提供していない

前述の通り9-patch画像は端末によってリサイズされる可能性があり、Nexus6などでのズレの原因となります。

ズレを防ぐためには、Stretchable Patchesを大きくする以上に複数のdpi向けに9-patchを用意することが有効です。

また、drawable-nodpiに入れられるような9-patchであればそちらも検討したほうが良いでしょう。

drawable-nodpiに9-patch画像を格納した場合、端末の画面密度によるリサイズが発生しないためズレが発生しなくなります。

(代わりに角丸や吹き出し部分も拡大されなくなってしまうため、適用できる画像は限られます)


padding設定を上書きしている

Contents Areaを持った9-patchを背景に指定したViewにpaddingを設定すると、9patchに設定されているContents Area情報が上書きされてしまいます。

ちなみにOptical Boundsはmarginに設定されるわけではないため、marginを設定しても上書きされることはありません。


すべての領域をStretchable Patchesに指定している

この場合、画像が単純に縦横に引き伸ばされるだけになってしまいます。

Contents Areaを設定した場合はpadding設定の効果は残っていますが、paddingはViewサイズを考慮していない9-patch画像内のdpで指定されるため、大抵の場合はズレます。

例として、以下のような9-patch画像を背景に指定した場合、縦方向の拡大がContents Areaと咬み合わなくなるため、表示が吹き出しに収まらなくなります。

ic_image_filter_frames.9.png

20160111175846_378501d03cedc6f716988d182ef2f64f516d9f9a.png


グラデーション

画像の一部を引き延ばすという仕組み上、グラデーション表現は9-patchに向いていません。

よく見かける「角丸で背景グラデーションの9-patch」はxmlで表現可能なので、できる限りxmlで表現したほうが良いでしょう。


gradient_button.xml

<shape xmlns:android="http://schemas.android.com/apk/res/android"

android:shape="rectangle">
<gradient
android:startColor="@android:color/holo_orange_dark"
android:endColor="@android:color/holo_orange_light"
android:angle="270" />
<corners android:radius="8dp" />
<padding
android:left="10dp"
android:top="10dp"
android:right="10dp"
android:bottom="10dp" />
</shape>


20160111180832_2acb5b01a4759f77097c7eb227550d32378d5bdf.png


ストライプやドットなどの繰り返しパターン

グラデーション同様、9-patchには向いていません。

下記のようにdrawable画像を繰り返し指定したdrawableリソースを定義することはできますが、角丸などの定義はxmlだけではできません。

View階層を重ねて描画領域を調整したり、square/picassoなどで画像加工する必要があるかもしれません。

<bitmap xmlns:android="http://schemas.android.com/apk/res/android"

android:src="@drawable/striped_tile"
android:tileMode="repeat" />