3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Appcelerator TitaniumAdvent Calendar 2015

Day 15

FragmentでAndroidでもSplitWindowを実現する

Last updated at Posted at 2015-12-14

作るもの

frag.png

iPadではおなじみの、こんな画面を作ります。

はじめに

こちらのモジュールを利用します。
https://github.com/nborracha/com.yallaya.fragments
インストールは特別な手続きは不要なのでいつも通りにやっちゃってください。

できること

  • masterとdetailの画面分割ができるようになります。
  • もちろんそれ以上に分割することもできます。

できないこと

  • iOSのSplitWindowと違って、分割されたWindowごとにNavigationBar(ActionBar)を持つことはできません。
  • iOSのように自動的に分割されたWindowのボーダーが表示されません。
  • ActionBarの操作にはコツが必要です。

Fragmentとは

普段Titaniumを使っているとあまり意識することはありませんが、AndroidにはActivityという基本的な構成単位があります。1つのWindowが1つのActivityだと理解すればだいたい合っています。最小の構成単位が1つのアプリになっているiOSでは、アプリの起動中にまた別のアプリを起動して、用事が終わったら前のアプリの画面に戻ってくるなんて芸当は(Facebookのような一部の例外を除いて)出来ませんが、AndroidはActivityで構成されているので、Activity間を行き来するのは同一のアプリ内に用意されたものだろうが外部の公開されたActivityだろうが関係なく行き来することができるようになっています。

AndrdoidのUIはそんなActivityの上に各種のViewを乗せて画面を作り上げていくことになるわけですが、時々、それでは都合が悪くなることがあります。例えばTabGroupは複数のTabの下にWindowがぶら下がっています。


<Alloy>
  <TabGroup>
    <Tab title="Tab1">
      <Window />
    </Tab>
    <Tab title="Tab2">
      <Window />
    </Tab>
  </TabGroup>
</Alloy>

こうなった時、1つの画面の中に複数のActivityがあるわけで、結構大きいオブジェクト(HeavyWeightっていうくらいですからね)であるWindow(=Activity)をメモリ上に一気に展開するのは厳しいものがあります。

そのため、Titaniumを使っているとこの辺りはうまく隠蔽されていますが、実際にはこれらのWindowにはActivityではなくFragmentと呼ばれるコンポーネントが利用されています。複数のFragmentを合成して1つのActivityを作ることができるようになっているわけです。

Fragmentのライフサイクル

それぞれのFragmentは独立して成り立っており、Activityのように自身のライフサイクルを持っています。

作成した時 閉じる時
attach pause
create stop
createView destroyView
activityCreated destroy
start deatch
resume

Fragmentのライフサイクル

モジュールからこれらのイベントを検知することができます。

作ってみよう

ファイル構成はこんな感じです。assetsなどは省略します。

app
├── alloy.js
├── controllers
│   ├── anotherView.js
│   ├── detailView.js
│   ├── index.js
│   └── masterView.js
├── styles
│   ├── app.tss
│   └── index.tss
└── views
    ├── anotherView.xml
    ├── detailView.xml
    ├── index.xml
    └── masterView.xml

まず、app/alloy.jsにモジュールの設定を追加します。

Alloy.Globals.Fragments = require('com.yallaya.fragments');

次に、以下の手順でFragmentを画面に追加していきます。

  1. Frameと呼ばれる、Fragmentを置く場所を作成する(公序良俗に反しない限りいくつでも可)
  2. Frameに設置するFragmentと、Fragmentの中に表示するViewを作成
  3. FragmentをFrameに設置する

これで完了します。まずindex.jsに2つのFrameを作成します。

var masterView = Alloy.Globals.Fragments.createFragmentFrame({
  backgroundColor: '#56ACEE',
  width: '30%',
  height: Ti.UI.FILL,
  top: 0,
  left: 0
});

var detailView = Alloy.Globals.Fragments.createFragmentFrame({
  backgroundColor: 'Yellow',
  width: '70%',
  height: Ti.UI.FILL,
  top: 0,
  left: '30%'
});

