2012年8月31日金曜日

UITextFieldのキャレットを操作(UITextRangeとUITextPosition)

まず、「キャレット」と「選択範囲」という言葉について。

  キャレ|ットは3文字目の位置にある

こういう文字入力時のカーソルのことを「キャレット」と呼びます。

  2文字目から6文字目が選択されている

複数の文字を選択している場合、キャレットは非表示となり「選択範囲」がハイライト表示されます。

iOSのテキストシステムの内部ではキャレットと選択範囲のハイライトとを、同一の仕組みで扱っているため、以下で「キャレット」と呼んだ場合、「選択範囲」も含むものだと考えてください。

UITextFieldのキャレットの位置を取得するためには、selectedTextRangeプロパティを参照します。このプロパティはUITextRangeクラスのオブジェクトです。

----

Objective-Cには、NSRangeという範囲を保持する構造体があります。NSStringを操作するときに使わされてお世話になってることだと思います。

なぜUITextFieldではUITextRangeをわざわざ使うのでしょうか?文字列の範囲を保持するために、構造体ではなくオブジェクトを使うのは一見無駄に思えます。

その理由は二つです。
  • HTMLのようなネストしたドキュメントにおいて、メタテキストと可視テキストの両方の追跡が求められるため。
  • iPhoneのテキストシステムの基となったWebKitフレームワークで、テキスト始点や終点をオブジェクトで表す必要があった。
後者は完全にAppleの都合ですが、前者については理はあります。例えばHTML文書の「強調」という文字を選択しているとき、可視テキストを見れば「"強調"の2文字選択している」という解釈になりますが、HTMLとしては「"<b>強調</b>"の9文字選択している」のです。

NSRangeのような単純な型では、このようなメタ情報までは保持できないのです。

----

UITextRangeは3つのプロパティを持ちます。

まずstartとendプロパティ、二つのUITextPositionがあります。これらは選択の始点(start)と、終点(end)を保持するものです。

始点と終点の位置が同じならば、選択ではなくキャレットの状態ということになります。これを調べるためのemptyプロパティが存在します。BOOL型なのでisEmptyで参照できます。

UITextRangeは不変オブジェクトであり、startやendの位置を操作できません。UITextPositionもまた、一切のメソッドを宣言しない(アクセサも当然ないから公開プロパティを持たない)クラスであり、特定の位置を示すUITextPositionを生成することもできません。

それはそのはずで、UITextPositionの実体は対象がプレーンテキストか、或いはHTML文書かによって指し示すものが変わるものであり、UITextPositionやUITextRangeは文書の意味構造に関わらず、共通の文書操作のインターフェースを提供するための抽象クラスだからです。

このことは、もし何らかの操作可能な文書を保持するクラスを作るときは、その文書の意味構造に応じて、UITextRangeとUITextPositionをオーバーライドする必要があることを意味します。

----

以上から、UITextFieldのキャレット(すなわちUITextRange)を操作するためには、UITextField自身のメソッドを利用する必要があることが分かります。

UITextRangeやUITextPositionの操作に必要なメソッド群は、UITextInputプロトコルで宣言されているものです。関連したものを以下に列挙します。

プロパティ
  • beginningOfDocument
    • ドキュメントの先頭のUITextPositionを保持
  • endOfDocument
    • ドキュメントの終端のUITextPositionを保持
  • selectedTextRange
    • 現在選択されているテキスト範囲をUITextRangeで保持
メソッド
  • offsetFromPosition:toPosition:
    • 指定した2つのUITextPositionの距離をNSIntegerで取得。
  • positionFromPosition:offset:
    • 指定したUITextPositionから第二引数だけ離れた新しいUITextPositionを取得。
  • positionWithRange:atCharacterOffset:
    • 選択範囲の始点からCharacterOffsetだけ離れたUITextPositionを取得?
  • textInRange:
    • UITextRangeから対応する文字列を取得
  • textRangeFromPosition:toPosition:
    • 指定した2つのUITextPositionを範囲とするUITextRangeを取得
これで全てではありません。その他については、UITextInputのプロトコルリファレンスを参照してください。

----

以下は「UITextFieldにキャレット移動メソッドを追加させてみた」という実例です。

/*
 キャレットをドキュメント始点方向に移動させます
 */
-(void)caretMoveToBackward
{
        UITextRange *currentRange = self.selectedTextRange;
        if([currentRange.start isEqual:self.beginningOfDocument]){
                return;
        }

        UITextPosition *newPosition = 
              [self positionFromPosition:currentRange.start offset:-1];

        UITextRange *newRange;
        if([currentRange isEmpty]){
                newRange = [self textRangeFromPosition:newPosition
                                            toPosition:newPosition];
        }else{
                newRange = [self textRangeFromPosition:newPosition
                                            toPosition:currentRange.end];
        }

        self.selectedTextRange = newRange;
}

見ての通り、死ぬほど面倒です。

まず既に始点ならばガード節で弾きます。UITextFieldに不正なUITextRangeを与えると、問答無用でアプリが落ちてしまうからです。

次に選択範囲の始点から-1の距離を指定した新しいUITextPositionを取得します。

そして現在のNSRangeが、キャレットか範囲選択かに応じて場合分けをします。

ただここで考えなければならないのは、範囲選択時に「キャレットを前へ動かす」というボタンに対して、ユーザーがどういう挙動を期待しているのか、ということです。考えられるものを列挙してみると、
  1. 選択範囲全体が動くべき
  2. 始点だけが動いて選択範囲が広がるべき
  3. 選択を解除してキャレットに戻した上で一文字移動するべき
  4. 選択を解除してキャレットに戻すだけで移動はするべきではない
Windows PCでカーソルキーを押したときの挙動は、意外にも4.なのです。

もっともiOSは範囲を選択しやすいマウスのあるWinPCとは違います。ここでは指で細かい範囲指定を要求されて、発狂する人を助けるための機能として作ったので、2.で実装しました。

----

「使いやすさ」という言葉に対しては様々な議論がありますが、個人的には「ユーザーが期待した動作を実現する」ことが重要だと考えています。

ユーザーの期待の実現、すなわちメンタルモデルに沿わせるための簡単な方法は、標準にあわせることです。

Appleがガイドラインを公開して、それに沿うよう強制するのも、アプリ毎に個々のUIの意味や挙動が変わり、悪い意味で期待を裏切り、思わぬ結果に驚かせるような、ユーザー体験を損ねる挙動を出来るだけ排除するという狙いがあるのでしょう。

なのでそれをする必然な理由が存在しないならば、独自の挙動は出来るだけ回避するべきだと思います。

----

しかしこのAPIはないわー感満載。せめてNSRangeからUITextRangeに変換するメソッドくらいあってもいいのに…。

----

余談。

テキストフィールドのbecomeFirstResponderをコールすると次の流れで処理されます。

  1. UIResponderのbecomeFirstResponderを呼ぶ。
  2. UITextFieldの_becomeFirstResponderが呼び出される。
  3. UIControlのUIControlEventEditingDidBeginイベントにバインドされたメソッドが呼び出される。
  4. UITextFieldのtextfieldDidBeginEditingが呼び出される。
UITextRangeを操作できるのは、textfieldDidBeginEditing以降です。

UIControlのUIControlEventEditingDidBeginイベントにキャレットを操作するメソッドを実行させることはできません。より厳密に言えば、変更がなかったことにされて、endOfDocumentの位置にキャレットが移動されています。

