Flex/AIR の、モバイル端末用カスタムスキンを作成しているときに、 FXG (Flash XML Graphics) の scale-9 grid (9スライス) にハマった。
そもそも scale-9 grid とは?
定義
scale-9 grid
とは、日本語で言うところの 9スライス
のこと。あっ、Android界隈では、この 9スライス
のことを、 9-patch
と呼ぶそうな。むー。
Adobe Open Source で公開されている FXG の仕様書を見ると、scale-9 grid
が ベクター分割、slice-9 grid
が ビットマップ分割、というように、英語表記では意味合いが違ってくるので、この記事では、「9スライス」という日本語名称をできるだけ使わずに、scale-9, slice-9 grid を使い分けて表記することにします(どうしても、以下の文章内で、「9スライス」と表記している箇所がありますが、それは画像オーサリングソフトでの表記が"それ"だったり、scale-9とslice-9の総体的な意味合いの"それ"として、表記しています)。
「9スライス」ってなにがおいしいの?
たとえば、次のようなボタンの素材を、Adobe Fireworks で作ったとします。

このとき、ボタンの大きさは、固定だったら楽できて嬉しいんだけど、実際のところは、ボタン内の文字の量とかフォントサイズ等に影響を受けるので、可変。つまり、横長のボタンもあれば、正方形みたいな真四角なボタンもあるかもしれないです。なので、ボタンの大きさが任意に変わっても大丈夫なものをちゃんと用意しなきゃいけないってこと。
先ほどのボタンのベクター画像を、次のように伸縮してみると、下図のように、縦横比を固定しないで拡大/縮小した際に、問題が発生しちゃう。

この問題を解決するために、ここで「9スライス」が大活躍!
Fireworks の場合の手順としては、下図のように、まずは「シンボル化 (オブジェクトを選択して、[F8]キー)」を行って・・・

そんで、そのシンボルを編集して、下図のように、「9スライスのガイド」を設定ッ!(下図のガイドの設定は、あくまで例)

たったのこれだけで、Fireworks での、9スライスの設定が完了ッ!!
9スライスを設定した、このボタンのベクター画像を、先ほどと同様に伸縮させてみると、下図のように、バッチリ拡大/縮小が行えているのが、見て取れるハズ・・・!?

9スライス、バンザイ!
Flex/AIRの、モバイル端末用カスタムボタンスキンの作成
んじゃ、こっから、どこでハマったのか、書いてみる。
ちゃんとうまいこと伸縮しない件について…
まずは、さきほど Fireworks で作成した、ボタンの素材(とりあえず、320 dpi なモバイル端末用のカスタムボタンスキンの up ステートと down ステートの 2 枚だけ)を FXG 形式で書き出しました。



書きだした FXG ファイルのコンパイル済みコードを読みやすいようにちょっとだけ整形して、実際にレンダリングさせてみると、下のような状態です。下のレンダリング画像内の、赤色の補助線は、scale-9 grid の分割線です。
- Button_up.fxg
- Button_down.fxg
net.chsmea.mioproject.skins.test.mobile320.assets.Button_up.fxg

