KLab若手エンジニアの これなぁに?

KLab株式会社の若手エンジニアによる技術ブログです。phpやRubyなどの汎用LLやJavaScript/actionScript等のクライアントサイドの内容、MySQL等のデータベース、その他フレームワークまで幅広く面白い情報を発信します。

2010年12月

Androidでかっこよくプレゼンしたい

はじめまして。fukaya-aと申します。
12/16から本日までインターンでKLabにお邪魔させていただいておりました。
恐れ多くも若手ブログに記事を書かせていただけるということで、ちょっと緊張しています。

さて、今回のインターンではかっこよくプレゼンするためのAndroidアプリを作りました。
私は普段研究の発表などでプレゼンをする機会がそれなりにあるのですが、
いろいろと不満点がありました。

・レーザーポインター、ストップウォッチなどを毎回準備しなければならないのが面倒
・操作のたびにPCの側にいなくてはいけない
・レーザーポインターがぶれまくる
調べてみると、こういう問題を解決するためのスマートフォン用リモコンアプリはそれなりに存在するようでしたが、
なかなか自分の欲しい機能をすべて揃えたものはありませんでした。
ということで、自分で作ることにしました。

<!--more-->
まず、自分が欲しいと思う機能は次の通りです。

・Bluetooth接続 : Wifi環境がなくても大丈夫なように
・スライド操作の方法が多彩 : ボタン、物理ボタン、加速度(ジェスチャー)
・クリック動作可能
・レーザーポインタ機能
・ストップウォッチ機能 : 指定時間で振動
ということで
bluetoothのRFCOMMのUUIDを探しまわったり、
解像度の違う端末の対応に苦労したりしましたが、
なんとか期間内に動くものを作ることが出来ました。

端末を振って次のスライドにということで、名前は安直にShakeNextとしました。
<div style="height:10px; clear:all;"></div>
<div align="center">

<img class="alignnone" title="icon" src="http://lh3.ggpht.com/_-cdXx9wc4dA/TRHHpilLARI/AAAAAAAAADw/wY07_zQLO3A/s144/next512.png" height="144" width="144">
</div>
<br />
<br />

アイコンはこんな感じです。
今思うともうちょっと作りこんでもよかった気もします…
<div style="height:10px; clear:all;"></div>
<div align="center">
<img class="alignnone" title="main" src="http://lh3.ggpht.com/_-cdXx9wc4dA/TRHHpvNniUI/AAAAAAAAAD0/Oo09PZKG3CQ/s144/main000.png" height="144" width="86">

</div>
<br />
<br />
メイン画面のキャプチャです。
画面上にあるのがストップウォッチでタップすることでスタート・ストップ、
ロングタップでリセット出来ます。また、指定時間(2つ設定可能)で振動・テキストの色が変わります。

ボタン類は見たとおりです。PC側のレシーバーソフトで動作を変更することもできます。
これらをタップすることで、スライドを操作します。
また画面では見えませんが、ボリュームボタンや端末をシェイクすることでもスライド送りができます。

画面下の領域は仮想レーザーポインターの操作に使います。
例えば画面右下を触ると、ポインターが左下に移動します。
またボリュームボタンと組み合わせてマウスのようにクリックすることもできます。

年内にはAndroidマーケットにリリースする予定ですので、
Android + Windowsな環境の方はぜひ試してみて感想をお聞かせください。
※快く公開の許可を下さり、ありがとうございました。

iPadで動く電子書籍アプリを作ってみる(3)

1 前回のフォロー

前回の記事でリリースしたコードに不具合を見つけました。Tipsでもあるので説明します。 初期画面から文書名をタップするとページのイメージが表示されますが、よく見るとナビゲーションバーの下辺にページの上辺がくっついて表示されています。本来は、ページの上辺はステータスバー(時計や電波状態が表示されている部分)の下辺から表示され、ナビゲーションバーと重なる部分はナビゲーションバーの下に透過して表示されるようになっていないといけません。 いろいろ調べたのですが、私がとった方法は、ナビゲーションバーのビュー階層の直下にスクロールビューを配置せず、間にUIViewを挟む方法です。