なぜこういう仕様なのかは謎ですが、システムで予約されたプライベートメソッド内でそういう処理をしているのでどうしようもないです。迂闊にいじろうものならリジェクト確実なので疑問を持ってはいけません。

もし「既に入力済みのフィールドを選択したら、選択済み状態にして欲しい」などの機能を実装するときは、デリゲートメソッド以降でキャレットを操作しましょう。

2012年8月29日水曜日

deallocでnil代入すべきではない理由について

XCodeが生成するコードを見ると、viewDidUnloadではnil代入によってプロパティ初期化しているのに、deallocではインスタンス変数を直接releaseしています。

なぜ二つのやり方を使い分けるのか、疑問に思ったことがないですか?

---

オブジェクトの解放は、すべてself.hoge = nilでやるべきだと推奨する人もいます。

なぜかというと、プロパティの実体であるインスタンス変数をreleaseしたあとの、self.hogeが指し示すアドレスは、nil(0番地)ではなくもともとデータが存在していたアドレスです。

そこにアクセスしようとすると、retain忘れや過剰releaseと同様に、忌まわしいEXC_BAD_ACCESSが発生します。

nil代入はオブジェクトを解放しつつ、プロパティのポインタを安全なnilで初期化する良いイディオムなのです。

---

しかしキー値監視が行われている可能性がある場合、話は変わります。

キー値監視とは、その名前の通り、他のオブジェクトのプロパティが変化したとき、コールバックを受け取る仕組みのことです。もしプロパティにキー値監視が行われている場合、nil代入によって通知が行われるかもしれません。

しかしdeallocフェーズではオブジェクトの状態は不安定になり、メソッドの結果は予測不可能になります。

運がよければ何事もなく正しい結果を返します。悪ければ必要なメンバが既に解放されているため、不正なアドレスからデータを読み込もうとしてエラーで落ちます。

ものすごく運が悪いと、メソッドの実行に必要なメンバが解放されているにも関わらず、処理自体は通って不正な値を返すといったことがありえます。後々になってそのことが発覚するような場合、デバッグは困難を極めます。

deallocフェーズでのプロパティ操作は、オブジェクトが不安定な状態でキー値監視通知を発生させる恐れがあるので、XCodeが生成するコードではそれを避けるようになっているのです。

---

キー値監視なんて使ったことないし関係ないじゃん?みたいに思うのですが、CocoaやInterfaceBuilderが裏側で使っているとか使っていないとかみたいです。(iOSでの依存度については、良く分かんないんですが…)

カテゴリでプロパティを実装する(関連参照による擬似インスタンス変数)

mファイルに@interface ClassName()と書くことを、「プライベートカテゴリ」とか「匿名カテゴリ」と呼びますが、多分正確な用語では「クラス拡張」と呼ばれるもので、カテゴリとはクラスの実装をカテゴライズして複数のファイルに分散する機能です。

この記事におけるカテゴリは、クラス拡張ではなく、本来の意味のカテゴリを指します。

カテゴリは実装を分散するだけでなく、既に存在するクラスに対して、継承することなく新たなメソッドを追加できる柔軟性を持っていますが、インスタンス変数を保持できない制約が存在します。

それが問題になることは通常では稀です。なぜなら、カテゴリがインスタンス変数を持てなくても、クラス本体の@privateを含めたインスタンス変数にアクセス可能だからです。

(もっとも、XCode4.4の今、privateメンバはクラス拡張でプロパティ宣言することが基本となり、@interfaceで@privateインスタンス変数を宣言することが減ったため、インスタンス変数にアクセスできるメリットはほぼないのですが…。当然、クラス拡張で宣言したプロパティの実体である、@implementationで宣言されるインスタンス変数は直接触ることはできません)

---

カテゴリでプロパティを宣言した場合、@sythesizeは使えません。synthesizeとはインスタンス変数の宣言とsetter/getterの実装を簡略化する糖衣構文に他ならないので、インスタンス変数を宣言できないカテゴリでは、@dynamicを宣言してsetter/getterを自分で書く必要があるのです。

クラス本体のインスタンス変数を使える場合は問題になりません。例えばクラス本体が弧度法(ラジアン)で角度のプロパティを持っているとき、それを度数法に変換して取得できるdynamicプロパティをカテゴリに記述したい、などのケースです。

---

もしカテゴリで実装したい振る舞いにデータの保持が必要で、しかし何らかの理由でクラス本体のヘッダや実装ファイルにインスタンス変数を追加することができない(例えばUKitのクラスにプロパティを持たせたいなど)場合、関連参照というテクニックが使えます。

関連参照については、Appleの公式ドキュメント、「Objective-C プログラミング言語」でもちゃんと解説されているのですが、初学の時点ではおそらく理解できないので、そのままになっているのかもしれません。

「OSX v10.6以降使用可能」とありますが、iOS5現在ちゃんと動作することを確認しています。

---

関連参照とは何かというと、オブジェクトと一意のIDをキーにして、値をストアする仕組みのことです。実質的には、既存クラスに擬似インスタンス変数を追加します。

その値はインスタンスが生存している間有効であることが保証され、オブジェクトを保持(retain)することができ、その場合、インスタンスが解放されると同時に自動的に解放(release)されます。

使い方は簡単です。

まずランタイム関数を使用するので、<objc/runtime.h>をインポートします。

値の設定に使うのはobjc_setAssociatedObjectです。

objc_setAssociatedObject (
   保持したいオブジェクト(selfでOK),
   キー(staticなNSStringでOK),
   値,
   関連ポリシー(プロパティの属性に合わせて選択)
);

保持したいオブジェクトは、プロパティでは自分自身なのでselfでOKです。インスタンスメソッドでのselfは、自身のオブジェクトを指します。(クラスメソッドでのselfはメタなクラスオブジェクトなので注意です)

一意なキーはNSStringをstaticで宣言します(NSStringでなくても、voidポインタならばなんでもOKです)。UITableViewCellのreuseIdentifierと同じような感じです。

値に保持したいデータを指定します。もしintや構造体などのプリミティブ型を保持したい場合は、NSNumberやNSValueでラップする必要があります。

関連ポリシーはプロパティのretain, nonatomicなどの属性に合わせて指定します。例えば通常のオブジェクトであればOBJC_ASSOCIATION_RETAIN_NONATOMIC、文字列であればOBJC_ASSOCIATION_COPY_NONATOMICです。

この関数を実行すると値が保持されます。

値を取得するには、objc_getAssociatedObject関数を使います。引数として、値を保持しているオブジェクトと、キーを指定します。

---

エアコードですがこんな感じでプロパティを実装できます。オブジェクトの保持も、破棄もランタイムが面倒を見てくれるので、すごく簡単です。

hogeClass+AdditionProperty.h

@property (retain, nonatomic) id fuga;

---

hogeClass+AdditionProperty.m

static NSString *theKey = @"fugaKey";

@dynamic fuga;