001 | <? xml version = "1.0" encoding = "UTF-8" ?> |
011 | < Graphic version = "2.0" |
020 | scaleGridBottom = "56" > |
026 | < Path data = "M 22 42 C 22 31 31 22 42 22 C 42 22 250 22 250 22 C 261 22 270 31 270 42 C 270 42 270 64 270 64 C 270 75 261 84 250 84 C 250 84 42 84 42 84 C 31 84 22 75 22 64 C 22 64 22 42 22 42 Z " > |
028 | < SolidColor color = "#febf01" /> |
031 | < DropShadowFilter angle = "-90" |
038 | < DropShadowFilter angle = "90" |
045 | < DropShadowFilter angle = "90" |
056 | < Path data = "M 35 41 C 35 41 35 41 35 41 C 35 41 35 41 35 41 C 35 41 36 41 36 41 C 36 41 36 40 36 40 C 36 39 36 38 35 37 C 35 36 35 36 35 35 C 34 34 34 33 34 32 C 33 31 33 31 33 30 C 33 30 33 29 33 29 L 34 29 C 34 29 37 25 37 25 C 37 25 37 25 37 25 C 37 24 38 24 38 24 C 38 24 38 23 39 23 C 39 23 39 23 39 22 C 39 22 40 21 40 21 L 41 20 C 41 20 41 20 41 20 C 41 20 41 19 41 19 C 41 19 41 19 41 19 C 41 19 41 19 40 18 C 40 18 39 18 38 18 C 37 17 36 17 35 17 C 34 17 33 16 32 16 C 31 16 30 16 29 15 C 29 15 28 14 28 13 C 28 12 27 11 27 10 C 27 9 26 9 26 8 C 25 7 25 6 24 5 C 24 5 23 5 23 5 C 23 5 23 5 23 5 C 22 5 22 5 22 5 C 22 5 22 5 22 5 C 21 5 21 5 21 5 C 21 5 21 6 21 6 C 20 6 20 7 20 8 C 19 9 19 10 18 10 C 18 11 17 12 17 13 C 17 14 16 15 16 15 C 15 16 14 16 13 16 C 12 17 12 17 11 17 C 10 17 9 18 8 18 C 7 18 6 19 5 19 C 5 19 5 20 5 20 C 5 20 5 21 5 21 C 6 22 6 22 7 23 C 7 23 8 24 9 24 C 9 25 10 26 10 26 C 11 27 11 27 12 28 C 12 29 11 30 11 31 C 11 31 11 32 11 33 C 10 34 10 35 10 36 C 10 37 10 37 9 38 C 9 38 9 39 9 39 C 9 39 9 39 10 39 C 10 40 10 40 10 40 C 10 40 10 40 10 40 C 10 40 11 41 11 41 C 12 41 13 40 14 40 C 15 40 16 39 17 39 C 18 38 19 38 20 38 C 20 38 23 36 23 36 C 24 36 25 37 26 37 C 27 38 28 38 29 39 C 30 39 31 40 32 40 C 33 40 33 41 34 41 C 34 41 35 41 35 41 C 35 41 35 41 35 41 Z " > |
058 | < LinearGradient x = "23" y = "5" |
061 | < GradientEntry color = "#f2dc5e" |
064 | < GradientEntry color = "#e7ca12" |
070 | < DropShadowFilter angle = "270" |
077 | < DropShadowFilter angle = "90" |
088 | < Path data = "M 276 91 C 276 91 276 91 276 91 C 276 91 277 91 277 91 C 277 91 277 91 277 91 C 277 91 277 90 277 90 C 277 89 277 89 277 88 C 276 87 276 87 276 86 C 276 85 276 85 275 84 C 275 84 275 83 275 82 C 275 82 275 82 275 82 L 275 81 C 275 81 278 79 278 79 C 278 79 278 78 278 78 C 278 78 278 78 279 78 C 279 78 279 77 279 77 C 279 77 280 77 280 77 C 280 77 280 76 280 76 L 281 75 C 281 75 281 74 281 74 C 281 74 281 74 281 74 C 281 74 281 74 281 74 C 281 74 281 74 281 73 C 280 73 279 73 278 73 C 278 73 277 72 276 72 C 276 72 275 72 274 72 C 273 72 273 71 272 71 C 272 70 271 70 271 69 C 271 68 270 68 270 67 C 270 66 269 66 269 65 C 269 64 268 64 268 63 C 268 63 267 63 267 63 C 267 63 267 63 267 63 C 266 63 266 63 266 63 C 266 63 266 63 266 63 C 266 63 266 63 266 63 C 266 63 265 64 265 64 C 265 64 265 65 264 65 C 264 66 264 67 263 67 C 263 68 263 68 262 69 C 262 70 262 70 261 71 C 261 71 260 72 259 72 C 259 72 258 72 257 73 C 257 73 256 73 255 73 C 255 73 254 74 253 74 C 253 74 253 75 253 75 C 253 75 253 75 253 75 C 253 76 254 76 254 77 C 255 77 255 78 256 78 C 256 79 257 79 257 80 C 257 80 258 80 258 81 C 258 82 258 82 258 83 C 258 84 257 84 257 85 C 257 86 257 86 257 87 C 257 88 257 88 256 89 C 256 89 256 89 256 89 C 256 89 256 90 257 90 C 257 90 257 90 257 90 C 257 90 257 90 257 90 C 257 90 257 91 257 91 C 258 91 259 91 260 90 C 261 90 262 90 262 89 C 263 89 264 89 265 88 C 265 88 267 87 267 87 C 268 87 269 88 269 88 C 270 89 271 89 271 89 C 272 90 273 90 274 90 C 274 90 275 91 276 91 C 276 91 276 91 276 91 C 276 91 276 91 276 91 Z " > |
090 | < LinearGradient x = "267" y = "63" |
093 | < GradientEntry color = "#f2dc5e" |
096 | < GradientEntry color = "#e7ca12" |
102 | < DropShadowFilter angle = "270" |
109 | < DropShadowFilter angle = "90" |
net.chsmea.mioproject.skins.test.mobile320.assets.Button_down.fxg

