[Titanium勉強日記 2] 画面遷移してみる

HelloWorldができたので次は画面遷移をしたいですね。それで適当にググりながら下記の様なコードを書いてみたのですが上手く動きません。アニメーションされずにパッと画面が切り替わってしまいます。

// 実験1
var win = Ti.UI.createWindow({backgroundColor: 'blue'});
var btn = Ti.UI.createButton({title: 'push to open'});
btn.addEventListener('click', function() {
	var win2 = Ti.UI.createWindow({backgroundColor: 'red'});
	win2.open({animated: true});
});
win.add(btn);
win.open();

そこでwin2.openを呼び出す際のプロパティを下記のように変更すると、iPhoneの時のみアニメーションするようになりましたが、それでは駄目です。何の為にTitanium使うと思ってるんですか。Androidでも普通のアニメーションしながら画面遷移くらいしてくれと思いますよね。

// 実験2
var win = Ti.UI.createWindow({backgroundColor: 'blue'});
var btn = Ti.UI.createButton({title: 'push to open'});
btn.addEventListener('click', function() {
	var win2 = Ti.UI.createWindow({backgroundColor: 'red'});
	win2.open({transition: Titanium.UI.iPhone.AnimationStyle.CURL_DOWN});
});
win.add(btn);
win.open();

その後、色々試行錯誤して結局以下のように書き換えて上手くいきました。シングルウィンドウのアプリでもTabGroupを使えば良いようです。その代わりにtabBarHiddenを指定します。これでiPhoneの場合はUINavigationControllerにpushした時の挙動を示します。Androidでもデフォルトのアニメーションが動きますし、遷移先からバックボタンで元の画面に戻れます。

// 実験3
var tabGroup = Titanium.UI.createTabGroup();

var win = Titanium.UI.createWindow({  
    title:'Tab 1',
    backgroundColor:'blue',
	tabBarHidden: true,
});
var tab = Titanium.UI.createTab({  
    title: 'Tab 1',
    window: win
});

var btn = Ti.UI.createButton({title: 'push to open'});
btn.addEventListener('click', function() {
	console.log('click');
	var child = Ti.UI.createWindow({title:'child', backgroundColor: 'red'});
	tab.open(child, {animated:true});
});
win.add(btn);

tabGroup.addTab(tab);
tabGroup.open();

ただ問題は残っていて、Androidの方でtabBarHiddenが動作せず、タブが1つ表示されてしまって微妙です。なんとかならないのと調べてみると、公式フォーラムに解答がありました。

まず(app-project-dir)/platform/android/res/layout/というディレクトリを作成して、その中にtitanium_tabgroup.xmlというファイル名で下記を追加すればタブバーを消す事ができます。

<?xml version="1.0" encoding="utf-8"?>
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
 
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:padding="0dp">
 
        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:padding="0dp"
            android:layout_weight="1"/>
 
        <TabWidget
            android:id="@android:id/tabs"
            android:layout_width="fill_parent"
            android:layout_height="0dp"
            android:layout_weight="0"/>
 
    </LinearLayout>
	
</TabHost>

以上で、最初にやりたいなと思っていた挙動は実現できたのですが、まだ釈然としない事がありました。それはopenを呼ぶ際に指定できるオプションがドキュメントを見ても良く解らなかった事です。一番最初のコードではanimatedが動作しませんが、最後のコードではanimatedが動作します。でも理由が分からないのです。Titanium BBSで質問してみたところ、そういうものだという事なので、やはりある程度ソースコードを確認する必要があるようです。

プロジェクトルート直下のbuildディレクトリにiPhoneの場合はxcodeprojが生成されています。デバッグ実行も可能なので適当にソースコードを追ってみたところ、TiWindowProxy.mのopenが下記のようになっていて、openに渡したargsをそのままTiAnimation::animationFromArgの呼び出しに使っています。