-(void)setFuga:(id)fuga
{
    objc_setAssociatedObject (
      self, theKey, fuga, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(id)fuga
{
    return  objc_getAssociatedObject(self, theKey);
}

もし値を明示的に破棄したい場合にはnilをsetします。

ただしインスタンスと共に破棄されるので、その必要性に迫られることは稀でしょう。普通のインスタンス変数ですらMRCでは明示的にリリースする必要があることを考えると、擬似インスタンス変数ですが、部分的には本家を超えていますね…。

カテゴリのヘッダファイルもちゃんとimportしないと、警告が出たり、ドット構文でのアクセスができなかったりするので注意して下さい。

2012年8月24日金曜日

InterfaceBuilderを用いて再利用可能なコンポーネントを作る

「こういう風にやればできます」系の画像付き説明は多いんだけど、冗長なやり方してる場所が多い気がする。そもそもObjective-Cの基礎的な言語知識がある程度ないと、やり方を知れても、理解することはできないかもな感じですが。

xibとnibの違い

InterfaceBuilderで生成したレイアウトファイルがxib。この中身はxmlです。

以前書いたけど、xibとは単にグラフィカルレイアウトが記述されているファイルではなくて、実行時にデシリアライズされるオブジェクト群が記述されています。

コンパイル時にオブジェクトがシリアライズされたnibに変換されるのです。

nibのデシリアライズのやり方

nibを使ってViewだけを取り出したいという場合、無名ViewControllerを使うやり方と、Bundleから直接取り出すやり方があります。

1.無名ViewControllerを使う方法

ViewControllerのinitWithNibname:bundle:メソッドを用いると、UIViewControllerクラスがFile's Ownerに指定されているnibを生成することができます。

File's OwnerがUIViewControllerでないnibをこの方法で生成しようとすると、当たり前だけど失敗してエラーで落ちます。

この方法で生成した無名ViewControllerは、viewをマネジメントするためのカスタムコードが記述されていないので何もしません。releaseを忘れると、viewリソースがリークしますので、さっさとreleaseするのが基本です。

何らかのマネジメント(例えば画面回転時にviewの表示をオリエンテーションに対応させたいなど)が必要ならば、ViewControllerを記述して親ViewControllerをコンテナ化させる必要があります。

2.Bundleを使う方法

NSBundleクラスのmainBundleでアプリケーションのメインバンドルを取得できます。

initWithNibname:bundle:でも使うバンドルとは、実行可能ファイルや画像などの各種リソースをパッケージングする仕組みです。メインバンドルとは、アプリケーションの実行ファイルや設定ファイルが格納されたバンドルのことです。

無名ViewControllerなど使わなくても、Bundleから直接nibを生成することができます。そのためのメソッドがloadNibNamed:owner:options:です。

この方法ではownerに指定した任意のオブジェクトをFile's Ownerに指定できるので、ViewControllerがFile's Ownerではないnibも呼び出せます。

返り値はnibのトップレベルオブジェクトが保持されたNSArrayなので、lastObjectを使うとルートビューが取得できます。

3.nibからオブジェクトを呼び出すときの注意

nibを使った場合、使用される初期化メソッドがinitWithCoder:になります。これはオブジェクト永続化のために定められた、NSCodingプロトコルで宣言されているデシリアライズに使うメソッドです。もし追加の初期化処理が必要な場合はここに書きます。

nibから生成されたすべてのオブジェクトは、生成処理が行われたにawakeFromNibがコールされます。時々「InterfaceBuilderを使うときには、awakeFromNibで初期化を行う」という記述を見ますが、これはオブジェクト自身の初期化に使うメソッドではありません

オブジェクトのデシリアライズ順序は保証されませんので、nibからロードされた特定のオブジェクト間を関連付けるような追加の初期化作業が必要な場合に、使うメソッドです。

例えば複数種類の外観を持つnibを、ユーザーが動的に選択するようなケースに使用するメソッドです。

File's Ownerで「動的」に

「動的」はObjective-Cの鍵です。

「静的」はコンパイル時点ですべてが決定されていることを意味します。

無名ViewControllerを使うコードをやたら見ますが、必要なデリゲート処理やアウトレットの保持が、xib内部のオブジェクトで完結しているケースなのでしょう。設計の時点で「静的」に閉じているケースではそれでいいのかもしれません。

「動的」は実行時に決定されることを意味します。

File's Ownerを実行時に関連付けることで、実行時にメソッド実装を切り替えたり、あるいは呼び出すnibを実行時に選択することで、初心者向けの簡単操作バージョンと上級者向けのボタン大量バージョンを、同一のViewControllerで提供できるのです。


例)エレガントにカスタムキーボードを呼び出す

UITextFieldやUITextViewは、カスタムクラスを作り、inputViewをオーバーライドすると、カスタムキーボードを使用することができます。

カスタムキーボードのレイアウトをInterfaceBuilderでやる場合、その処理を誰にデリゲーションすればいいのでしょうか?わざわざViewControllerを用意する必要はありません。

UITextFieldなどのカスタムクラスをFile's Ownerに指定して、カスタムキーボードに必要なメソッドを実装、UIのイベントと紐付けます。

あとはinputViewで、メインバンドルからloadNibNamed:owner:options:を呼び出し、オーナーを自分自身に関連付ければいいのです。

後日、ランドスケープでもカスタムキーボードが使えるようにしたいと仕様変更が入りました。オートサイジングでは見栄えが悪いのでレイアウトを変えたいです。

どうすればいいのか分かりますよね?ランドスケープ用にxibを定義して、画面の傾きに応じてどちらのnibを呼び出すのか動的に変更するようにすればいいのです。

2012年8月22日水曜日

組み込みコンテナビューコントローラをカスタムコンテナビューコントローラに組み込むときの落とし穴

iOS3まではrootViewControllerの概念がなかったので、windowに対して直接addSubviewしていた。このときwindowのboundsから、ステータスバーの長さ20ポイント分だけoriginとheightを操作する必要があった。

iOS4以降はrootViewControllerに指定したViewControllerのviewは、ステータスバーの長さを省いたwindowの大きさに、自動的に調整されるようになった。

---

本来、最も画面の外側(すなわちコンテナの役割を持つ)ViewControllerのViewは、インターフェース要素を加味して、自身の位置を調整する必要があったのだ。

しかも厄介なのは、子viewは親viewのoriginからの相対位置であるローカル座標系で計算するのに対して、最外周ではUKit論理座標系で計算しなければならないということにある。UKit論理座標とは、(画面の向きに関わらず)スクリーン左上が原点(0,0)となる座標系のことである。

例えばビューを平行移動したいとき、ローカル座標系でframeのwidthプロパティを操作すると、デバイスの向きに関わらず左右移動になるのに対して、UKit論理座標系のframeを持つwindowのルートビューのwidthを操作すると、ポートレートでは左右移動になるが、ランドスケープでは上下移動になってしまう。

逆に言えば、ローカル座標系のおかげで画面の向きによってwidthとheightの意味が逆になるといった煩わしい問題から解放されているのである。

(現在はやるべきではないがwindowに対してaddSubviewするときに、frameではなくboundsを使ったのも、frameはUKit論理座標系なのに対して、boundsはwindowのローカル座標系になるので、向きを考慮する必要がなくなるためである)

---

この問題が組み込みビューコンテナビューコントローラとどう繋がるのかと言うと。

NavigationControllerやTabBarControllerは最外周のコンテナとして振舞うことを前提に作られているので、UKit論理座標系として自身のインターフェース要素分のマージンを考慮したframe値を設定しようとするのである。

そのため、組み込みビューコンテナを自作コンテナの子にしてしまうと、ローカル座標系に対してUKit座標系を想定した操作を行うために、以下のような現象が発生する。

  • rootViewControllerがステータスバーのマージンを設定しているにも関わらず、追加で不要なマージンを設定しようとする。
  • 実際の画面の向きと異なる方向にマージンを設定しようとする。
  • widthとheightの意味を取り違えて、ランドスケープモードにも関わらず、縦長のsizeを設定しようとする。

もっとも、実際にはrootViewControllerの特性によって、rootViewControllerのviewプロパティは、windowを満たすようにリサイズされてしまうせいなのか、顕在しにくいですが。

---

簡単に問題に遭遇するならば、自作コンテナViewControllerを作成し、組み込みコンテナビューコントローラを対象にtransitionFromViewController:toViewController:duration:options:animations:completion:で、トランジションではなく、カスタムUIViewアニメーションを実行すると、rootViewControllerの補完が働く前にどのようなframe値が設定されるのか一目瞭然となります。

またその対策として最も簡単と思われる方法は、transitionFromViewController以下略メソッドを封印して、組み込みコンテナビューコントローラのviewを直接addSubviewした上で、正しいframe値を与えることです。正しいframe値の取得は、rootViewControllerのviewのself.view.boundsを使えば問題ないはずです。

---

コンテナviewControllerは自身のviewの位置を調整する責務があり、どこかでviewの位置を調整する処理を行っています。考えられる場所はviewWill(Did)Appearや、iOS5以降ではviewWillLayoutSubviewsあたりです。

上の解決法は、

  • 直接addSubviewを行った場合、viewWill(Did)Appearが呼ばれない。
  • addSubviewによってviewWill(Did)Appearによる調整が行われた後で、frame値を変更しているため、view調整を上書きしている。
のいずれかの現象が起きているのだと当たりをつけて、実証コード(NavigationControlelrのプロトコルでコールバックメソッドをフックしてプロパティを参照)を書いてみたところ、viewWillAppearの時点ではUKit座標系の間違った情報を持っていて、viewDidAppearで修正されていました。

コールバックメソッドが呼ばれていることから1の可能性はなく(iOS5以降はほぼこれらのメソッドはコールされるようです。バグで複数回コールされることも)、Didの時点でframe値の修正が行われていることから、2の可能性も否定されました。

どういうことなんだろう。もうちょっと調査してみるかもしれず。

2012年8月20日月曜日

画面回転時にビューを画面いっぱいに表示させたい

UIViewControllerのクラスリファレンスによれば、
 If a view controller is owned by a window object, it acts as the window’s root view controller. The view controller’s root view is added as a subview of the window and resized to fill the window. If the view controller is owned by another view controller, then the parent view controller determines when and how the child view controller’s contents are displayed.

もしビューコントローラーがwindowオブジェクトによって保持される場合、それはルートビューコントローラとして動作する。windowのサブビューにaddSubviewされたビューコントローラーのルートビューは、windowを満たすようにリサイズされる。

もしビューコントローラーが他のビューコントローラーに保持される場合、親のビューコントローラーはいつどうやって子のビューコントローラーのコンテンツが表示されるかを決定する。

とあります。

----

例えばxibをルートビューコントローラーとなるよう呼び出した場合、ポートレートモードでもランドスケープモードでも、オートサイジングが機能して全画面を満たすように表示されます。

ナビゲーションコントローラーやタブバーコントローラーの子とした場合も、使える領域をフルに使えるよう、自動的にビューのサイズが調整されます。

こういう場合は何も考えなくていいです。

----

問題はカスタムコンテナUIViewControllerを使った場合です。

カスタムコンテナUIViewControllerの子コンテンツビューコントローラーとして、xibを呼び出した場合、そのままでは画面をいっぱいに使ってくれるのは、xibで指定した画面モードだけです。

ポートレートモードで画面を作った場合、ランドスケープモードでは残念なことになります。

xibはあくまで設定した画面モードの見た目だけを保持しているだけなのです。windowを満たそうとするルートビューコントローラーの特性や、ナビゲーションやタブバーが内部でリサイズしてくれるために、「xibで作ったレイアウトは当然画面を回転させれば自動でリサイズされる」と考えてしまいますが、それは間違いです。

----

なぜナビゲーションコントローラーやタブバーコントローラーは、自動的に画面サイズ調整を行なってくれるのでしょう?

それはユーザーがナビゲーションバーやタブバーなど、インターフェース要素のために必要なマージンを計算する手間を省くためでしょう。コンテンツの表示に利用可能な領域を算出する機能を持つならば、その領域にあわせてリサイズする機能も併せ持つことは合理的です。

翻ってカスタムコンテナUIViewControllerについて考えると、カスタムコンテナが他にインターフェース要素やビューを持っていて、どう子ビューをリサイズして配置するべきなのかシステムに知る術はないので、プログラマ側で配置するコードを書く必要があるわけです。

---

androidだとWRAP_CONTENTやMATCH_PARENTみたいに自動で計算してくれるオプションがあるので、iOSは凄く不便だと思う場所ですね…。

androidはリサイズのためにビュー階層を複数回探索しているので、その分速度を犠牲にしてもいます。iOSでは親が子のビューのサイズを算出するような仕組みが存在しないので、テーブルビューやポップオーバーでは専用のメソッドを用いてサイズを伝えなければならず、この辺りの一貫性のなさもあまり好きにはなれません。

----

カスタムコンテナUIViewControllerが、適切に子ビューコントローラーに回転状態を伝播させていれば、willRotateToInterfaceOrientation:duration:メソッドおよび、didRotateFromInterfaceOrientation:メソッドが呼び出されます。

ここで各々の画面の向きに応じて、子ビューがどう配置されるのかを計算して、frameプロパティを設定すればよいのです。

或いはルートビューコントローラーとなる、コンテナUIViewControllerのビューに、オートサイジング設定を施した空のUIViewを保持させて、子ビューコントローラーはそのUIViewにaddSubviewすることでどうにかなるような気もします。未確認。

----

まとめ

  • ルートビューコントローラーは常にウインドウを満たすようにリサイズする特性を持つ。
  • ナビゲーションコントローラー・タブバーコントローラーは、ナビゲーションバーなどインターフェース要素を除いた使用可能な領域を算出し、それを満たすようにリサイズしてくれる。
  • 自身でカスタムUIViewControllerをコンテナ化する場合は、子ビューコントローラーのビューがどう配置されなければならないのか記述する必要がある。画面転回時に自動的にリサイズされないので注意。

2012年8月18日土曜日

コンテナUIViewController

iOS4まではコンテナUIViewControllerは、用意されたNavigtionやTab、Splitを利用するものだったが、iOS5ではユーザーの手でカスタマイズされたコンテナUIViewControllerを作成できるようになった。

利用するパターンは、例えば次のような場合のときである。

  • NavigationやTabでは表現できない階層構造を表現したいとき
  • MasterDetailでは作成できない3ペインのアプリを作りたいとき
  • xibで生成した複数種類のコンポーネントを動的に切り替えたいとき
iOS4以前でも一画面内に複数のカスタムコンテンツUIViewControllerのviewを混在させること自体はできたが、UIViewControllerを階層化できるようになったことで、トランジションエフェクト付きでビューの遷移を行えたり、画面回転イベントなどを子のUIViewControllerに通知できるなど、利便性が大きく改善されている。

---

使い方は、UIViewControllerのクラスリファレンスより、「Implementing a Container View Controller」を読むのが手っ取り早い。

日本語であれば、UIViewControllerのコンテナ機能が、スライド資料だけどサンプルコードも付いているので、具体的な使い方が分かると思う。

英語資料でもよければ、Writing high-quality view controller containersがもっとも情報量が多い。

---

UIViewControllerをコンテナUIViewControllerにするために、特別に何かを行う必要はない。

iOS5以降では、最初からすべてのUIViewControllerはコンテンツとしても、コンテナとしても、あるいはその両方としても振舞うことができる。

---

ビューコントローラーを子要素にするためには、addChildViewControllerを使用する。

あくまでビューコントローラー間に階層が生まれるだけなので、実際に画面にビューを表示するためには、別途addSubviewでビュー間に階層を作る必要がある。

これにより、親のビューコントローラーのNSArrayプロパティ、ChildViewControllersに子ビューコントローラーが格納され、子ビューコントローラーのUIViewControllerプロパティ、parentViewControllerに親のビューコントローラーが格納される。

※parentViewControllerは、iOS4まではモーダル表示元のUIViewControllerが格納されていたプロパティなので混乱に注意。

逆に子要素のビューコントローラーを取り除くには、子のビューコントローラからremoveFromParentViewControllerを呼ぶ。

---

子ビューコントローラ間で表示を切り替えるために、専用のトランジションメソッドが用意されている。

transitionFromViewController:toViewController:duration:options:animations:completion:

fromとtoに子ビューコントローラを指定する。

  • fromのビューコントローラが保持するビューにremoveFromSuperviewが飛ぶ。
  • 変わりにtoのビューコントローラが保持するビューがaddSubviewされる。
    • fromを保持していたビューに対して、addSubviewが行われる。
  • addChildViewController、removeFromParentViewControllerは呼ばれない。
    • fromが必要なくなった場合は、completionブロックでremoveFromParentViewControllerを呼んで解放する。
  • 書く必要のないaddSubviewを書くと、警告が出るので注意。
また、durationでアニメーション時間を、optionsでいくつかのトランジションエフェクトを選択できる。

組み込みのトランジションエフェクトが気に入らなければ、animationsにカスタムアニメーションを記述できる。

記述方法はblock構文によるUIViewアニメーションと同じ。self.viewのプロパティを操作するのではなく、ここではfromとtoのviewのプロパティを操作する。

---

基本的な使い方としては、

  1. [self.childViewControllers lastObject]で直前のビューコントローラの参照を取得する
  2. [self addChildViewController:newViewController]で新しいビューコントローラを子に加える
  3. それぞれをFrom、Toとして遷移アニメーションを実行
  4. completionブロックでFromのビューコントローラを解放

といった感じ。

例えば複数ペインの画面構成など、常時複数のビューコントローラーを子として保持する場合には、lastObjectは使えないので、isKindOfClassでクラス判定を行うか、プロパティとして保持しておくのがいいと思う。

---

注意すべき点

コンテナUIViewControllerを実装する場合、
  • addChildViewController:の直後にdidMoveToParentVieController:
  • removeFromParentViewController:の直前にwillMoveToParentViewController:
を、それぞれ呼び出さなければならない(must)。
  • addChildViewController:の直前にwillMoveToParentViewController:
  • removeFromParentViewController:の直後にdidMoveToParentViewController:
は、デフォルトでは自動的に呼ばれるので、何もしなくて良い。

ただしautomaticallyForwardAppearanceAndRotationMethodsToChildViewControllersをオーバーライドして、NOを返すと上記のメソッドも自動的には呼ばれなくなるので、自分の手で明示的に呼び出す必要がある。

画面転回によるイベントを子ビューコントローラーに自動転送したくない状況が存在する場合を除いては、デフォルト設定のままで問題ないと思う。たぶん。

2012年8月15日水曜日

xibファイルとは

InterfaceBuilderで作成するxibファイルとは、単純にレイアウトを記述するものではなくて、「オブジェクトの配置」と「イベントハンドルのデリゲート」を記述するものである。

オブジェクトの配置とは、UI部品など可視オブジェクトの表示座標だけではなく、例えばテーブルビューと、そこに表示するデータソースとなるオブジェクトとを関連付けることも指す。

そしてObjective-Cの柔軟なデリゲーション機能によって、外部コントローラオブジェクトに、UI部品のイベントハンドルを委譲する。ボタンは純粋にボタンとしての機能だけを持ち、実際にユーザーがボタンを押したことで何が起きるかは、xibがロードされた時に、すなわちボタンアクションがコントローラオブジェクトのデリゲートメソッドに接続されたときに決定される。

----

この仕組みはOSX上で動いていたころを考えるととても美しいと思うんだけど、iOS上ではあまり良い仕組みじゃない気がする。

OSXは複数ウインドウを展開しやすいOSだから、一つのxibを一つのコントローラが管理するプログラムは自然なんだけど、iOSは一つのウインドウしか存在しないから、複数の画面部品(xib)を扱いたいと考えたとき、一つの画面にいくつものコントローラが混在し結合して一つの画面を構成することになり、それらをプログラム上で生存管理しなければならなくなる。

----

…と、ここまで書いて閃いたんだけど。

画面を構成するビューコントローラを一つだけ生成し、複数のxibのFile's Ownerにそのビューコントローラを指定してデリゲーションメソッドを実装、最初のコンポーネントのみawakeFromNibで生成し、その後はloadNibNamedで呼び出してOwnerを関連付けさせれば、不要なビューコントローラーの生成を回避できる気がする。

----

[XCODE] Nibファイルを複数使って一つの画面を作成する

同じことを考えている人がいた。

2012年8月14日火曜日

iOSでの画面遷移メモ

ViewController(前提知識)

iOSではMVCアーキテクチャでいうViewとControllerを一体化させた、その名もViewControllerが、ユーザーインターフェースのキーになっている。

ビューを管理するViewController(コンテンツViewController)

Viewは自身をサブビューにする(Compositeパターン)ことで階層化されており、ViewControllerはその階層化されたViewの、ルートを保持することで、階層化されたView全体を管理する。

具体的に言えば、viewdidload、viewdidunloadなどのコールバックメソッドに応じて、ビューの表示・解放を行うことで、iOSのタイトなリソースをフル活用して、リッチなインターフェースを実現させている。

UIViewControllerを継承して自作するカスタムViewControllerと、表形式でデータを表示することに特化したTableViewControllerがここに入る…のだが、iOS5で事情が変わっている(後述)。

複数のViewControllerを管理するViewController(コンテナViewController)

コンテナViewControllerは、複数のViewControllerを保持して、それらを切り替える。

ViewControllerを階層的に表示するNavigationControllerは、親子となるViewControllerの表示の切り替えを管理し、その表示履歴をスタックで保持することで、複数のレベルに分かれた画面構成を提供する。

TabBarControllerは、「階層構造を持つNavigationController」とは対極的に、独立・並列した複数のViewControllerの切り替えを、タブの操作によって提供する。

PageViewControllerは、NavigationControllerとTabBarControllerやその他画面遷移を理念どおりに使用すると、電子書籍アプリすらまともに作るのも大変という悲しい現実を前に、組み込みで追加されたページ切り替え専用のViewControllerである。

iPadで追加されたいくつかのViewController

SplitViewControllerは画面を二つに分割して、マスタと詳細を表示することでiPadの広い画面を有効活用する…という名目のビューだが、自動的に縦横画面に対応してくれるのはいいものの、制約がひどく使い勝手はよろしくない。

PopoverControllerは他のコンテンツViewControllerの表示を、ポップアップで行うことができる。基本的には、iPadではアクションシートやモーダルによる全画面トランジションは使い勝手が悪いので、その代替として利用するもの。

iOSでできる基本の画面遷移

・NavigationControllerによる階層的な画面遷移

上部にナビゲーションバーが表示され、上位階層に戻るためのボタンが提供される。何も考えなくとも階層的なアプリケーションを作れる。

・TabBarControllerによる並列的な画面遷移

タブバーが表示される。モノトーンの素材を用意するだけでオサレなタブを表示できる反面、タブの外観のカスタマイズは面倒かつ、ダーティな手法が必要。

・モーダルビューによる遷移

「現在の表示に対して割り込みを行い、ユーザーの応答を受け取る」ためにモーダルモードとして別のViewControllerの内容を表示する。本来は応答待ちのための、一時的な割り込みの遷移として存在しているが、それを守らなければもっとも単純に画面遷移を行える。

※あと内臓辞書で単語の意味を調べて表示するUIReferenceLibraryViewControllerとかもある。

iOS5で追加されたセグエの概念

storyboardという時点で、初心者を除いてどうでもいい感じのセグエは、ソースとディスティネーション二つのコンテナViewControllerを関連付けて、それぞれのViewControllerの生成と解放、そして画面遷移のアニメーションを行う。

pushセグエ、modalセグエ、reration ship、カスタムセグエがあり、pushはNavigationController、modalはモーダルビュー、reration ship(正しくはセグエではない)はTabControllerの遷移に名前をつけたもの。

ViewControllerの存在を隠蔽して画面遷移を表に出しているが、「階層ナビゲーション」「タブによる並列表示」「モーダルによる応答処理」の3つが基本であることは変わらない。

カスタムセグエは、UIStoryboaredSegueのサブクラスをカスタムすることで、独自の画面遷移を作れる。サンプルコードを見る限り、実際の画面遷移自体は、モーダルビューの遷移を使っているので、カスタムセグエのコードを書けるレベルならモーダルビューで切り替えるだけだと思う…。

iOS5で追加された自作コンテナViewController

iOSの画面遷移は「階層ナビゲーション」「タブによる並列表示」「モーダルによる応答処理」の3つに関して言えば、少ない労力で高品質なインターフェースを構築できる。

しかし「画面の一部だけを切り替えたい」となるとSplitViewControllerの面倒な制約は邪魔になることが多く、「階層でも独立でもないナビゲーションをしたい」となると、モーダルやaddSubviewの切り替えをそれらしく見せるしかなかった。

が、iOS5ではUIViewControllerが、UIViewController自身を子を持てる(コンテナとして振舞える)階層構造に変化した。

addChildViewControllerで子UIViewControllerを登録すると、viewDidLoadなどのコールバックメソッドが子に伝播するようになる。またchildViewController同士の画面をトランジション付きで切り替えることなどができる。

ページ遷移を行うPageViewControllerは、カスタムコンテナUIViewControllerの実装例に過ぎないと思う。

まとめ
  • iOSの画面遷移の基本は「階層ナビゲーション」「タブによる並列表示」「モーダルによる応答処理」の三つである。
  • ストーリーボードはかっこよく言い換えて煙に巻いているが、基本は変わらない。
  • ただそれとは別にカスタムコンテナUIViewControllerの作成が可能になり、画面遷移の自由度は大幅に増した。
  • iOS5で進化した自由な画面遷移の例として、PageViewControllerがある。

2012年8月10日金曜日

カラー状態リストリソースと、状態Drawableは組み合わせられない

デフォルトのボタンを避けたい時、画像を用意できればいいんだけど、イラストレーターやらPhotoshopを使いこなせる人がいないって時は、さくっとShape Drawableで作るのが常套手段。

「CSS3で実装されたボタン」系のサイトをカラーカタログにするといい感じのボタンが簡単に作れます。

押しても反応がないのはボタンとしては致命的欠陥なので、押されている、有効無効、フォーカスが当たっている、など必要な状態に応じて何パターンか作る必要があります。

しかしボタンの形状はどれも同じなので、色の指定だけが違う大量のxmlを作ることになります。コピペをするとDRY神の教えに反するようでとてもバツが悪い気分です。

----

状態に合わせて異なる色を示すリソースがあれば良い様に思います。

まさにそういう目的で作られているのが、カラー状態リストリソース(Color State List)です。

ボタン画像を作ったら、直接Buttonのbackgroundに指定するのではなくて、stylesリソースを経由すると思うのですが(していないのであれば、ぜひしましょう)、ボタンが無効になっているときは画像だけでなく文字も暗くなっていた方がいいですし、ハイライト時に色が変わるとより映えます。

stylesリソースで、生成したボタン画像を背景に設定する際は、一緒にカラー状態リストで文字色を指定すると、そうした状態による色の違いを簡単に表現できます。

これでボタンの色も変えられるならば、ボタンの形状を示すShapeDrawableのxml1種類と、StateDrawableのxml、そしてカラー状態リストのxmlを必要数用意すればよくなります。

----

しかし、残念ながらその組み合わせはできません。

カラー状態リストとはStateColorListインスタンスを作るための設計書に過ぎないのです。そのStateColorListは実行時に状態に応じて色を返すだけのオブジェクトであり、色として指定できる色リテラルそのものではないのです。

同様に、StateDrawableもStateListDrawableのインスタンスを生成するための設計書に過ぎませんから、xmlからボタンをinflateするその瞬間は、「状態が存在しない状態」なので、機械的にカラー状態リストの先頭の色が選ばれます。

内部構造も見てみると、StateListDrawableのパーサは、個々のDrawableを生成するためにDrawableのパーサを利用しています。Drawableのパーサに引き渡された時点で、「状態」の情報は失われているわけですね。

2012年8月7日火曜日

handleleakがうざったい。

静的解析ツールであるandroid lintは、大量の警告がぐわーっと出てきて大変うざい(特にバージョンアップで仕様変わった後とか…)んですけど、コーディングのミスなどを指摘してくれて、ものによってはクイックフィックスで自動的に解決してくれたり、Explain Issueを選ぶとなぜこの警告を出したのか、どうすれば解決できるのか教えてくれます。…英語ですけど。

iOSが静的解析でメモリリークを撲滅して、ガベージコレクタなしでのメモリ自動管理(なぜか古参開発者は自分で管理するのを好んで使わないARC)を実現しているので、androidおっくれってるー!感はなきにしもあらずですけど、まあ便利です。

---

androidにLintが本格的に組み込まれるようになってから見るようになったこの警告。

This Handler class should be static or leaks might occur

「ハンドラクラスはstaticにせよ、さもないとリークが起きるであろう」…Explain Issueを読むと、「ハンドラはstaticなinnerクラスで、かつouterクラスを弱参照で持つように修正しろ」とあります。

具体的なコードはStackOverflowに載っています。

This Handler class should be static or leaks might occur: IncomingHandler

このLint警告は気にし過ぎなくてもいい気がするんですけどねえ。

以下なんで出るかの解説です。

---

内部クラスには四種類あります。非static内部クラスと、static内部クラス、そして匿名内部クラスです。

これ意外と頭に入らないんですよね。匿名内部クラスはリスナー書いてれば自然と覚えられるとして、デザインパターンが頭に入っていけば、static内部クラスは外部クラスのインスタンスがなくても作れる(=外部クラスのインスタンスのビルダーとして使われる)、非staticは外部クラスのメンバへのアクセス手段を持つ(=アダプタやイテレータとして使われる)として自然に覚えられると思います。

ローカルクラス?覚えなくていいです。

---

非staticな内部クラスは、Effective Javaでは非推奨だったり、一部のコーディングスタイルだと使うな!とか書かれていたりします。(もちろんIteratorなど正当な使用方法の場合は別でしょうけど)

その理由としてJavaの仮想マシンの仕様があります。Java言語仕様上内部クラスと呼ばれているものは、仮想マシン上ではただのクラスとして認識しているのです。

staticを宣言した内部クラスは、外部クラスのインスタンスがなくとも独立して生成できます。なので位置づけとしては特殊なスコープの、普通のクラスです。

一方、非staticの内部クラスは必ず外部クラスのインスタンス(=エンクロージングインスタンス)と紐付けられます。エンクロージングインスタンスへの暗黙の参照(OuterClass.thisってやつです)を持つばかりか、エンクロージングインスタンスのprivateフィールドへも自由にアクセスできます。

匿名クラスはfinalなローカル変数にアクセスできること、名前がないためコンストラクタで生成することができない(例えば空のコンストラクタが絶対必要なフラグメントを匿名クラスで生成すると、再生成できないから画面を傾けるとアプリが死ぬ)という2点を除いて、非static内部クラスと同じです。

――仮想マシンからはただのクラスとしか認識されていならば、外部クラスと内部クラスは別のクラスなのに、どうやってprivateフィールドにアクセスしているのか。

その答えは単純で、コンパイラが外部クラスのprivateフィールドへアクセスするメソッドを、非static内部クラスのために自動的に生成しているのです。

暗黙の参照の存在を理解していないと、リソースリークに繋がりかねませんし、バイトコードに変換されたときに不本意にカプセル化が破られている問題があったり、まあいろいろ嫌われているわけです。

---

Handlerはandroidのキモの一つですね。LayoutInflatorとHandlerが説明できるようになれば中級者、なイメージです。自分的には。

Handlerに関する説明は世の中いくらでも分かりやすいのが転がっているんで、知らなかったら探して読んで欲しいんですけど、アプリケーションのメインループにタスクを渡すためのオブジェクトです。メインループのタスクキューに詰まれたHandlerは、タスクが実行するまで保持され続けます。

----

以上より次の事実が導けます。

  • 非static内部クラス・匿名内部クラスはエンクロージングインスタンスの暗黙の参照を持つ
  • Looperに渡されたHandlerはタスクが消化されるまで保持され続ける

さて、1時間後にタスクが起動するようなHandlerを生成した場合どうなるでしょうか?

そのころにはとっくにアプリを終了させていると思うのですが、Looperに詰まれたHandlerはそんなことを知りませんから、エンクロージングインスタンスへの暗黙の強参照を持ち続け、ガベージコレクタの回収を妨害してしまうのです。

Handlerを内部クラスとして定義したいときは、別スレッドからメインスレッド(UIスレッド)を操作したいというケースなので、エンクロージングインスタンスはメインスレッドを持つアクティビティなのが一般的です。

下手なHandlerの使い方をすると、アクティビティがメモリリークし続けるのです。

----

Android Lintが提案する解決法が理解できたと思います。

staticインナークラスにすることで、外部クラス(おそらくはアクティビティ)への暗黙の参照を持たないようにしつつ、弱参照で保持するように改修する。

そうすれば、エンクロージングインスタンスが不要になった(アクティビティが終了した)時点で、きちんとガベージコレクタの回収対象としてくれるので、Handlerによるリークは起こりえなくなるのです。

しかし、例えば画面の再描画タスクのような、エンクロージングインスタンスと内部クラスとの生存時間がほぼ等しくなるような状況で、いちいち気にして余計なコードを書く必要性はないと思うのですけど…。

2012年8月5日日曜日

androidの標準アニメーションで円を描く(2)

androidの標準アニメーションで円を描くの続きです。

---

レギュレーションを、自作Interpolatorの許可、コードでの合成許可にすると、円運動は非常に簡単に実現できます。

https://github.com/glayash/Madoka/blob/master/src/glay/ash/madoka/MainActivity.java

与えられる0.0f~1.0fの値を、0~2πに変換して、x軸とy軸をそれぞれcosとsinで算出するカスタムInterpolatorを用いて、アニメーションを合成するだけでいけます。

「カスタムInterpolatorはフレームワークが与える入力値を操作・キャッシュできる」のがポイントです。ストップボタンを設置して、それ以降は入力値を捨ててキャッシュ値を使うようにすれば、アニメーション中断が実現できます。

https://github.com/glayash/Madoka/blob/stopAnimation/src/glay/ash/madoka/MainActivity.java

停止した時点の中断値を取得して、そこをアニメーションの始点とすることで、再開も実現できそうな感じですが、大変そうなのでそこまではやりませんでした。

---

頑張れば使える子だと思う標準アニメーションなんですけど、実用方面ではやっぱり難しいところがあります。

移動中・移動後のViewの座標を取得できない(カスタムInterpolatorでフレームワークからの入力値を横取りして算出する、という小細工に頼る必要があります)、動いているのは見た目だけで、判定を動かすアニメーションは3.0以降でしか使えない、などなど。

---

あと落とし穴の一つとして、ビューの表示順序を操作できません。

どのビューが前面に描画されるかは、フレームワークがビュー階層をトラバースする順番によって決定付けられていて、ユーザーが関与することができないのです。

例えば、リストビューの要素はそれぞれ優先順位が違うので、要素同士が交差するスライドアニメーションは、意図した動きにならないと思います。

androidの標準アニメーションで円を描く

結論から言うと惜しくも単独のアニメーションリソースではダメでした…!

現場で使える[逆引き+実践] Androidプログラミングテクニック」という本で、円運動させるのに独自をViewを使っていて、「標準アニメーションは、指定の位置へ一定の時間をかけて直線的に移動することはできます」とDISられてたので、円運動くらいは標準アニメーションにもできるんじゃないの?と試してみたのです。

androidやiOS、jQueryなど手軽にアニメーションを扱えるAPI全般に言えることですが、あれらはすべてプロパティ(座標やカラーコードなど)を数学的に補完しているだけなので、望む動きをどうやって数学的に表現するかというのを考えればいいのです。

---

レギュレーションとして、API Lv1から存在する標準アニメーション(ビューアニメーション)の、最初から存在する機能のみで作ります。

複数のアニメーション要素を重ね合わせられるsetで、x軸とy軸に異なる補完関数を設定したtranslateを重ねれば、円運動くらいは簡単に作れるはずです。

しかしここで想定外の出来事が。translatetranslateを4つ以上重ねると、アニメーションが正常に機能しない…。計算が複雑になりすぎると失敗するんでしょうね…。

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:shareInterpolator="false" >

    <translate
        android:duration="1000"
        android:fromXDelta="-300%"
        android:interpolator=
       "@android:anim/accelerate_decelerate_interpolator"
        android:repeatCount="1"
        android:repeatMode="reverse"
        android:toXDelta="300%" />
    <translate
        android:duration="500"
        android:fromYDelta="0%"
        android:interpolator=
        "@android:anim/decelerate_interpolator"
        android:repeatCount="1"
        android:repeatMode="reverse"
        android:toYDelta="300%" />
    <translate
        android:duration="500"
        android:fromYDelta="0%"
        android:interpolator=
        "@android:anim/decelerate_interpolator"
        android:startOffset="1000"
        android:toYDelta="-300%" />

</set>

上記アニメーションを以下のコードで呼び出して、widthとheightが同じ長さの適当なViewを動かしてみてください。
//適当な正方形のviewを画面中央に配置
final View view = (View)findViewById(R.id.hogeview);

//適当なボタン        
Button button = (Button)findViewById(R.id.button1);
button.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
     Animation animation = AnimationUtils.loadAnimation(MainActivity.this, R.anim.guruguru);
     textView.startAnimation(animation);
  }
});