なぜこのようなコードが必要かを推測するに、どうやらNavigationControllerが初期画面を表示する際、初期画面がスクロール可能、すなわち、UIScrollViewを継承するビューの場合に、ビュー内部の表示内容がナビゲーションバーの領域に被さらないように、contentInsetをナビゲーションバーの高さだけ下に下げて表示させているようのですが、さらにpushViewControllerで次のViewを表示する際にも属性を引き継ぐかもしくは同じ調整を行っているようです。 これは、ちょっと余計なお世話な仕様のように思います。こういったことはフレームワークが暗黙的にやるより、フレームワークはデフォルトではcontentInsetを調整せずに表示して、UIScrollViewのドキュメントに、「ナビゲーションバーと併用するときは、.contentInsetプロパティで表示位置を調整してね」って書いてあった方がかえって混乱が少なくなる気がします。フレームワーク設計の常として、暗黙的にやってあげるべきことと、ユーザの判断でやってもらった方が良いことの線引きはなかなか難しいですね。

2 マウスイベントのハンドリング

さて、本題に入ります。前回でページイメージの表示を実現しましたが、まだ文書の1ページ目までしか表示出来無い状態でした。今回はタップイベントをハンドリングして、ページ送りを行えるようにします。 今回は、画面の左右両側1/3の領域をタップした場合に、ページを前後させ、中央部分をタップしたらナビゲーションバーの表示/非表示を切り替えるという仕様で考えます。



タップイベントのハンドリングにはiOS3.2、すなわちiPadリリースとともにサポートされたUIGestureRecognizerファミリのクラスを用います。 viewDidLoaddでUITapGestureRecognizerを作成して登録します。initWithTaget:action:の引数として、それぞれ、selfと、@selector(handleTapFrom:)を与えることで、タップ操作が起きたらPageViewController宛にhandleTapFrom:を送るよう指定します。

    1   UITapGestureRecognizer *recognizer;
    2   // UITapGestureRecognizerの作成と登録
    3   // recognizerが認識したイベントは自分宛をhandleTapFrom:送ってくるように設定
    4   recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapFrom:)];
    5   [self.view addGestureRecognizer:recognizer];
    6   [recognizer release];

ユーザが画面をタップすると送られるhandleTapFrom:を実装します。とりあえず今回は、左をタップすると次ページ、右をタップすると前ページを表示するように実装します。


    1 - (void)handleTapFrom:(UITapGestureRecognizer *)recognizer {
    2
    3   CGPoint location = [recognizer locationInView:nil]; // nilを指定するとスクリーン座標が取得できる
    4   LogPoint(location);
    5   UIWindow* window = [[UIApplication sharedApplication] keyWindow];
    6
    7   if( window.frame.size.width / 3 * 2 < location.x){ // 右
    8     Log("-->");
    9     if( document_.currentNombre > 1 ){
   10       document_.currentNombre--;
   11     }
   12     [self renderPage];
   13   }else if( location.x <= window.frame.size.width / 3 ){ // 左
   14     Log("<--");
   15     if( document_.currentNombre < [document_ pageCount] ){
   16       document_.currentNombre++;
   17     }
   18     [self renderPage];
   19   }
   20   else {// 真ん中
   21     Log(@"真ん中");
   22   }
   23
   24   [self renderPage];
   25
   26   [self.view setNeedsDisplay];
   27 }

