Flutter で BottomNavigationBar に FloatingActionButton を埋め込んでみた

こんな感じで、BottomNavigationBar に FloatingActionButton を埋め込んでみた。
![スクリーンショット 2024-08-31 18.13.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177438/daacece8-e6ba-bd46-635f-ceff8653fae3.png)
FloatingActionButton は丸くして、ぼわっと縁取ったカスタム仕様。

# コード
## カスタマイズした FloatingActionButton
まずは少しカッコよく見える、以下の特徴を持つ　FloatingActionButton を作った。
- 円形である。
- ぼわっとした縁取り。 
```dart

import 'package:flutter/material.dart';

/// ぼわぁっとした縁取りを持つ円形の FAB.
class BlurCircleFloatingActionButton extends StatelessWidget {
  final VoidCallback onPressed;
  final Widget child;
  final Color backgroundColor;
  final Color borderColor;
  final Color shadowColor;
  final double borderWidth;
  final double blurRadius;
  final double spreadRadius;
  final double width;
  final double height;

  const BlurCircleFloatingActionButton({super.key,
    required this.onPressed,
    required this.child,
    required this.backgroundColor,
    required this.borderColor,
    required this.shadowColor,
    this.borderWidth = 2.0,
    this.blurRadius = 10,
    this.spreadRadius = 5,
    this.width = 36.0,
    this.height = 36.0,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      width: width,
      height: height,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        boxShadow: [
          BoxShadow(
            color: shadowColor,
            blurRadius: blurRadius,
            spreadRadius: spreadRadius,
          ),
        ],
      ),
      child: FloatingActionButton(
        onPressed: onPressed,
        backgroundColor: backgroundColor,
        shape: CircleBorder(
          side: BorderSide(
            color: borderColor,
            width: borderWidth,
          ),
        ),
        child: child,
      ),
    );
  }
}
```

## FloatingActionButton を任意の位置に配置する
FloatingActionButton を任意の位置に配置するために、FloatingActionButtonLocation をカスタマイズしたクラスを用意。
BottomNavigationBar への Dock位置と、そこからのオフセットを指定できるようになる。
```dart
class CustomizeFloatingLocation extends FloatingActionButtonLocation {
  FloatingActionButtonLocation location;
  double offsetX;
  double offsetY;
  CustomizeFloatingLocation(this.location, this.offsetX, this.offsetY);
  @override
  Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
    Offset offset = location.getOffset(scaffoldGeometry);
    return Offset(offset.dx + offsetX, offset.dy + offsetY);
  }
}
```

## BottomNavigationBar 
そして、 BottomNavigationBar ！
build の部分以外は省略します。
```
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text('タイトル'),
      ),
      body: const SafeArea(
        child: Center(
          Text('サンプル',),
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items:  const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.photo_outlined),
            label: '写真',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.videocam_outlined),
            label: ' ',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.movie_outlined),
            label: '動画',
          ),
        ],
        currentIndex: _selectedIndex,
        onTap: _onItemTapped,
        type: BottomNavigationBarType.fixed,
        backgroundColor: Theme.of(context).colorScheme.primary,
        selectedIconTheme: IconThemeData(size: 30, color: Theme.of(context).colorScheme.secondary),
        selectedLabelStyle: TextStyle(color: Theme.of(context).colorScheme.secondary),
        selectedItemColor: Theme.of(context).colorScheme.secondary,
        unselectedIconTheme: IconThemeData(size: 25, color: Theme.of(context).colorScheme.onPrimary),
        unselectedLabelStyle: TextStyle(color: Theme.of(context).colorScheme.onPrimary),
        unselectedItemColor: Theme.of(context).colorScheme.onPrimary,
      ),

      floatingActionButtonLocation: CustomizeFloatingLocation(FloatingActionButtonLocation.centerDocked, 0, 28),
      floatingActionButton: BlurCircleFloatingActionButton(
        onPressed: () async {
        },
        width: 60,
        height: 60,
        backgroundColor: Theme.of(context).colorScheme.surface,
        borderColor: Theme.of(context).colorScheme.secondary,
        borderWidth: 2,
        shadowColor: Theme.of(context).colorScheme.secondary.withOpacity(0.5),
        blurRadius: 10,
        spreadRadius: 5,
        child: Icon(Icons.videocam_outlined, color: Theme.of(context).colorScheme.onSurface, size: 36,),
      ),
    );
  }
```
ポイントとしては以下。
- BottomNavigationBarItem は均等に配置するために FAB の裏にも用意してます。
- floatingActionButtonLocation　で、バーの中央にドック。
オフセットを指定して下方向にずらしています。

# 最後に
ちょっとはカッコいい感じにできました！
色々調べると、BottomNavigationBar より　BottomAppBar を使った方が、FABの周りに隙間を簡単に作ったり、色々とカスタマイズしやすいらしいです（コードは多めに書く必要があるけど）。