読者です 読者をやめる 読者になる 読者になる

TitaniumMobile勉強記

Web系エンジニア向けのキャリアアドバイザーやってましたが現在はフリーランスで開発含めて色々やってます。技術ネタとしてはRuby/RailsとJavaScript関連(Node.js、Titanium)あたり

Android版のGoogleMapsアプリのようなUIをTitaniumで実現する方法

Titanium Mobile

先月に、Nexus5にMNPしてから、iPhoneは基本的に利用しなくなり、Nexus5で基本的には過ごしてますが、Androidな流儀になれてきたのと端末自体の性能が良いこともあり、全くストレス感じることなく日常使いできてます。

日常的に、Androidを使ってて、

「あーそういえば最低限の機能しか作ってなかったCraftBeerFanのAndroid版、折角なのでテコ入れしよ」

と思ってGoogle Maps アプリを参考にしたUIを実現させたくなり、ここ最近ずっとそれに取り組んでいたのですが意外と難しかったので実装方法についてまとめてみました

やりたいこと

MapViewを使って地図が表示されておりお店の場所にピンが配置されてる状態で、ピンをタッチした時に、そのお店に関する情報が表示されることがやりたいこととします。

絵にするとこういう感じです。

f:id:h5y1m141:20140315074940p:plain

検討した実装方法

さて、上記やりたいことに対して、どうやって実装するのか試行錯誤していて、覚えてる範囲で以下の3つをアプローチを考えてみました。

  • Clickイベント発火のタイミングでMapViewとViewの高さを変更する
  • MapViewとViewをほぼ同時にアニメーションさせる
  • アニメーション後のコールバック関数をうまく利用する

以下それぞれについて、CoffeeScriptでのサンプルコードを抜粋しながら解説していきます

Clickイベント発火のタイミングでMapViewとViewの高さを変更する

これは一番単純ですが、

shopInfoView.addEventListener 'click',(e) ->
  mapView.height      = "50%"
  shopInfoView.height = "50%"

という感じです。もちろん意図したレイアウトにはなるのですが、一瞬で高さが変更されるため、ユーザの使い勝手に少し違和感を与えそうなのでこれは却下しました。

MapViewとViewをほぼ同時にアニメーションさせる

一種でレイアウトが変更されるのは違和感があるので、Clickイベント発火のタイミングでそれぞれアニメーションさせればOKかなと思って以下の様なものを考えました

shopInfoView.addEventListener 'click',(e) ->
  t1 = Titanium.UI.create2DMatrix()
  animation = Titanium.UI.createAnimation()
  animation.transform = t1
  animation.duration = 400
  animation.height = "50%"
  shopInfoView.animate(animation,() ->
    Ti.API.info "shopInfoView animation done"
  )
  t2 = Titanium.UI.create2DMatrix()
  animation1 = Titanium.UI.createAnimation()
  animation1.transform = t2
  animation1.duration = 500
  animation1.height = "50%"
  
  mapView.animate(animation1,() ->
    Ti.API.info "mapView animation done"
  )

mapViewと shopInfoViewのduretiaonを変えてる意図ですが、shopInfoViewの高さ調整をした結果、それに適応する形でmapViewも縮まるという視覚効果を与えたいなぁと思ってこのようにしてみました。

このようなコードは、iPhoneの方は全く問題ないが、Androidの場合だと、アニメーション処理後にViewに配置してるTi.UI.Labelのtopの値が変わってしまう不具合がありました。

アニメーション完了後のコールバック関数内で、ズレてしまうTi.UI.Labelのtopの位置を微調整すればOKではあるのですが、AndroidデバイスがPortraitモードとLandscapeモードとでそれぞれのズレ方に差があるためその処理を考慮するのが大変なのでこれも一旦諦めました。

アニメーション後のコールバック関数をうまく利用する

最終的にはこのアプローチに落ち着きました。アニメーション後のコールバック関数をどう利用するのか以下図を使いながら解説します

詳細情報がスライドするUIイメージ

  1. Viewには詳細情報を表示したいためその領域を確保するためにまずViewの高さを伸ばす
  2. Viewのアニメーション開始
  3. Viewのアニメーション完了時に呼ばれるコールバック関数内でMapViewの高さを縮める

というアプローチで以下の様なコードでshopInfoViewの高さ調整をしてそれに適応する形でmapViewも縮まるという視覚効果も得られて、意図したようなことができました。

shopInfoView.addEventListener 'click',(e) ->
  shopInfoView.height = 600  # (1)
  t1 = Titanium.UI.create2DMatrix()
  animation = Titanium.UI.createAnimation()
  animationSpeed = 500
  animation.transform = t1
  animation.duration = animationSpeed
  animation.top = 0
  
  mapView.animate(animation,() ->   # (2)
    mapView.height = 400   # (3)
    Ti.API.info "mapView animation done"
  )

実装終えた後の素朴な疑問

Android向けのアプリを開発する時に、iPhoneの場合と比べると結構悩ましい問題があります。

それは、iPhoneの場合に、ハードウェアとしての種類が少ないためスクリーンサイズを意識したUIはそこまで細かく意識しなくてもOKですが、Androidの場合には端末別に物理的なサイズ、解像度が異なります。

Androidの場合にいくつかの単位があるよ腕、そのあたりの説明がこちらのものが詳しいのでいくつか引用しておきます

Androidで扱う各種単位   Androidには"長さ"を扱う単位として「px:ピクセル」以外に「dp」「sp」「in」「mm」「pt」などがあるが、ここでは「px」と「dp」について言及する。

1.「px:ピクセル」は、画面上に点1つを表示する単位である。いま、貴方が見ているこのモニターの画面は、無数の点の集合体であり、この「点の個数」は「モニターの大きさ」や「グラフィックカードの性能」の違いによって変ってくる。  仮に、モニターの解像度が1680×1050であれば、画面に横1680個、縦1050個の点を出力できるわけだ。 〜中略〜 この時「px:ピクセル」単位を使用して、ウイジェットの余白や大きさを調節し、アプリケーションが完成したと仮定しよう。開発環境では、思った通りのインターフェイスを出力できるが、解像度の違う他のAndroidフォンでそのアプリケーションを見た場合、ウイジェットの位置や間隔が不自然に見える、という問題だ。

2.「dp(dip)=Density-independent Pixel: 密度非依存ピクセル」は、上記の問題を解決した表示方法における「単位」だ。コード上に記述する時は「dp」とか「dip」を使う。「dp(dip) 密度非依存ピクセル」では、LCDのピクセル密度(density)が160の時の画面では、1dpの画像と1px(ピクセル)の画像は同じ大きさになる。 しかし、LCDのピクセル密度(density)が240の画面になると、1dpの画像より、1px(ピクセル)の画像は小さくなる。1px(ピクセル)を1.5倍すれば同じ大きさになる。

それぞれのUI要素の指定をする際に、dpという単位で指定するのが割と一般的かと思い、実際これまでもその流儀にならっていたのですが

  • Viewの上に配置したTi.UI.Label要素が複数ある
  • dpで単位指定

という状態で、該当のViewをanimationさせた場合に、アニメーション完了後に、Ti.UI.Labelがズレる現象がありました。

この部分は自分の端末だけなのかどうかが正直わからないのと、自分のコードの書き方でどこかおかしいところがあるのかもしれないのですが、正直細かく検証しきれていませんが、素朴な疑問として、Android向けのアプリで、UI要素をアニメーションさせる場合に、こういう単位指定含めてどう実装しておくと地雷を踏みづらいのかが素朴な疑問として残りました。