見たまんまのコードですが、3行目、locationInView:でnilを渡すと、スクリーン座標系のタップ位置が取得できます。locationInView:はUIGestureRecognizerを設定したビューの階層関係にあるビューであればどのビューの座標系にも変換してくれます。余談ですが、Windowsのマッピングモードで苦労した記憶がある筆者にとってはとても便利なメソッドに思えます。 実際にタップを実行すると、なめらかにイメージが切り替わって表示されるのがわかると思います。これは、前回紹介したように、CALayerを用いてCGImageオブジェクトを設定すると、即座に画像が切り替わるのではなく、CoreAnimationの機能で自動的にアニメーションするようフレームワーク側が処理します。これは"Implict Animation"(暗黙的アニメーション)と定義されています。暗黙的とは逆に、明示的にアニメーションすることも出来ます。後の回で、Core Animationを用いてページめくりのバリエーションを試してみたいと思います。

3 ナビゲーションバーの表示・非表示

次に画面の真ん中をタップすると、ナビゲーションバーを非表示にすることで、ページ全体を読めるようにします。 ついでに、タイマーを用いて、文書を開いてから一定時間経過した場合もナビゲーションバーを非表示にするようにします。 まず、ナビゲーションバーを出したり消したりするメソッド、toggleNavbarです。 基本的にはナビゲーションバーの状態を参照して、現在の状態と逆の状態を設定するだけですが、表示させた場合にのみタイマーをセットして一定時間経過したら勝手に消えるようにします。


 01  -(void)toggleNavbar {
 02    if( self.navigationController.navigationBarHidden ){ // ナビゲーションバーが消えている
 03      [self.navigationController setNavigationBarHidden:NO animated:NO];// 表示する
 04      [self setNavbarTimer];	// 表示した場合は非表示タイマーを設定する
 05    }else {// ナビゲーションバーが出ている
 06      [self.navigationController setNavigationBarHidden:YES animated:NO];
 07      [self unsetNavbarTimer];
 08      // 非表示にした場合は消しっぱなし
 09    }
 10  }

-(void) setNavbarTimer {
  [self unsetNavbarTimer]; 
  navbarTimer_ = [[NSTimer timerWithTimeInterval:kEraseMenuInterval target:self selector:@selector(navbarTimerFired:)
				       userInfo:nil repeats:NO] retain];
    [[NSRunLoop currentRunLoop] addTimer:navbarTimer_ forMode:NSDefaultRunLoopMode];
} 
タイマーを生成して、retainして持っておきます。 タイマーが発火したら、navbarTimerFired:を送るように指定しておきます。

- (void)navbarTimerFired:(NSTimer*)timer {
  [self unsetNavbarTimer];
  Log(@"%s %@", __FUNCTION__, timer);
  [self toggleNavbar];

}
navbarTimerFired:はタイマーを無効化して、toggleNavbarを呼び出しているだけです。 タイマーの無効化メソッド、unsetNavbarTimerは下記のコードです。invalidateしてタイマーを無効化し、タイマーはこのコントローラがretainしていたのでreleaseすることでタイマーを解放します。

- (void) unsetNavbarTimer {
  if(navbarTimer_){
    [navbarTimer_ invalidate];
    [navbarTimer_ release];
    navbarTimer_ = nil;
  }
}
忘れがちですが、ビューそのものが非表示になるタイミングで送られるviewWillDisappear:メソッドの処理において、タイマーの解放のためにunsetNavbarTimerを呼び出すことと、反対に、ビューが表示されるタイミングで送られるviewWillAppear:中ではsetNavbarTimerを呼び出すようにします。 最後に、handleTapFrom:メソッドで画面中央部がタップされた場合のコードを修正し、

   20   else {// 真ん中
   21     [self toggleNavbar];
   22   }
として呼び出します。これで、ナビゲーションバーの表示/非表示をサポートできました。

4 まとめ

今回、基本的なタップ操作をサポートしたことでようやくビューアらしくなってきました。次回は画面の拡大・縮小について説明します。 今回までのコードは

bzr branch -r article-3-release lp:~klabrd/jisuireader/trunk article-3-release
で取得してください。

5 前回までの目次

iPadで動く電子書籍アプリを作ってみる(1) iPadで動く電子書籍アプリを作ってみる(2)
 KLab若手エンジニアブログのフッター