左側にmasterViewとして設置するFrameには幅30%を指定しています。右側にdetailViewとして設置するFrameは左側の余白を30%にしています。

この2つのFrameの上に設置するFragmentと、そのFragmentの中に表示するViewを設定します。ここにはiPadのSplitViewのようにWindowやNavigationWindowを設置することはできません。

var detailViewContent = Alloy.Globals.Fragments.createFragment({
  container: Alloy.createController('detailView', {frame: detailView, parentView: "FirstDetailViewContainer"}).getView(),
  tag: "FirstDetailViewContainer",
  putFragment: true,
  addToBackStack: true
});
detailViewContent.addEventListener('destroy', function(){
  //右側のdetailViewのrootなのでこのViewが閉じる時はアプリも終了する
  $.index.close();
});

var masterViewContent = Alloy.Globals.Fragments.createFragment({
  container: Alloy.createController('masterView', {frame: detailView}).getView(),
  tag: "FirstMasterViewContent",
  putFragment: true,
  addToBackStack: true
});
masterViewContent.addEventListener('destroy', function(){
  //左側のdetailViewのrootなのでこのViewが閉じる時はアプリも終了する
  $.index.close();
});

それから重要なのが、Fragmentはモジュールのオブジェクト自体が管理するので、Alloy.Globals.Fragmentのメソッドを呼び出して追加します。これは設置するWindowが表示された後でないと実行できません。

$.index.addEventListener('open', function(e){
  Alloy.Globals.Fragments.add({
    frameId: masterView.getUid(),
    fragment: masterViewContent,
    addToBackStack: true
  });
  Alloy.Globals.Fragments.add({
    frameId: detailView.getUid(),
    fragment: detailViewContent,
    addToBackStack: true
  });
});

// FrameをWindowsに追加します
$.index.add(masterView);
$.index.add(detailView);

$.index.open();

新しいViewをFrame内に表示するには、Fragmentを作成してaddメソッドを利用します。

function addView(frame){
  var fragment = Alloy.Globals.Fragments.createFragment({
    container: Alloy.createController('anotherView').getView(),
    tag: 'anotherView',
    putFragment: true,
    addToBackStack: true
  });
  Alloy.Globals.Fragments.add({
    frameId: frame.getUid(),
    fragment: fragment,
    addToBackStack: true
  });
}

この時、Fragmentを表示するframeのidを指定するので、frameオブジェクトを利用しています。

逆に、Fragmentを閉じる場合は

function close(){
  Alloy.Globals.Fragments.popBackStack('anotherView');
}

のように、各Fragmentの作成時に指定したtagを利用します。

ActionBarとの連携

FrameやFragmentはそれ自体のActionBarを持つことはできません。そのため、ActionBarと連携する時は普通のWindowと違ってやり方を工夫しなければいけません。例えばタイトルを動的に変更する時は、Fragment(の中のView)からActionBarにアクセスする方法を確保する必要があります。

createViewする際の引数に親Windowオブジェクトを渡してもいいのですが、あまり深い階層まで引き廻すとコードの見通しが悪くなってしまいます。

そんな時は、親Windowがイベントリスナーを用意してそれぞれのFragmentとメッセージを交換し合うやり方を使うと便利です。

// index.js
function updateTitle(e){
  var activity = $.index.getActivity();
  if(activity){
    activity.actionBar.title = e.title;
  }
}
Ti.App.addEventListener('actionBar:updateTitle', updateTitle);
$.index.addEventListener('close', function(e){
  $.index.removeEventListener('actionBar:updateTitle', updateTitle);
});

// anotherView.js
Ti.App.fireEvent('actionBar:updateTitle', {
  title: "We're gonna have a real good tile together"
});

トラブルシューティング

Fragmentとして利用するViewがScrollViewやTableViewだとうまく動きません。

<Alloy>
  <ScrollView>
    ....

そんな時はViewで囲ってしまいましょう。

<Alloy>
  <View>
    <ScrollView>
    ....

以上、駆け足ですがTitaniumで扱うFragmentの紹介でした。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?