👻

FulutterでiOS風タブUI

2020/10/01に公開

(2018/02/02 追記) 現在の Flutter では CupertinoTabScaffold に書いてあるサンプルコードのように、CupertinoTabScaffold, CupertinoTabBar, CupertinoTabView を使うようにすることで、iOS風タブUIを問題なく実装できます。

CupertinoTabScaffold 内で、この記事中にある Offstage と TickerMode の処理をし、CupertinoTabView が内部で Navigator を持っているのでナビゲーション・スタックも問題ありません。


iOSのUITabControllerのような、複数のタブを完全に切り替えて表示するUIをFlutterで実装する方法についてです。

FlutterのTabBar、TabBarViewあたりは横にページが並んでいる形式のタブUIです。Androidの世界でよく見るタブで、iOSだとUIPageViewControllerに近いものです。

さて、iOS風の下タブUIとしては、BottomNavigationBarというクラスが提供されています。

まず単純に実装すると以下のようになります。

int index = 0;


Widget build(BuildContext context) {
  return new Scaffold(
    body: index == 0 ? new Text("Left") : new Text("Right"),
    bottomNavigationBar: new BottomNavigationBar(
      currentIndex: index,
      onTap: (int index) { setState((){ this.index = index; }); },
      items: <BottomNavigationBarItem>[
        new BottomNavigationBarItem(
          icon: new Icon(Icons.home),
          title: new Text("Left"),
        ),
        new BottomNavigationBarItem(
          icon: new Icon(Icons.search),
          title: new Text("Right"),
        ),
      ],
    ),
  );
}

ScaffoldのbottomNavigationBarを設定することで下タブが表示されます。onTapで選択されたタブのインデックスを変更するようにハンドラを実装し、そのインデックスに応じたページ内容をbodyにセットしています。

このサンプルは単純なので問題なく動作しますが、画面から非表示になったWidgetは解放されてしまいます。bodyの内部にStatefulWidgetを使ったり、そこからさらに別の画面を表示するような場合は工夫が必要です。

そこで、画面から取り除かないように、Stackで画面を重ねておきます。また表示されていないほうは、Offstageで非アクティブにしたり、TickerModeでアニメーションを停止したりしておきます。こうすることで、Widgetツリーには残っているのでStateが保持されつつ、表示されない画面を実現できます。

new Scaffold(
  ...
  body: new Stack(
    children: <Widget>[
      new Offstage(
        offstage: index != 0,
        child: new TickerMode(
          enabled: index == 0,
          child: new Text("Left"),
        ),
      ),
      new Offstage(
        offstage: index != 1,
        child: new TickerMode(
          enabled: index == 1,
          child: new Text("Right"),
        ),
      ),
    ],
  ),
  ...

さて、Stateが保持されるようにはできましたが、さらに画面遷移をする場合は、タブごとに別のナビゲーションスタックを持つようにする必要があります。

このためには、各タブ画面をMaterialApp(もしくはWidgetsAppやNavigator)にします。

new Scaffold(
  ...
  body: new Stack(
    children: <Widget>[
      new Offstage(
        offstage: index != 0,
        child: new TickerMode(
          enabled: index == 0,
          child: new MaterialApp(home: new YourLeftPage()),
        ),
      ),
      new Offstage(
        offstage: index != 1,
        child: new TickerMode(
          enabled: index == 1,
          child: new MaterialApp(home: new YourRightPage()),
        ),
      ),
    ],
  ),
  ...

この記事ではやりませんでしたが、最初に各タブのページすべてを生成せず、最初に表示に表示されるタイミングで生成するようにしたほうが、パフォーマンス上もよさそうですね。

このあたりの処理は CupertinoScaffold.tabbed あたりを見ると似たような実装をしていますので、参考になると思います。
https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/scaffold.dart

この記事はQiitaの記事をエクスポートしたものです

Discussion