每个页面底部的持久小部件

Uni*_*LSD 6 flutter

我正在制作一个音乐应用程序,并希望当前播放状态显示在大多数页面的底部。我通过bottomNavigationBar: const NowPlayingBar()在每个需要该杆的脚手架上使用来完成此操作。这有 2 个问题:

  1. 从技术上讲,这为每条拥有导航栏的路线制作了多个导航栏副本
  2. 导航栏不会“停留在”页面转换之上(下面的视频)

我发现执行此操作的唯一真正方法是使用 permanent_bottom_nav_bar 包,但这似乎不允许自定义小部件(有这个但NavBarStyle.custom似乎不存在)。有没有办法在所有页面上不断显示该栏?

这是显示问题 2 的视频:https://streamable.com/gxcswk

这是我现在正在播放的栏小部件(它基本上只是一个监听更改的列表图块):

import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';

import '../components/AlbumImage.dart';
import '../services/mediaStateStream.dart';
import '../services/FinampSettingsHelper.dart';
import '../services/processArtist.dart';
import '../services/MusicPlayerBackgroundTask.dart';

class NowPlayingBar extends StatelessWidget {
  const NowPlayingBar({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // BottomNavBar's default elevation is 8 (https://api.flutter.dev/flutter/material/BottomNavigationBar/elevation.html)
    const elevation = 8.0;
    final color = Theme.of(context).bottomNavigationBarTheme.backgroundColor;

    final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();

    return Material(
      color: color,
      elevation: elevation,
      child: SafeArea(
        child: StreamBuilder<MediaState>(
          stream: mediaStateStream,
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              final playing = snapshot.data!.playbackState.playing;

              // If we have a media item and the player hasn't finished, show
              // the now playing bar.
              if (snapshot.data!.mediaItem != null) {
                return SizedBox(
                  width: MediaQuery.of(context).size.width,
                  child: Dismissible(
                    key: const Key("NowPlayingBar"),
                    confirmDismiss: (direction) async {
                      if (direction == DismissDirection.endToStart) {
                        audioHandler.skipToNext();
                      } else {
                        audioHandler.skipToPrevious();
                      }
                      return false;
                    },
                    background: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 16.0),
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: const [
                          AspectRatio(
                            aspectRatio: 1,
                            child: FittedBox(
                              fit: BoxFit.fitHeight,
                              child: Padding(
                                padding: EdgeInsets.symmetric(vertical: 8.0),
                                child: Icon(Icons.skip_previous),
                              ),
                            ),
                          ),
                          AspectRatio(
                            aspectRatio: 1,
                            child: FittedBox(
                              fit: BoxFit.fitHeight,
                              child: Padding(
                                padding: EdgeInsets.symmetric(vertical: 8.0),
                                child: Icon(Icons.skip_next),
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                    child: ListTile(
                      onTap: () =>
                          Navigator.of(context).pushNamed("/nowplaying"),
                      // We put the album image in a ValueListenableBuilder so that it reacts to offline changes
                      leading: ValueListenableBuilder(
                        valueListenable:
                            FinampSettingsHelper.finampSettingsListener,
                        builder: (context, _, widget) => AlbumImage(
                          itemId: snapshot.data!.mediaItem!.extras!["parentId"],
                        ),
                      ),
                      title: Text(
                        snapshot.data!.mediaItem!.title,
                        softWrap: false,
                        maxLines: 1,
                        overflow: TextOverflow.fade,
                      ),
                      subtitle: Text(
                        processArtist(snapshot.data!.mediaItem!.artist),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                      ),
                      trailing: Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          if (snapshot.data!.playbackState.processingState !=
                              AudioProcessingState.idle)
                            IconButton(
                              // We have a key here because otherwise the
                              // InkWell moves over to the play/pause button
                              key: const ValueKey("StopButton"),
                              icon: const Icon(Icons.stop),
                              onPressed: () => audioHandler.stop(),
                            ),
                          playing
                              ? IconButton(
                                  icon: const Icon(Icons.pause),
                                  onPressed: () => audioHandler.pause(),
                                )
                              : IconButton(
                                  icon: const Icon(Icons.play_arrow),
                                  onPressed: () => audioHandler.play(),
                                ),
                        ],
                      ),
                    ),
                  ),
                );
              } else {
                return const SizedBox(
                  width: 0,
                  height: 0,
                );
              }
            } else {
              return const SizedBox(
                width: 0,
                height: 0,
              );
            }
          },
        ),
      ),
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

Sha*_*een 7

您可以做的是构建另一个导航器,其中包含正在播放的栏,如下所示:

(这是针对您的用例的该解决方案的简化版本)

这是一个展示它的外观的视频

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: VisiblePage(),
    );
  }
}

class VisiblePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    /* You could also make the body of the scaffold a stack, column, 
    etc. that has the the buildNavigator and the now playing bar as 
    its children instead of setting the NowPlayingBar as the bottom 
    nav bar and the buildNavigator as the body. */

    return Scaffold(
      backgroundColor: Colors.white,
      body: _buildNavigator(context),
      bottomNavigationBar: NowPlayingBar(),
    );
  }

  Map<String, WidgetBuilder> _routeBuilders(BuildContext context, Map args) {
    return {
      "/": (context) {
        return MainPage();
      },
      '/page1': (context) {
        return Page1();
      },
      '/page2': (context) {
        return Page2();
      }
    };
  }

  Widget _buildNavigator(BuildContext context) {
    return Navigator(
      onGenerateRoute: (settings) {
        final args = settings.arguments ?? {};
        var routeBuilders = _routeBuilders(context, args as Map);
        return MaterialPageRoute(
          fullscreenDialog: true,
          settings: settings,
          builder: (context) {
            return routeBuilders[settings.name]!(context);
          },
        );
      },
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

要导航到第 1 页,只需使用:

Navigator.of(context).pushNamed('/page1');

对于第 2 页(不包含栏),将 rootNavigator 设置为 true:

Navigator.of(context, rootNavigator: true)
                      .push(MaterialPageRoute(builder: (context) => Page2())); 
Run Code Online (Sandbox Code Playgroud)

这将使用“MyApp”页面中的初始导航器。您还可以通过将命名路由添加到“MyApp”页面来使其成为命名路由。