如何在Flutter中模拟Android的showAsAction="ifRoom"?

14
在我的Flutter应用中,我有一个屏幕,它是一个MaterialApp,其主页是Scaffold小部件。 这个Scaffold的appBar属性是一个AppBar小部件,其中actions属性填充了一些操作和弹出菜单来存放其余的选项。 问题在于,据我所知,AppBar的actions列表的子元素可以是通用小部件(将添加为操作),也可以是PopupMenuButton的实例,在这种情况下,它将添加平台特定的图标,当触发时,打开AppBar弹出菜单。 在本机Android上不是这样工作的。我只需要填充具有菜单项的菜单,并且每个项目都可以被强制成一个操作,强制不是一个操作,或者具有特殊值“ifRoom”,即“如果有空间,则成为操作,否则成为弹出菜单中的项目”。 在Flutter中有没有一种方法可以拥有这种行为,而不必编写复杂的逻辑来填充AppBar的“actions”属性? 我已经查看了AppBar和PopupMenuButton文档,到目前为止没有解释如何做这样的事情。我可以模拟这种行为,但那么我就必须实际编写计算可用空间并相应地构建“actions”列表的例程。 以下是一个典型的混合操作和弹出菜单条目的Android菜单示例。请注意,“load_game”条目如果有空间可以成为操作,否则将成为菜单条目。
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/new_game"
          android:icon="@drawable/ic_new_game"
          android:title="@string/new_game"
          android:showAsAction="always"/>
    <item android:id="@+id/load_game"
          android:icon="@drawable/ic_load_game"
          android:title="@string/load_game"
          android:showAsAction="ifRoom"/>
    <item android:id="@+id/help"
          android:icon="@drawable/ic_help"
          android:title="@string/help"
          android:showAsAction="never" />
</menu>

另一方面,在Flutter中,我必须提前决定选项是操作还是菜单条目。

AppBar(
  title: Text("My Incredible Game"),
  primary: true,
  actions: <Widget>[
    IconButton(
      icon: Icon(Icons.add),
      tooltip: "New Game",
      onPressed: null,
    ),
    IconButton(
      icon: Icon(Icons.cloud_upload),
      tooltip: "Load Game",
      onPressed: null,
    ),
    PopupMenuButton(
      itemBuilder: (BuildContext context) {
        return <PopupMenuEntry>[
          PopupMenuItem(
            child: Text("Help"),
          ),
        ];
      },
    )
  ],
)
我希望的是AppBar实际上只有一个“action”属性,而不是“actions”。该属性将仅是一个小部件,允许我拥有任何东西,因此如果我只想要一系列操作,那么填充了IconButton的行就足够了。 除此之外,PopupMenuButton中的每个 PopupMenuItem 都将具有 "showAsAction" 属性。如果在 PopupMenuButton 内的一个或多个 PopupMenuItem 被检查为操作或 "ifRoom"(如果有空间),则 PopupMenuButton 将水平扩展并将这些项作为操作放置。
1个回答

7

这个功能不是开箱即用的,但你可以复制那种行为。我创建了以下小部件:

import 'package:flutter/material.dart';

class CustomActionsRow extends StatelessWidget {
  final double availableWidth;
  final double actionWidth;
  final List<CustomAction> actions;

  const CustomActionsRow({
    @required this.availableWidth,
    @required this.actionWidth,
    @required this.actions,
  });

  @override
  Widget build(BuildContext context) {
    actions.sort(); // items with ShowAsAction.NEVER are placed at the end

    List<CustomAction> visible = actions
        .where((CustomAction customAction) => customAction.showAsAction == ShowAsAction.ALWAYS)
        .toList();

    List<CustomAction> overflow = actions
        .where((CustomAction customAction) => customAction.showAsAction == ShowAsAction.NEVER)
        .toList();

    double getOverflowWidth() => overflow.isEmpty ? 0 : actionWidth;

    for (CustomAction customAction in actions) {
      if (customAction.showAsAction == ShowAsAction.IF_ROOM) {
        if (availableWidth - visible.length * actionWidth - getOverflowWidth() > actionWidth) { // there is enough room
          visible.insert(actions.indexOf(customAction), customAction); // insert in its given position
        } else { // there is not enough room
          if (overflow.isEmpty) {
            CustomAction lastOptionalAction = visible.lastWhere((CustomAction customAction) => customAction.showAsAction == ShowAsAction.IF_ROOM, orElse: () => null);
            if (lastOptionalAction != null) {
              visible.remove(lastOptionalAction); // remove the last optionally visible action to make space for the overflow icon
              overflow.add(lastOptionalAction);
              overflow.add(customAction);
            } // else the layout will overflow because there is not enough space for all the visible items and the overflow icon
          } else {
            overflow.add(customAction);
          }
        }
      }
    }

    return Row(
      mainAxisAlignment: MainAxisAlignment.end,
      children: <Widget>[
        ...visible.map((CustomAction customAction) => customAction.visibleWidget),
        if (overflow.isNotEmpty) PopupMenuButton(
          itemBuilder: (BuildContext context) => [
            for (CustomAction customAction in overflow) PopupMenuItem(
              child: customAction.overflowWidget,
            )
          ],
        )
      ],
    );
  }
}