残念ながら完全な円ではなくて、270度回転になってしまいましたが、単独の標準アニメーションリソースでも、これだけのものは作れます。リスナを使ってアニメーション同士をチェインさせれば完全な円も作れました。

一応解説すると、最初のtranslateがX軸の動きです。AccelerateDecelerateInterpolatorにより、最初と最後に減速するので、ちょうど円運動のX軸の運動になります。

二つ目のtranslateはY軸の180度分の動きです。DecelerateInterpolatorは最後に減速するので、これをreverseで2回アニメーションさせると、半円のY軸の運動になります。

三つ目のtranslateは二つ目のアニメーションの、移動方向を反転させたものです。startOffsetによって開始時間を遅延させることで、入れ替わりで機能するようにします。ここではrepeatは使えません。startOffsetの待機も繰り返してしまうためです。

これで四つ目を加えて正常に動けば、単独のアニメーションリソースで円運動できたんですけどねー。

---

X軸とY軸をバラせるので、理論的にはどんな動きでもできると思います。

特にコード上でアニメーションを生成するならば、自作の補完関数を使える(自作InterpolatorをXML上で呼び出す手段もきっとあるような気がしますが)ので、複雑奇怪な動きもいろいろできるはず。

また「タップした位置をアニメーションの基点とする」「移動先が動的に変化する」ようなアニメーションは、タップ位置を変数として持たないといけないので、コード上でのみ実現できます。

