9patchの異常な仕様、あるいはなぜ私はドットを打つのをやめて scriptを書くようになったか
これは俺の Advent Calendarの 3日目の記事です。
今日はガチ Androidの話を書きます。面倒くさい PNGの編集を gradle plugin を作って楽にした話です。時間がない人は 後半から読んでもらうといいかもです。
9patchは必要だけどクソ
Android programmerなら 9patch というのを聞いたことがあると思います。任意のサイズにあわせられるように、PNG画像を 9分割して、真ん中の部分を縦横に伸ばすやつです。
ご存知のようにこれは通常の PNGのを上下左右 1pixelずつ広げて、そこに特殊な黒い点を打つことによって指定します。はっきり言って変な仕様です。きっと設計したプログラマは「やった、一つのファイルで拡大する場所と画像を両方指定できる! 関連するデータが一つになって便利だ! 俺頭いい!!」とか思ったんでしょう。
でもこれを今からディスります。
UI Designerにあまり頼めない
9patchの仕様をちゃんと理解している UI Designerにあったことがありません。お願いしてみたことはありますが、指定するところが厳密に黒と透明じゃなかったりして、aaptにエラーをはかれたりして自分で直す羽目になったりしました。
まあ、UI Desingerがやってくれたとして、そうしたら彼らが以下の労苦を背負うことになるわけですが。
そんなこんなで Designerは単純な PNGと広がる場所の座標指示をくれて、Programmerがしこしこ Google謹製の draw9patch
とかで編集したりするわけです。なれないマウスとかを使って。いや、最近の Programmerはマウスぐらいは慣れてるか。それでも普段使い慣れた text editorや IDEから離れて作業するつらみがあります。
dpiごとに一つ一つ編集しなきゃいけない
Androidは dpiに応じて別々の PNGを用意する必要があります。ldpiは最近は無視していいでしょうが、mdpi,hdpi,xhdpi,xxhdpiの4種類は用意することが多いでしょう。頑張る場合、xxxhdpiもいるかもしれません。
9patchは編集したものを拡大、縮小するわけにはいきません。streach/paddingを指定するエリアの幅が 1pixelでなくてはならないからです。つまり、各dpiについてしこしこ 9patchを描く必要があります。
同じような広がり方をする画像一つ一つ編集しなきゃいけない
一つの画像がいくつかのバリエーションを持つことはよくあります。たとえば buttonなら押された状態や disabled状態など。それぞれについて 色違いの PNG素材を用意してもらうことも多いでしょう。そうすると、また draw9patch
で編集する対象が増えます。
こんな苦労を画像差し替えのたびに繰り返さなきゃいけない
実際のプロダクツは一度絵をもらって完成、というわけにはいきません。iterationのうちに、UIを見直すのは当然、というかやらねばならないことです。そのたびに、やっぱりこっちの絵のほうがいいよね、となるのは熱心な UI designerのいる幸せな環境では当然、というか喜ぶべきことです。
とはいうものの、こんな苦労をいちいち繰り返さなければならないのでは、9patchの差し替えに怖気づくようになります。確実に昔の自分はそうでした。すべての PNGのふちに正しくドットを書くのは気が遠くなる、かつ間違えたらと思うと恐ろしい作業でした。どこまで終わったのか勘違いすることもよくあります。組み込んで、すべてのバリエーションを通るテストをするのも一苦労です。UIの見栄えのテストなんで自動化もできません。
一方、正しく UI改善で価値を向上しようとしている UI designerはそんな苦労も知らないので、なんで絵の差し替えぐらいでそんなに億劫がるのか理解できず、あなたの技能や仕事に対する態度を疑うことになります。チーム間に溝ができます。あなたの言動をとりあえず negativeに解釈するようになります。ほかの UI変更に対してもどうせできないのかとあきらめるようになります。プロダクトの改善が滞り、マーケットの reviewに一つ星が増えます。プロダクトはキャンセルされあなたは仕事を失い路頭に迷うことになります。南無三。
要するに 9patchはクソ
最後はエスカレートしすぎた気もしますが、要点は以上の問題は乗算的に積み重なるということです。
n種類の dpiについて m 種類のバリエーションががあって、l回差し替えがあったとしたら、おおよそ O(n*m*l)
の時間が費やされるわけです。
こんな maintenancabilityを考えてないシステムは、現状ではクソといえるでしょう。
まあ、元組み込みおっさんエンジニアとしては、8年前の状況を考えればそれも仕方ないかな、という気もしなくもありません。The lean startupも発刊されてなく、ましてやそんな素早い iterationが携帯電話で可能だとは信じられなかった時代です。Waterfall的手法で1回のリリース前に手をかけることはよしとされていた頃ですから。
とはいえ、時代遅れの仕組みに付き合うのはまっぴらです。
gradle-android-9patch-pluginによる改善
前置き長かった気がしますが、そこで gradle-android-9patch-pluginを作りました。要は Programmerが点を一つ一つ打つことなく、gradle scriptで指定すれば自動的に点を打って resourceに突っ込んでくれるものです。
sampleより
面倒くさいので sampleから引っ張ってきます。
こんな round_rect.png
と:
こんな scriptから:
ninepatch {
round_rect {
vStretch 8, 32
hStretch 8, -8
vPadding 7, 33
hPadding 7, -7
}
}
こんな ninepatchができて、元のリソースを overrideします:
ちなみに元の PNGを drawableフォルダに置いておくようになっているため、コードを書いている時でも IDEの resource id補完とかはちゃんと効きます。UI previewとかはおかしくなっちゃいますが、まあ、あれにそんなに精度を求めてる人いないよね、ってことで。
使い方
toplevelの build.gradle
に以下のように書きます:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'io.github.dagezi.ninepatch:plugin:0.0.2'
}
}
appの build.gradle
で pluginを applyして ninepatch対象を書いていきます。
apply plugin: 'io.github.dagezi.ninepatch'
ninepatch {
round_rect { // 通常、これが対象となる PNGファイル名
// 座標はすべて dp指定。mdpiの PNGの座標と思っておけばいい
vStretch 8, 32 // 8行から 31行目が伸びる。end exclusive
hStretch 8, -8 // 負の値は終わりからの距離を示す
vPadding 7, 33 // NG: hdpiでの誤差を考えると偶数にすべき
hPadding 7, -7
}
balloon {
// 複数のファイルを `src` で指定する同じ設定を適用できる
// その場合、closure先頭のファイル名は無視される
src 'balloon'
src 'balloon_red'
// 一方向に複数の stretch領域を指定できる
// 領域の長さによって伸びやすさを指定できる
// この例では、吹き出しの下の部分は上の部分より 2倍伸びるので、
// 常に突端が上側につく。
vStretch 10, 12
vStretch 30, 34
hStretch 12, 32
vPadding 8, -8
hPadding 10, -8
}
また、対象が多くなった時は sampleのように別ファイルにしてもいいでしょう。
感想待ってます
ということで、わが社のプロジェクトではこれを実践導入して UI designerとの幸せな信頼関係を築けて、製品がどんどん良くなってます。嘘です。これからよくなる予定ですが、もう気が大きくなって、Designerには吹き出しの変更でもなんでもバンバン持ってきてよ、と言っています。
あと、gradle pluginを作るのは初めてだったんでまずいところもあるかもしれません。あったら教えてください。その他、ご意見、ご批判などもお待ちしてます。よろしく。
ということで俺の活動についてわかってもらえたでしょうか。明日の俺の advent calendar担当は俺です。何を書くのかさっぱりわかりません。何とかなるでしょう。楽しみですね。