class CustomAction implements Comparable<CustomAction> {
  final Widget visibleWidget;
  final Widget overflowWidget;
  final ShowAsAction showAsAction;

  const CustomAction({
    this.visibleWidget,
    this.overflowWidget,
    @required this.showAsAction,
  });

  @override
  int compareTo(CustomAction other) {
    if (showAsAction == ShowAsAction.NEVER && other.showAsAction == ShowAsAction.NEVER) {
      return 0;
    } else if (showAsAction == ShowAsAction.NEVER) {
      return 1;
    } else if (other.showAsAction == ShowAsAction.NEVER) {
      return -1;
    } else {
      return 0;
    }
  }
}

enum ShowAsAction {
  ALWAYS,
  IF_ROOM,
  NEVER,
}

您需要指定所有图标的可用总宽度(包括溢出图标),以及每个操作的宽度(它们都需要具有相同的宽度)。以下是使用示例:

return Scaffold(
  appBar: AppBar(
    title: const Text("Title"),
    actions: <Widget>[
      CustomActionsRow(
        availableWidth: MediaQuery.of(context).size.width / 2, // half the screen width
        actionWidth: 48, // default for IconButtons
        actions: [
          CustomAction(
            overflowWidget: GestureDetector(
              onTap: () {},
              child: const Text("Never 1"),
            ),
            showAsAction: ShowAsAction.NEVER,
          ),
          CustomAction(
            visibleWidget: IconButton(
              onPressed: () {},
              icon: Icon(Icons.ac_unit),
            ),
            showAsAction: ShowAsAction.ALWAYS,
          ),
          CustomAction(
            visibleWidget: IconButton(
              onPressed: () {},
              icon: Icon(Icons.cancel),
            ),
            overflowWidget: GestureDetector(
              onTap: () {},
              child: const Text("If Room 1"),
            ),
            showAsAction: ShowAsAction.IF_ROOM,
          ),
          CustomAction(
            visibleWidget: IconButton(
              onPressed: () {},
              icon: Icon(Icons.ac_unit),
            ),
            showAsAction: ShowAsAction.ALWAYS,
          ),
          CustomAction(
            visibleWidget: IconButton(
              onPressed: () {},
              icon: Icon(Icons.cancel),
            ),
            overflowWidget: GestureDetector(
              onTap: () {},
              child: const Text("If Room 2"),
            ),
            showAsAction: ShowAsAction.IF_ROOM,
          ),
          CustomAction(
            overflowWidget: GestureDetector(
              onTap: () {},
              child: const Text("Never 2"),
            ),
            showAsAction: ShowAsAction.NEVER,
          ),
        ],
      ),
    ],
  ),
);

请注意,我没有添加任何断言,但是对于 showAsActionALWAYSIF_ROOM 的操作,您应始终提供 visibleWidget,并对于 showAsActionIF_ROOMNEVER 的操作,始终提供 overflowWidget
使用上述代码的结果如下: enter image description here enter image description here 您可以根据需要自定义 CustomActionsRow,例如,您可能具有不同宽度的操作,在这种情况下,您希望每个 CustomAction 提供自己的宽度等。

感谢@Ovidiu的回答。自那以后,我已经学到了很多关于Flutter的知识,但我从未尝试解决这个问题。我一定会使用你的代码。 - Dancovich
2
这个实现还可以,但它实际上并没有遵循 Android 规范。这段代码与以下链接紧密相关:https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/ActionMenuPresenter.java#629 但是你会注意到 MaxItems 是从当前策略计算出来的。这个策略总是如下所示:https://android.googlesource.com/platform/frameworks/base/+/master/core/java/com/android/internal/view/ActionBarPolicy.java#46这是它的 Dart 翻译:https://gist.github.com/slightfoot/20882841df46bd952a45d668a6c79e37 - Simon
太好了!你应该在pub.dev上发布它!我发现了一个bug,我会在编辑中修复它。它导致某些操作未显示在溢出菜单中。 - Peter Keefe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接