📖

フリック操作可能なBottomNavigationBar

2024/10/25に公開

はじめに

PageViewBottomNavigationBarを組み合わせて、フリック操作でページを移動できるボトムナビゲーションバーを作成するためのソースコードと解説を紹介します。

対象読者

  • BottomNavigationBarをはじめて使う方
  • PageViewとBottomNavigationBar`を組み合わせる方法を知りたい方

本記事の内容

  1. BottomNavigationBarの基本的な使い方
    BottomNavigationBarの基本構造や使用例を紹介します。
  2. PageViewとBottomNavigationBarを組み合わせる方法
    PageViewBottomNavigationBarを連携させて、タブの切り替えとページ移動を同期させる方法を解説します。
  3. [応用] BottomNavigationBarの一部にPageViewを組み合わせる方法
    BottomNavigationBarの特定のタブにPageViewを組み込み、フリック移動に制限をかける方法を紹介します。

1. BottomNavigationBarの基本的な使い方

BottomNavigationBarは、アプリケーションの画面下部に複数のアイコンを表示し、それぞれのアイコンをタップすることでページを切り替えることができるFlutterのウィジェットです。ここでは、4つのタブ(Home, Search, Favorites, Profile)を持つ基本的なBottomNavigationBarの使い方を紹介します。

ソースコード

main.dart
import 'package:flutter/material.dart';
import 'package:flutter_application/bottom_nav.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: BottomNav(),
    );
  }
}
bottom_nav.dart
import 'package:flutter/material.dart';

class BottomNav extends StatefulWidget {
  const BottomNav({super.key});
  
  
  State<BottomNav> createState() => _BottomNavState();
}

class _BottomNavState extends State<BottomNav> {
  // 現在選択されているタブのインデックス
  int _selectedIndex = 0;

  // 各タブに対応するウィジェット
  static const List<Widget> _pages = <Widget>[
    Center(child: Text('Home Page')),
    Center(child: Text('Search Page')),
    Center(child: Text('Favorites Page')),
    Center(child: Text('Profile Page')),
  ];

  // タップされたときに呼ばれる関数
  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        title: const Text('BottomNavigationBar Example'),
      ),
      body: child: _pages[_selectedIndex],   // 選択されたページを表示
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed, // 各タブの幅を固定
        currentIndex: _selectedIndex,        // 選択されたタブのインデックス
        backgroundColor: Colors.white,       // 背景色
        selectedItemColor: Colors.blue,      // 選択されたタブの色
        unselectedItemColor: Colors.grey,    // 選択されていないタブの色
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: 'Search',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.favorite),
            label: 'Favorites',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'Profile',
          ),
        ],
        onTap: _onItemTapped, // タップされたときに呼ばれる関数
      ),
    );
  }
}

解説

  • BottomNavigationBarItemの設定
    BottomNavigationBarItemで、各タブのアイコンとラベルを定義しています。今回は、Icons.homeIcons.searchIcons.favoriteIcons.personという4つのアイコンを使用しています。

  • 選択されたタブに応じたページの切り替え
    _selectedIndexという変数で現在選択されているタブを管理し、_onItemTapped関数でタップされたタブのインデックスを更新しています。_pages[_selectedIndex]で、選択されたタブに対応するページが動的に表示されます。

2. PageViewとBottomNavigationBarを組み合わせる方法

BottomNavigationBarPageViewを組み合わせることで、タップによるタブの切り替えだけでなく、ページをフリックして移動することができるようになります。

ソースコード

main.dart
main.dartに変更はありません。
bottom_nav.dart
import 'package:flutter/material.dart';

class BottomNav extends StatefulWidget {
  const BottomNav({super.key});
  
  
  State<BottomNav> createState() => _BottomNavState();
}

class _BottomNavState extends State<BottomNav> {
  // 現在選択されているタブのインデックス
  int _selectedIndex = 0;

+  // PageControllerを作成
+  final PageController _pageController = PageController();

  // 各タブに対応するウィジェット
  static const List<Widget> _pages = <Widget>[
    Center(child: Text('Home Page')),
    Center(child: Text('Search Page')),
    Center(child: Text('Favorites Page')),
    Center(child: Text('Profile Page')),
  ];

  // タブがタップされたときに呼ばれる関数
  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
+    // PageViewのページをアニメーション付きで切り替え
+    _pageController.jumpToPage(index);
  }

+  // ページがスワイプされたときにタブのインデックスを更新
+  void _onPageChanged(int index) {
+    setState(() {
+      _selectedIndex = index;
+    });
+  }

+  
+  void dispose() {
+    _pageController.dispose();
+    super.dispose();
+  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        title: const Text('BottomNavigationBar Example'),
      ),
-     body: child: _pages[_selectedIndex],   // 選択されたページを表示
+     // PageViewを追加
+     body: PageView(
+       controller: _pageController,     // PageControllerを設定
+       onPageChanged: _onPageChanged,   // ページが変更されたときにタブのインデックスを更新
+       children: _pages,                // 各ページのウィジェットを表示
+     ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed, // 各タブの幅を固定
        currentIndex: _selectedIndex,        // 選択されたタブのインデックス
        backgroundColor: Colors.white,     // 背景色
        selectedItemColor: Colors.blue,    // 選択されたタブの色
        unselectedItemColor: Colors.grey,  // 選択されていないタブの色
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: 'Search',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.favorite),
            label: 'Favorites',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'Profile',
          ),
        ],
        onTap: _onItemTapped, // タップされたときに呼ばれる関数
      ),
    );
  }
}

解説

  • PageViewの追加
    PageViewは、childrenに指定した画面をスワイプで切り替えられるウィジェットです。今回のコードでは、children_pagesHome, Search, Favorites, Profile)を設定しています。

  • ページとタブの連動
    ページの移動をPageControllerを使って制御するようにコードを変更します。BottomNavigationBarのタブがタップされたとき、_onItemTapped関数が呼ばれ、_selectedIndexを更新し、PageViewが対応するページにアニメーション付きで移動します。逆に、ページがスワイプされたときには_onPageChanged関数が呼ばれ、タブのインデックスも更新されるようにしています。

3. [応用] BottomNavigationBarの一部にPageViewを組み合わせる方法

この応用例では、BottomNavigationBarHomeタブはタップでのみ移動可能とし、Search, Favorites, Profileの3つのタブはタップとフリックの両方でページを切り替えられるようにします。

ソースコード

main.dart
main.dartに変更はありません。
bottom_nav.dart
import 'package:flutter/material.dart';

class BottomNav extends StatefulWidget {
  const BottomNav({super.key});

  
  State<BottomNav> createState() => _BottomNavState();
}

class _BottomNavState extends State<BottomNav> {
  int _selectedIndex = 0;
  final PageController _pageController = PageController();

  // 各タブに対応するウィジェット
  static const List<Widget> _pages = <Widget>[
    Center(child: Text('Home Page')),
    Center(child: Text('Search Page')),
    Center(child: Text('Favorites Page')),
    Center(child: Text('Profile Page')),
  ];

  // タブがタップされたときに呼ばれる関数
  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
-    // PageViewのページをアニメーション付きで切り替え
-    _pageController.jumpToPage(index);
+    // Homeから他のページへの遷移
+    if (_selectedIndex == 0) {
+      _pageController.jumpToPage(0); // Homeページへの遷移
+    } else {
+      _pageController.jumpToPage(index); // 他のページへの遷移
+    }
  }

  // ページがスワイプされたときにタブのインデックスを更新
  void _onPageChanged(int index) {
    setState(() {
-     _selectedIndex = index;
+     // SearchからHomeへの移動は許可しない
+     if (index == 0 && _selectedIndex == 1) {
+       _pageController.jumpToPage(1); // Searchページに留まる
+     } else {
+       _selectedIndex = index; // それ以外はインデックスを更新
+     }
    });
  }

  
  void dispose() {
    _pageController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        title: const Text('BottomNavigationBar Example'),
      ),
      body: PageView(
        controller: _pageController,
        onPageChanged: _onPageChanged,
+       physics: _selectedIndex == 0
+           ? const NeverScrollableScrollPhysics() // Homeページではフリック移動を無効
+           : const BouncingScrollPhysics(), // 他のページではフリック移動を有効
        children: _pages,
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        currentIndex: _selectedIndex,
        backgroundColor: Colors.white,
        selectedItemColor: Colors.blue,
        unselectedItemColor: Colors.grey,
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: 'Search',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.favorite),
            label: 'Favorites',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'Profile',
          ),
        ],
        onTap: _onItemTapped,
      ),
    );
  }
}

解説

  • フリック移動の制御
    • physicsプロパティを使い、Homeタブはフリック移動を無効とし、Search, Favorites, Profileはフリック移動が可能に設定しています。ただしこれだけではHomeからSearchのフリック移動は禁止できますが、SearchからHomeへのフリック移動はできてしまいます。
    • onPageChangedメソッド内でインデックスの値に応じて、特定の条件下でのみフリックによるページ移動を許可します。
      if (index == 0 && _selectedIndex == 1)_selectedIndexは現在のインデックス、indexは遷移先のインデックスを表すため、現在Search(_selectedIndex == 1)にいて、フリックでHome(index == 0)に移動しようとした際に同じページ(Search)に遷移させることでフリック移動を制限しています(半ば無理やりですが、、、)。

まとめ

今回はBottomNavigationBarPageViewを組み合わせる方法について紹介しました。さらなる応用として、HomeタブではBottomNavigationBarを表示しないでSearch, Favorites, ProfileでのみBottomNavigationBarを表示する、といったこともできそうです。

Discussion