ついにUIScrollViewクラスの拡張っす。
元にするApple'sサンプルプロジェクト3_Tilingは、主に2つの機能のサンプルになってます。
前回も言ったようにGoogleのマップアプリが、この2つの機能を使ってる典型ですな。あれはUIScrollViewではなさそうだけどね。理屈は同じ。
何故に「画面上では一枚にみえるように複数の画像で構成する」必要があるかというとメモリが有限だからですわ。
マップの最大スケールで表示した場合、日本全土を表示するための画像なんてとてもメモリ上に置けないわけでして(iPhone 3GSが物理メモリで256MBらしい)、いくつかのブロックに分けて、現在の画面表示に必要な画像だけ保存場所から抜き出してメモリ上に置いて利用するってことになるんですな。
保存場所ってのはGoogleのマップアプリの場合インターネット上のサーバーであり、Apple'sサンプルだとResourcesグループに置かれたPNGファイル群って事になります。
ひとつのブロックを小さめにすると、ブロックを更新する時間が少なくなって反応がよくなるけど、細かすぎるとブロックの構成に時間を食いはじめたりして、ここらへんは用途によって調整が必要。
同じように「表示スケールによって使う画像を切り替える」も、上の理由の延長線上にあるんだけど...
それだけではなく、特定のスケールを単に大きくしたり小さくしたりするだけだと、下図のように使い物にならなくなる場合があるってのも必要になる理由のひとつでしょう。
文字まで小さくなられちゃ読めないわけっすよ
で、この機能を実現するためにApple'sサンプルではUIScrollViewを拡張してるわけですな。
このやり方しか無いってわけじゃなくて、例えばUIScrollViewに埋め込むUIView側を拡張して対応してもできるんじゃないかと思うけど、ま、一例として勉強していこうと思います。
必要な画像を用意して、スクロールに合わせて切り替える機能の実現には、UITableViewで使ってるUITableViewDataSourceプロトコルの手法を取り入れてるようです。UITableViewとUITableViewDataSourceはエリカ本でも取り上げられてるし、あっちこっちで紹介されてるので、ここではスルーします。わからない人は「UITableView UITableViewDataSource」あたりでググりましょう。
あの場合、指定された行のUITableViewCell*を設定して返してたわけですが、今回は指定されたブロックのUIView*を返すようです。
こんな感じ。
ここで送られるrowやcolumnは、全体画像の左上を(0、0)番目としたタイルの何行、何列目のタイルが必要かを意味してます。
で、UIView資源にも限りがあるわけで、バンバン作って返すわけにはいかないわけで、ここでもUITableViewの手法と同じく、まず、あまってるUIViewをTiledScrollViewに尋ねてるってことしてます。そいつがTiledScrollViewの
というメソッドでした。
下の図だと1行4列目(どちらも0から始まる)のタイルを要求されたtiledScrollViewがdequeueReusableTileで空いてるタイルある?と聞いたら1行0列目のタイルが使われてないから、これ使ってと返してきたので、内容部を新しく1行4列目用に変えてから返す。って動作になります。
以下、軽くTiledScrollViewのメソッドの説明
- (id)initWithFrame:(CGRect)frame
初期化です。
使っていないタイルビュー(タイル用UIView)保持用インスタンスreusableTilesの確保や、表示タイル領域変数の初期化。タップ検出、およびタイルビュー埋め込み用ビューtileContainerViewの埋め込みをおこなってます。
TiledScrollViewに直接タイルビューを埋め込まずにtileContainerViewでワンクッションおいてますな。タップ検出なんかは画面全体で考えるべきなんで、こういう配置にしてるんでしょう。
- (void)layoutSubviews
こいつが今回の目玉で、スクロールするというのはboundsのoriginを変更することなので、layoutSubviewsはスクロール時に必ず呼ばれることになるんでしょう。このさいにタイルの交換や調整をおこなうという仕組みみたいです。
という作業をやってます。
- (void)annotateTile:(UIView *)tile
タイル用UIViewの再利用具合を確認できるようにUIViewに作られた順に番号を埋め込んでます。
あくまで確認用で、実際にアプリケーションをリリースする時は必要ないんですが、スクロールを繰り返すと以下のように同じ場所を表示しても使われるタイル用UIViewが変わるってのが確認できます。
画面の外にスクロールして、また戻ってってのをやると、こんな感じで毎回使われるUIViewが変わる
今回、まずはズームをサポートしないバージョンを作ってみますた。iPhone OSの対象は3.0以降です。
ズームサポートするには、もうちょい工夫がいります。それは次回。
それと、Apple'sサンプルプロジェクトではタイル用UIViewとして、PNG画像埋め込んだUIImageViewを使ってますが、2万 x 2万ピクセルという大きさのスクロールエリアを指定したかったので、tileViewという自分が担当しているタイルの座標を表示するだけのUIView継承クラスを用意して使うことにしました。ま~タイルでのスクロールの仕組みに影響する部分ではないんで、よしとしてください。
2万 x 2万ピクセル分の領域を仮に256x256ピクセルの分割PNG画像で用意しようとしたら6241個いるんすよ。無理だから。
こうやると、どんな広大な領域でも9個の256x256ピクセルのUIViewで切り回せるわけですな。Googleのマップアプリはこのタイル用UIViewの内容部の再描画にいろいろ工夫をこらしてストレス無いようにしてるわけです。そこんとこが腕の見せ所。
ロールプレイングゲームもばっちり作れます。
次回、ズーミングのサポート!
------------
サンプルプロジェクト:sc8.zip
元にするApple'sサンプルプロジェクト3_Tilingは、主に2つの機能のサンプルになってます。
- 画面上では一枚にみえるように複数の画像で構成する。
- 表示スケールによって使う画像を切り替える。
前回も言ったようにGoogleのマップアプリが、この2つの機能を使ってる典型ですな。あれはUIScrollViewではなさそうだけどね。理屈は同じ。
何故に「画面上では一枚にみえるように複数の画像で構成する」必要があるかというとメモリが有限だからですわ。
マップの最大スケールで表示した場合、日本全土を表示するための画像なんてとてもメモリ上に置けないわけでして(iPhone 3GSが物理メモリで256MBらしい)、いくつかのブロックに分けて、現在の画面表示に必要な画像だけ保存場所から抜き出してメモリ上に置いて利用するってことになるんですな。
保存場所ってのはGoogleのマップアプリの場合インターネット上のサーバーであり、Apple'sサンプルだとResourcesグループに置かれたPNGファイル群って事になります。
ひとつのブロックを小さめにすると、ブロックを更新する時間が少なくなって反応がよくなるけど、細かすぎるとブロックの構成に時間を食いはじめたりして、ここらへんは用途によって調整が必要。
同じように「表示スケールによって使う画像を切り替える」も、上の理由の延長線上にあるんだけど...
それだけではなく、特定のスケールを単に大きくしたり小さくしたりするだけだと、下図のように使い物にならなくなる場合があるってのも必要になる理由のひとつでしょう。
文字まで小さくなられちゃ読めないわけっすよ
で、この機能を実現するためにApple'sサンプルではUIScrollViewを拡張してるわけですな。
このやり方しか無いってわけじゃなくて、例えばUIScrollViewに埋め込むUIView側を拡張して対応してもできるんじゃないかと思うけど、ま、一例として勉強していこうと思います。
必要な画像を用意して、スクロールに合わせて切り替える機能の実現には、UITableViewで使ってるUITableViewDataSourceプロトコルの手法を取り入れてるようです。UITableViewとUITableViewDataSourceはエリカ本でも取り上げられてるし、あっちこっちで紹介されてるので、ここではスルーします。わからない人は「UITableView UITableViewDataSource」あたりでググりましょう。
あの場合、指定された行のUITableViewCell*を設定して返してたわけですが、今回は指定されたブロックのUIView*を返すようです。
@protocol TiledScrollViewDataSource
- (UIView *)tiledScrollView:(TiledScrollView *)scrollView
tileForRow:(int)row column:(int)column resolution:(int)resolution;
@end
こんな感じ。
ここで送られるrowやcolumnは、全体画像の左上を(0、0)番目としたタイルの何行、何列目のタイルが必要かを意味してます。
で、UIView資源にも限りがあるわけで、バンバン作って返すわけにはいかないわけで、ここでもUITableViewの手法と同じく、まず、あまってるUIViewをTiledScrollViewに尋ねてるってことしてます。そいつがTiledScrollViewの
- (UIView *)dequeueReusableTile;
というメソッドでした。
下の図だと1行4列目(どちらも0から始まる)のタイルを要求されたtiledScrollViewがdequeueReusableTileで空いてるタイルある?と聞いたら1行0列目のタイルが使われてないから、これ使ってと返してきたので、内容部を新しく1行4列目用に変えてから返す。って動作になります。
以下、軽くTiledScrollViewのメソッドの説明
- (id)initWithFrame:(CGRect)frame
初期化です。
使っていないタイルビュー(タイル用UIView)保持用インスタンスreusableTilesの確保や、表示タイル領域変数の初期化。タップ検出、およびタイルビュー埋め込み用ビューtileContainerViewの埋め込みをおこなってます。
TiledScrollViewに直接タイルビューを埋め込まずにtileContainerViewでワンクッションおいてますな。タップ検出なんかは画面全体で考えるべきなんで、こういう配置にしてるんでしょう。
- (void)layoutSubviews
こいつが今回の目玉で、スクロールするというのはboundsのoriginを変更することなので、layoutSubviewsはスクロール時に必ず呼ばれることになるんでしょう。このさいにタイルの交換や調整をおこなうという仕組みみたいです。
- tileContainerViewに埋め込まれたタイル用UIViewのうち表示矩形外はすべてreusableTilesに回収。tileContainerViewからもとりはぶく。
- 表示矩形に必要なタイル領域を計算。
- 計算されたタイル領域で新しく必要になるタイル用UIViewをdataSourceに要求し、tileContainerViewに埋め込む。
という作業をやってます。
- (void)annotateTile:(UIView *)tile
タイル用UIViewの再利用具合を確認できるようにUIViewに作られた順に番号を埋め込んでます。
あくまで確認用で、実際にアプリケーションをリリースする時は必要ないんですが、スクロールを繰り返すと以下のように同じ場所を表示しても使われるタイル用UIViewが変わるってのが確認できます。
画面の外にスクロールして、また戻ってってのをやると、こんな感じで毎回使われるUIViewが変わる
今回、まずはズームをサポートしないバージョンを作ってみますた。iPhone OSの対象は3.0以降です。
ズームサポートするには、もうちょい工夫がいります。それは次回。
それと、Apple'sサンプルプロジェクトではタイル用UIViewとして、PNG画像埋め込んだUIImageViewを使ってますが、2万 x 2万ピクセルという大きさのスクロールエリアを指定したかったので、tileViewという自分が担当しているタイルの座標を表示するだけのUIView継承クラスを用意して使うことにしました。ま~タイルでのスクロールの仕組みに影響する部分ではないんで、よしとしてください。
2万 x 2万ピクセル分の領域を仮に256x256ピクセルの分割PNG画像で用意しようとしたら6241個いるんすよ。無理だから。
こうやると、どんな広大な領域でも9個の256x256ピクセルのUIViewで切り回せるわけですな。Googleのマップアプリはこのタイル用UIViewの内容部の再描画にいろいろ工夫をこらしてストレス無いようにしてるわけです。そこんとこが腕の見せ所。
ロールプレイングゲームもばっちり作れます。
次回、ズーミングのサポート!
------------
サンプルプロジェクト:sc8.zip