実際にコード上アニメーション生成と自作補完関数を使ってみると、円を描くのももっと簡単にできます。androidの標準アニメーションで円を描く(2)で実際試してます。

---

標準アニメーションは残念な部分も多いのですけども。

おそらくスマートフォンにとって気持ちいいUIの重要性は高いということをGoogleも考えていて、API Lv1の時点でアニメーションフレームワークを練って、端末の多様性や速度など諸々のトレードオフを熟慮しつつこういう仕様になってるんだろうなという感があります。

プロパティアニメーション一本化はまだまだ無理でしょうし、標準アニメーションを上手く使いこなすことが、リッチなアプリケーションを作るうえで重要な課題かなーと思います。

「デザイナーにも簡単に作れるjQuery」によってHTMLでもリッチな体験が可能になった今、アプリはそこを主戦場にしても厳しいのかもしれませんけどね。でもだとしたらアプリでできる差別化ってなんだろー…。

2012年8月4日土曜日

GitHub for Windows

GUIクライアントなんてどうせ使いにくいだろうし、メトロ風のUIだけ見てアンインストールしようと思って入れたんだけど、Power ShellとGit bashも付いてきたのでそのまま常用しています。

さっさと環境構築すればいい話なんですけど、面倒臭がってegitを使ってストレス貯めていたので、Windowsに一発でCUI環境作れるだけでも入れた甲斐あったかも。