-(void)open:(id)args
{
	if ([[[[TiApp app] controller] modalViewController] isKindOfClass:[TiErrorController class]]) { // we have an error dialog up
		return;
	}
	// opening a window more than once does nothing
	if (opened==YES)
	{
		return;
	}

	[self rememberSelf];
	
	//First, we need to get our arguments in order. Perhaps in Opening.

	if (opening==NO)
	{
		modalFlag = [self isModal:args];
		fullscreenFlag = [self isFullscreen:args];
		if (!modalFlag)
		{
			[self forgetProxy:openAnimation];
			RELEASE_TO_NIL(openAnimation);
			openAnimation = [[TiAnimation animationFromArg:args context:[self pageContext] create:NO] retain];
			[self rememberProxy:openAnimation];
		}
		opening = YES;
	}
    TiThreadPerformOnMainThread(^{
        [self openOnUIThread:args];
    }, YES);
}

そしてTiAnimation::animationFromArgの内容が以下です。animatedが指定されている場合は、同時にanimationStyleが指定されない限りアニメーションが行われないようになっています。最初のコードでアニメーションが動かない理由が分かりましたね。

+(TiAnimation*)animationFromArg:(id)args context:(id<TiEvaluator>)context create:(BOOL)yn
{
	id arg = nil;
	BOOL isArray = NO;
	
	if ([args isKindOfClass:[TiAnimation class]])
	{
		return (TiAnimation*)args;
	}
	else if ([args isKindOfClass:[NSArray class]])
	{
		isArray = YES;
		arg = [args objectAtIndex:0];
		if ([arg isKindOfClass:[TiAnimation class]])
		{
			return (TiAnimation*)arg;
		}
	}
	else 
	{
		arg = args;
	}

	if ([arg isKindOfClass:[NSDictionary class]])
	{
		NSDictionary *properties = arg;
		KrollCallback *cb = nil;
		
		if (isArray && [args count] > 1)
		{
			cb = [args objectAtIndex:1];
			ENSURE_TYPE(cb,KrollCallback);
		}
		
		// old school animated type properties
		if ([TiUtils boolValue:@"animated" properties:properties def:NO])
		{
			float duration = [TiUtils floatValue:@"animationDuration" properties:properties def:1000];
			UIViewAnimationTransition transition = [TiUtils intValue:@"animationStyle" properties:properties def:UIViewAnimationTransitionNone];
			TiAnimation *animation = [[[TiAnimation alloc] initWithDictionary:properties context:context callback:cb] autorelease];
			animation.duration = [NSNumber numberWithFloat:duration];
			animation.transition = [NSNumber numberWithInt:transition];
			return animation;
		}
		
		return [[[TiAnimation alloc] initWithDictionary:properties context:context callback:cb] autorelease];
	}
	
	if (yn)
	{
		return [[[TiAnimation alloc] _initWithPageContext:context] autorelease];
	}
	return nil;
}

animatedが指定されない場合は、更にargをTiAnimation::initWithDictionaryに引き渡しています。TiAnimationはtransitionプロパティを持っていて、このイニシャライザで渡ってきたプロパティをセットしています。これで2つ目のtransitionが動いた理由も分かりました。同様にTiUITabProxy.mのopenOnUIThreadを確認すると、animatedをUINavigationController::pushViewControllerに渡している事が分かります。

というわけで同じopenというメソッドでもクラスによって指定できるオプションは違うし、同じ名前のオプションを指定したとしても内部での使われ方が違います。つまり完全にTitaniumをブラックボックスと捉えると、想定される挙動が得られない事があると言う事です。

どこかでTitaniumは独自のAPIを揃えているのでCocoaやAndroid SDKの知識はいらないよ、TitaniumのAPI覚えるだけで良いよ、と言った記述を見たのですが、それは正しいとは言えない気がします。恐らくObjective-CやJavaのコードと行ったり来たりする事になりそうで、思ったよりは大変そうですね。それでもObjective-CやJava単体でコード書くのに比べると格段に楽だとは思いますけども。

そんなわけでアプリ書き始めようと思います。

Leave a Reply

Your email address will not be published. Required fields are marked *