001 | <? xml version = "1.0" encoding = "UTF-8" ?> |
011 | < Graphic version = "2.0" |
020 | scaleGridBottom = "56" > |
026 | < Path data = "M 22 42 C 22 31 31 22 42 22 C 42 22 250 22 250 22 C 261 22 270 31 270 42 C 270 42 270 64 270 64 C 270 75 261 84 250 84 C 250 84 42 84 42 84 C 31 84 22 75 22 64 C 22 64 22 42 22 42 Z " > |
028 | < SolidColor color = "#edae01" /> |
031 | < DropShadowFilter angle = "-90" |
038 | < DropShadowFilter angle = "45" |
046 | < DropShadowFilter angle = "90" |
053 | < DropShadowFilter angle = "90" |
064 | < Path data = "M 35 41 C 35 41 35 41 35 41 C 35 41 35 41 35 41 C 35 41 36 41 36 41 C 36 41 36 40 36 40 C 36 39 36 38 35 37 C 35 36 35 36 35 35 C 34 34 34 33 34 32 C 33 31 33 31 33 30 C 33 30 33 29 33 29 L 34 29 C 34 29 37 25 37 25 C 37 25 37 25 37 25 C 37 24 38 24 38 24 C 38 24 38 23 39 23 C 39 23 39 23 39 22 C 39 22 40 21 40 21 L 41 20 C 41 20 41 20 41 20 C 41 20 41 19 41 19 C 41 19 41 19 41 19 C 41 19 41 19 40 18 C 40 18 39 18 38 18 C 37 17 36 17 35 17 C 34 17 33 16 32 16 C 31 16 30 16 29 15 C 29 15 28 14 28 13 C 28 12 27 11 27 10 C 27 9 26 9 26 8 C 25 7 25 6 24 5 C 24 5 23 5 23 5 C 23 5 23 5 23 5 C 22 5 22 5 22 5 C 22 5 22 5 22 5 C 21 5 21 5 21 5 C 21 5 21 6 21 6 C 20 6 20 7 20 8 C 19 9 19 10 18 10 C 18 11 17 12 17 13 C 17 14 16 15 16 15 C 15 16 14 16 13 16 C 12 17 12 17 11 17 C 10 17 9 18 8 18 C 7 18 6 19 5 19 C 5 19 5 20 5 20 C 5 20 5 21 5 21 C 6 22 6 22 7 23 C 7 23 8 24 9 24 C 9 25 10 26 10 26 C 11 27 11 27 12 28 C 12 29 11 30 11 31 C 11 31 11 32 11 33 C 10 34 10 35 10 36 C 10 37 10 37 9 38 C 9 38 9 39 9 39 C 9 39 9 39 10 39 C 10 40 10 40 10 40 C 10 40 10 40 10 40 C 10 40 11 41 11 41 C 12 41 13 40 14 40 C 15 40 16 39 17 39 C 18 38 19 38 20 38 C 20 38 23 36 23 36 C 24 36 25 37 26 37 C 27 38 28 38 29 39 C 30 39 31 40 32 40 C 33 40 33 41 34 41 C 34 41 35 41 35 41 C 35 41 35 41 35 41 Z " > |
066 | < LinearGradient x = "23" y = "5" |
069 | < GradientEntry color = "#ffffcc" |
072 | < GradientEntry color = "#ffcc33" |
078 | < DropShadowFilter angle = "270" |
085 | < DropShadowFilter angle = "90" |
096 | < Path data = "M 276 91 C 276 91 276 91 276 91 C 276 91 277 91 277 91 C 277 91 277 91 277 91 C 277 91 277 90 277 90 C 277 89 277 89 277 88 C 276 87 276 87 276 86 C 276 85 276 85 275 84 C 275 84 275 83 275 82 C 275 82 275 82 275 82 L 275 81 C 275 81 278 79 278 79 C 278 79 278 78 278 78 C 278 78 278 78 279 78 C 279 78 279 77 279 77 C 279 77 280 77 280 77 C 280 77 280 76 280 76 L 281 75 C 281 75 281 74 281 74 C 281 74 281 74 281 74 C 281 74 281 74 281 74 C 281 74 281 74 281 73 C 280 73 279 73 278 73 C 278 73 277 72 276 72 C 276 72 275 72 274 72 C 273 72 273 71 272 71 C 272 70 271 70 271 69 C 271 68 270 68 270 67 C 270 66 269 66 269 65 C 269 64 268 64 268 63 C 268 63 267 63 267 63 C 267 63 267 63 267 63 C 266 63 266 63 266 63 C 266 63 266 63 266 63 C 266 63 266 63 266 63 C 266 63 265 64 265 64 C 265 64 265 65 264 65 C 264 66 264 67 263 67 C 263 68 263 68 262 69 C 262 70 262 70 261 71 C 261 71 260 72 259 72 C 259 72 258 72 257 73 C 257 73 256 73 255 73 C 255 73 254 74 253 74 C 253 74 253 75 253 75 C 253 75 253 75 253 75 C 253 76 254 76 254 77 C 255 77 255 78 256 78 C 256 79 257 79 257 80 C 257 80 258 80 258 81 C 258 82 258 82 258 83 C 258 84 257 84 257 85 C 257 86 257 86 257 87 C 257 88 257 88 256 89 C 256 89 256 89 256 89 C 256 89 256 90 257 90 C 257 90 257 90 257 90 C 257 90 257 90 257 90 C 257 90 257 91 257 91 C 258 91 259 91 260 90 C 261 90 262 90 262 89 C 263 89 264 89 265 88 C 265 88 267 87 267 87 C 268 87 269 88 269 88 C 270 89 271 89 271 89 C 272 90 273 90 274 90 C 274 90 275 91 276 91 C 276 91 276 91 276 91 C 276 91 276 91 276 91 Z " > |
098 | < LinearGradient x = "267" y = "63" |
101 | < GradientEntry color = "#ffffcc" |
104 | < GradientEntry color = "#ffcc33" |
110 | < DropShadowFilter angle = "270" |
117 | < DropShadowFilter angle = "90" |
この FXG 画像を用いた、Flex/AIRのモバイル端末用カスタムボタンスキンクラスを、Flex/AIRのモバイル端末用デフォルトボタンコンポーネントクラスを拡張して、テスト用に実装してみたのが、下のコードです(今回の下のコードは、あくまでテスト用。実際の本番の実装では、アプリケーションDPI毎に画像を用意する必要がありますし、ほかにもいろいろ調整が必要で、とっても手数の多い、面倒な作業になります)。
net.chsmea.mioproject.skins.test.mobile.ButtonSkin.as
01 | package net.chsmea.mioproject.skins.test.mobile { |
03 | import net.chsmea.mioproject.skins.test.mobile320.assets.Button_down; |
04 | import net.chsmea.mioproject.skins.test.mobile320.assets.Button_up; |
06 | import spark.skins.mobile.ButtonSkin; |
17 | public class ButtonSkin extends spark.skins.mobile.ButtonSkin { |
22 | public function ButtonSkin() { |
26 | switch (applicationDPI) { |
30 | upBorderSkin = Button_up; |
31 | downBorderSkin = Button_down; |
34 | layoutCornerEllipseSize = 20 ; |
35 | layoutPaddingLeft = 48 ; |
36 | layoutPaddingRight = 48 ; |
37 | layoutPaddingTop = 50 ; |
38 | layoutPaddingBottom = 50 ; |
39 | layoutBorderSize = 50 ; |
40 | measuredDefaultWidth = 164 ; |
41 | measuredDefaultHeight = 106 ; |
52 | override protected function drawBackground(unscaledWidth : Number , unscaledHeight : Number ) : void { |
このとき、Flexライブラリビルドパスに、「(Adobe Flex SDK のインストール先)/frameworks/themes/Mobile.swc
」を通しておかないと、Flex/AIRのモバイル端末用のデフォルトスキンのクラスを拡張できないので、忘れずに!

そして、テスト用に用意した、このFlex/AIRのモバイル端末用カスタムボタンスキンクラスを利用する、Flex/AIRモバイルアプリケーションを次のように用意したとします。
- Main.mxml
- View.mxml
- style.css
(Project)/src/Main.mxml
1 | <? xml version = "1.0" encoding = "utf-8" ?> |
5 | firstView = "views.View" > |
7 | < fx:Style source = "./assets/style.css" /> |
9 | </ s:ViewNavigatorApplication > |
(Project)/src/views/View.mxml
01 | <? xml version = "1.0" encoding = "utf-8" ?> |
10 | < s:Button id = "buttonMiddle" |
12 | < s:Button id = "buttonShort" |
14 | < s:Button id = "buttonLong" |
15 | label = "長~~~いラベルをボタンに入れてみるテスト ほむ ほむほむ ほむほむほむほむ" /> |
(Project)/src/assets/style.css
06 | src : url ( "./assets/uzura.ttf" ); |
09 | advancedAntiAliasing: true; |
13 | src : url ( "./assets/uzura.ttf" ); |
14 | fontFamily: uzura_cff; |
16 | advancedAntiAliasing: true; |
25 | skinClass: ClassReference( "net.chsmea.mioproject.skins.test.mobile.ButtonSkin" ); |
26 | fontFamily: uzura_cff; |
よし、そんじゃ、このアプリケーションを、デスクトップ上のモバイル端末シミュレータで実行・・・と。

ダメです、うまく伸縮してくれません。
なぜか、FXG 画像の scale-9 grid が機能してくれないのです。
なぜ、機能してくれなかったのか?
先に答えを言ってしまうと、FXG の ベクター (Path) に、ビットマップフィルター (filters) を適用してしまっていたから でした。
・・・。
えっ。・・・あれれ!?
Flashって、scale-9 grid も、slice-9 grid も両方とも、ちゃんと機能してたはずでは?
そんな疑問だらけでしたが、 FXG version 2.0 の仕様書を読んだところ、slice-9 grid (ビットマップスライシング) 機能は FXG 2.0 の仕様として定まっていないので、表示オブジェクト内にビットマップが含まれていると、scale-9 grid も含めて、9スライスつまりはスライシング機能が適用されなくなる、とかいう制限があるとのこと。
そんなわけで、先ほどのボタンのコンパイル済みFXG画像のコードから、filtersを取っ払ったところ、下図のようにちゃんとうまく伸縮してくれるようになりました。

☆がいっぱいで、ぷちしあわせです。
はふーーー。
じゃぁ、影付けとかどうすんの!?
FXG 画像に、ドロップシャドウフィルター効果など(フィルター効果は残念ながらいまのところ全部ビットマップフィルター)をかけると、scale-9 grid が適用されなくなってしまうので、FXG 画像にたとえば影付けの効果を出したいときには、その影に当たる部分を、一つひとつパスを引いて、アルファ値を付けたブラックやグレーでフィルしていくしかなさそうです。他のフィルター効果の場合も、フィルター効果を追加した後のレンダリング結果(つまりは、見た目のこと)のように、ひたすらパスを引いたり、塗りつぶしの色や、アルファ値などをいじって、ベクターだけでそれっぽく作成していく必要があるようですね。
FXGでの9スライスにおける制限
FXG 2.0 画像の9スライスを行うとき、FXG 2.0 のそもそもの仕様や、Flash Player 側の仕様により、scale-9 grid, slice-9 grid の適用・レンダリングに関して、いくらかの制限があるのは、次のようなときのようです。
(出典元: FXG 2.0 Specification # Scale-9 / [ScaleGrid] Implementation Limitations - Flex SDK - Adobe Open Source *英語)
- Flash Player 側の仕様で、表示オブジェクト内の9スライスが設定された全ての継承について、scale-9 grid がちゃんと適用されないことがあるとのこと。具体的には、scale-9 を設定した子の表示オブジェクトを表示オブジェクトに吊るして、その親から子の表示リストを伸縮させた時なんかに、子の表示オブジェクトに対して scale-9 grid が適用されなくなるみたい。
- 歪み (
skew
) や 回転 (rotation
) を設定した表示オブジェクトにも、scale-9 grid の伸縮は正しく適用されないとのこと。これも、おそらくは Flash Player 側の仕様かと。
- Flash Player 側の仕様で、ネストされた scale-9 grid はサポートされていないとのこと。具体的には、scale-9 を設定した子の表示オブジェクトを、これまた scale-9 を設定した表示オブジェクトに吊るして、その親や子の表示オブジェクトを伸縮させた時なんかに、それらの表示オブジェクトに対して scale-9 grid が適用されなくなるみたい。
- ビットマップが含まれた表示オブジェクトに対する、ビットマップスライシング (slice-9 grid) は適用されないとのこと。そもそも、FXG 2.0 の仕様に、slice-9 grid を定義していない。
- 表示オブジェクトへの scale-9 grid の設定値 (
scaleGrid
) が、その視覚的コンポーネントの子の表示オブジェクトのバウンディングボックスの外側へ裁ち切られてしまうような値だった場合、scale-9 grid は適用されなくなるとのこと。
- scale-9 grid の設定値 (
scaleGrid
) に、不正な値を設定した際、当然ながら scale-9 grid は適用されないとのこと。
ここでいう不正な値とは、次の通り:
scaleGridLeft
及び scaleGridRight
の値が、横断してしまってはならない(つまり、画像の大きさ内で鏡面にすることなく設定しよう、ってこと)。
scaleGridTop
及び scaleGridBottom
の値が、横断してしまってはならない(つまり、画像の大きさ内で鏡面にすることなく設定しよう、ってこと)。
scaleGridLeft
の値が scaleGridRight
の値を超えてしまってはならない(つまり、左<右)。
scaleGridTop
の値が scaleGridBottom
の値を超えてしまってはならない(つまり、上<下)。
scaleGridLeft
, scaleGridRight
, scaleGridTop
及び scaleGridBottom
は、すべて画像の内側の値を取らなければならない(つまり、左・下・右・上のすべてに 1 px 以上の値を設定しよう、ってこと。0 px とか マイナスの値は設定しちゃダメ)。
.
ittun55
2012/08/2714:02
Flex4からscale-9 Gridが効かず、困っていました。とっても参考になります。ありがとうございました!!!