GUIクライアントも意外と悪くなかったんですけど、まあそもそもGitHub使ってないので…。

いずれソースコード公開とかやりたいんですけど、小物を作って飽きるの繰り返しで、公開に耐えうるものを作れていないし、そもそも今のリポジトリは全部コミッターが本名なのでずいぶん先の話になりそう。

---

非公開リポジトリが欲しかったので、Assemblaに登録してみました。

GitHub for Windowsで、GitHub以外のサーバーとSSH接続できるのかなと思ったんですけど、Shellで特に問題なくできました。

SSH鍵はインストールした時点で勝手に生成、ユーザーフォルダの.ssh以下に保存され、特に何もしなくてもSSHの設定は終わっています。公開鍵であるgithub_rsa.pubをAssemblaに登録すれば、Git Shellが秘密鍵を使ってSSH通信してくれます。

---

sshの自動設定はPower Shellを使っているときだけなのかな?Git bashは別に設定が要りそうな感じです。デフォルトで日本語も使えて楽なので、Power Shellでいいやって感じですけど。

EclipseにSupportPackageのjavadocを設定する

SupportPackageはデフォルトではjavadocは配置されません。

javadocが読めないこと自体はまだ致命的ではないのですけど、この状態だと仮引数がarg0、arg1…という意味のない羅列になってしまうので、javadocを認識させておいた方がなにかと便利です。

方法

前提として、ADT r20以上である必要があります。r17~r19くらいの間は、ライブラリにjavadocをアタッチできない不都合があったみたいなので、バージョンアップしましょう。


やり方はここで知りました。こちらはMacですが、Winでも手順は同じです。


プロジェクトのlibsディレクトリに、android-support-v4.jar.properties(jarの拡張子も含める点に注意)というファイルを作って、srcにソースディレクトリを絶対、もしくは相対パスで指定します。

  • sdkを保存したパスのextras/android/support/v4/srcがソースディレクトリになります。
  • ディレクトリの区切りは/です。ソースにディレクトリを指定する場合、最後の/は必須です。
  • docの指定は不要です。
    • javadocコマンドで生成して付与するやり方もあるみたいですが、過去バージョンでのお話なのか、現バージョンでは自分は出来ませんでした。
  • 文字コードはUTF-8を指定しました。

これでEclipseを再起動すると、Android Dependenciesのandroid-support-v4.jarにjavadocを保持していることを示すマークが付きます。

ついでに

srcをアタッチすると、javadocだけでなく、当然ソースも見ることができます。デバッガなどでAPI内部に入ったとき「ソースが添付されていません」と出るあれがなくなります。不具合の原因の特定などに非常に便利です。

当然、androidのAPIライブラリであるandroid.jarへのソース添付も可能です。しかもICS前後からはとても簡単になりました。

SDKマネージャーでソースを落とせるので、android.jarを右クリックして、「プロパティ」の「javaソースの添付」より外部ディレクトリから「<SDKのディレクトリ>/sources/android-XX」を指定するだけです。

2012年8月3日金曜日

NotificationCompat.Builderのドキュメント

NotificationCompat.Builder addAction missing?

JerryBeansで追加される通知スタイルも早速サポートしているみたいに書いてあるけれど、未実装みたいで…。(8月1日くらい時点)

ライブラリに収録漏れしたけどドキュメントだけアップデートされるってのも変な話だけど、不都合が見つかって取り下げられたのかな。

日本語でNotificationCompat.Builderでググっても40件しか出てこない…。

---

サービスの使い方を紹介する記事で、未だにonStart(@Deprecated)使ってるコードがあってぐんにょりしたけど、それもしょうがないくらいにandroidの断絶は深刻だなー。

それを解決するはずのSupportPackageが、日本語と英語によって断絶されているせいで、いまいち日本語圏で互換性に関する情報が見つからないような…。探し方が悪いってのもあるんだろうけど。

まあこれくらいならIDEが検出してくれるんだけど、onStartCommand使うと、大昔の端末では認識しないんだっけ…。

一応onStartも書いておき、onStartCommandに@Overrideアノテーションを書かない、というのが神経質に互換性に気を使った場合の、正答だった気がする。

ほんと断絶は深刻だー。