概述
Flutter提供了从单一代码库构建可移动设备、桌面和Web上运行的应用程序的新机会。然而,伴随着这些机遇,新的挑战也随之而来。您希望您的应用程序让用户感到熟悉,通过最大限度地提高可用性并保存舒适和无缝的体验来适应每个平台。也就是说,您需要构建的应用程序不仅是多平台,而且是完全平台自适应的。
开发平台自适应应用程序有很多考虑因素,但它们分为三大类:
- Layout
- Input
- Idioms and norms
一、创建自适应布局
将您的应用程序移植到多个平台时,您必须先考虑的事情之一是如何使其适应将在其上运行的各种尺寸和形状的布局。
1.1、布局小部件
如果您一直在构建应用程序或网站,您可能熟悉创建响应式界面。对于Flutter开发人员来说幸运的是,有大量的小部件可以让这一切变得更容易。
Flutter的一些最有用的布局小部件包括:
Single Child
Align——在自身内部对齐一个子部件。对于垂直和水平对齐,它采用介于-1和1之间的双精度值。AspectRatio——尝试将子部件的大小调整为特定的横纵比。ConstrainedBox——对其子项加大小限制,提供对最小或最大大小的控制。CustomSingleChildLayout——使用委托函数定位单个子项。委托可以确定子项的布局约束和定位。Expanded和Flexible——允许一个Row或一个Coolumn子项收缩或增长以填充任何可用空间。FractionallySizeBox——将其子项调整为可用空间的一小部分。LayoutBuilder——构建一个可以根据其父级大小自行回流的小部件。SingleChildScrollView——向单个孩子添加滚动。通常与Row或Column一起使用。
Multiple Child
Column、Row、Flex——在单个水平或垂直运行中布置子项。Column和Row都是继承自Flex。CustomMultiChildLayout——在布局阶段使用委托函数定位多个子项。Flow——类似于CustomMultiChildLayout,但效率更高,因为它是绘制阶段而不是布局阶段执行的。ListView、GridView和CustomScrollView——提供可滚动的子项列表。Stack——相对于边缘分层和定位多个子项。功能类似于CSS中的固定位置。Table——对其子项使用经典的表格布局算法,组合多行和多列。Wrap——在多个水平或垂直中显示其子项。
1.2、视觉密度
不同的输入设备提供不同级别的精度,这需要不同大小和点击区域。Flutter的VisualDensity类可以轻松调整整个应用程序的视图密度,例如,通过在触摸设备放大按钮(因此更容易点击)。
当您在MaterialApp,MaterialComponents中更改VisualDensity时,支持它会设置动画以匹配它们的密度。默认情况下,水平和垂直密度均设置为0.0,但您可以将密度设置为任何负值或正值。通过在不同密度之间切换,您可以轻松调整您的UI:
要设置自定义视觉密度,请将密度注入您的
MaterialApp主题:
double densityAmt = touchMode ? 0.0 : -1.0;
VisualDensity density = VisualDensity(horizontal: densityAmt, vertical: densityAmt);
return MaterialApp(
theme: Theme(visualDensity: density),
home: MainAppScaffold(),
debugShowCheckModeBanner: false,
);
要VisualDensity在您自己的视图中使用,您可以查找:
VisualDensity density = Theme.of(context).visualDensity;
容器不仅自己对密度变化做出反应,还会在密度变化时进行动画处理。这将您的自定义组件与内置组件联系在一起,以实现整个应用程序的平滑过渡效果。
如果所示,VisualDensiity是无单位的,因此对于不同的视图可能意味着不同的事物。在这个例子中,1个密度单位等于6个像素,但这完全取决于您的观点。它没有单位的事实使它非常通用,并且它应适用于大多数情况。
值得注意的是,Material组件通常为每个视觉密度单位使用大约4个逻辑像素的值。有关支持的组件的更多信息,请参阅VisualDensityAPI。有关一般密度原则的更多信息,请参阅Material Design 指南。
1.3、上下文布局
如果您需要的不仅仅是密度变化,而且找不到满足您需要的小部件,您可以采用更程序化的方法参数、计算大小、交换小部件,或者完全重构您的UI以适应特定的外形尺寸。
1.3.1、基于屏幕的断点
最简单的程序布局形式使用基于屏幕的断点。在Flutter中,这可以通过MediaQueryAPI来完成。对于此处使用的大小没有硬性规定,但这些事一般值:
class FormFactor {
static double desktop = 900;
static double tablet = 600;
static double handset = 300;
}
使用断点,您可以设置一个简单地系统来确定设备类型:
ScreenType getFormFactor(BuildContext context) {
// Use .shortestSide to detect device typee regardless of orientation
double deviceWidth = MediaQuery.of(context).size.shortestSlide;
if (deviceWidth > FormFactor.desktop) return ScreenType.Desktop;
if (deviceWidth > FormFactor.tablet) return ScreenType.Tablet;
if (deviceWidth > FormFactor.handset) return ScreenType.HHandset;
return ScreenType.Watch;
}
作为替代方案,您可以对其进行更多抽象并根据从小到大进行定义:
enum ScreenSize { Small, Normal, Large, ExtraLarge }
ScreenSize getSize(BuildContext context) {
double deviceWidth = MediaQuery.of(context).size.shoortestSide;
if (deviceWidth > 900) return ScreenSize.ExtraLarge;
if (deviceWidth > 600) return ScreenSize.Large;
if (deviceWidth > 300) return ScreenSize.Normal;
return ScreenSize.Small;
}
基于屏幕的断点最适用于您的应用程序做出顶级决策。在全局基础上定义时,最好更改视觉密度、填充或字体大小等内容。
您还可以使用基于屏幕断点来重排您的顶级小部件树。例如,当用户不在手机上时,您可以从垂直布局切换到水平布局:
bool isHandset = MediaQuery.of(context).size.width < 600;
return Flex(
children: [Text('Foo'), Text('Bar'), Text('Baz')],
direction: isHandset ? Axis.vertical : Axis.horizontal);
);
在另一个小部件中,您可能会完全交换一些child:
Widget foo = Row(
children: [
...isHandset ? _getHandsetChildren() : _getNormalChildren(),
],
);
1.3.2、使用LayoutBuilder获得额外的灵活性
尽管检查总屏幕大小对于全屏页面或做出全局布局决策非常有用,但对于嵌套子视图通常并不理想。通常,子视图有自己的内部断点并且只关心它们可用于渲染的空间。
在Flutter中处理这个问题最简单方法是使用LayoutBuilder类。LayoutBuilder允许小部件相应传入的本地大小约束,这可以使小部件比依赖全局值更加通用。
可以使用以下方式重写前面的示例LayoutBuilder:
Widget foo = LayoutBuilder(
builder: (context, constraints) {
bool userVerticalLayout = constraints.maxWidth < 400.0;
return Flex(
chilren: [
Text('Hello'),
Text(''World'),
],
direction: iseVerticalLayout: Axis.vertical : Axis.horizontal,
);
}
);
这个小部件现在可以在侧面板、对话框甚至全屏试图中组合,并根据提供的任何空间调整其布局。
1.3.3、设备细分
有时您希望根据您运行的事迹平台做出布局决策,而不管大小。例如,在构建自定义标题栏时,您可能需要检查操作系统类型并调整标题栏的布局,以免被本机窗口按钮覆盖。
要确定您所在的平台组合,您可以会用Platform API和kIsWeb值:
bool get isMobileDevice => !kIsWeb && (Platform.isIOS || Platform.isAndroid);
bool get isDesktopDevice => !kIsWeb && (Platform.issMacOS || Platform.isWindow || Platform.isLinux);
bool get isMobileDeviceOrWeb => kIsWeb || isMobileDevice;
bool get isDesktopDeviceOrWeb => kIsWeb || isDesktopDevice;
如果Platform不抛出异常,则无法从Web构建访问API,因为dart.io Web目前不支持包。结果,这段代码首先检查web,并且由于短路,Dart永远不会调用Platfform web目标。
1.4、样式的单一真实来源
如果您为样式值(如填充、间距、角形状、字体大小等)创建单一真实来源,您可能会发现维护视图会更容易。这可以通过一些辅助类轻松完成:
class Insets {
static const double xsmall = 3;
static const double small = 4;
static const double medium = 5;
static const double large = 10;
static const ddouble extraLarge = 20;
// etc
}
class Fonts {
static const String raleway = 'Raleway';
// etc
}
clas TextStyles {
static const TextStyle raleway = const TextStyle(
fontFamily: Fontss.raleway;
);
static TextStyle buttonText1 = TextStyle(fontWeight: fontWeight.bold, fontSize: 14);
static TextStyle buttonText2 = TextStyle(fontWeight: FontWeight.normal, fontSize: 11);
static TextStyle h1 = TextStyle(fontWeight: FontWeight.bold, fontSize: 22);
static TextStyle h2 = TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
static late TextStyle body1 = raleway.copyWith(color: Color(0xFF42AF55));
// etc
}
然后可以使用这些常量代替硬编码的数值:
return Padding(
padding: EdgeInsets.all(Insets.small),
child: Text('Hello!', style: TextStyles.body1),
);
所有视图都引用相同的共享设计系统规则,它们往往看起来更好、更一致。可以在一个地方特定平台更改或调整值,而不是使用容易出错的搜索替换。使用共享规则还有一个额外的好处,即有助于在设计方面加强一致性。
可以用这种方式表示的一些常见设计系统级别是:
- 动画时序
- 大小和断点
- 插图和填充
- 圆角半径
- 阴影
- 笔画
- 字体系列、大小和样式
与大多数规则一样,也有例外:在应用程序的其他任何地方都不会是哟哦弄个的一次性值。用这些值来混淆样式规则也没有什么意义,但值得考虑的是它们是否应该从现有值派生(例如:padding + 1.0)。您还应该注意相同语义值的重用或重复。这些值应该添加到全局样式规则集中。
1.5、设计每个外形规格的优势
除了屏幕尺寸之外,您还应该花在时间考虑不同外形规则的独特优势和劣势。让您的多平台应用程序在所有地方都提供相同的功能并不总是理想的。考虑在某些设备类别上专注于特定功能,甚至删除某些功能是否有意义。
例如,移动设备是便携式的并且有摄像头,但它们不太适合进行精细的创意工作。考虑到这一点,您可能会更专注于捕获内容并使用移动UI的位置数据对其进行标记,但会专注于为平板电脑或桌面UI组织或操作该内容。
另一个例子是利用网络极低的共享门槛。如果您正在部署Web应用程序,请决定要支持哪些深度链接,并在设计导航路线时考虑这些内容。
这里的关键要点是考虑每个平台最擅长什么,看看是否有您可以利用的独特功能。
1.6、使用桌面构建目标进行快速测试
测试自适应界面的最有效方法之一是利用桌面构建目标。
在桌面上运行时,您可以在应用程序运行时轻松调整窗口大小预览各种屏幕尺寸。这与热重载相结合,可以大大加快响应式UI的开发。
1.7、先解决触摸
构建出色的触摸UI通常比传统的桌面UI更难,部分原因是缺乏时输入加速器,如右键单击、滚轮或键盘快捷键。
应对这一挑战的一种方法是最初专注于出色的面向触摸的UI。您仍然可以使用桌面目标进行大部分测试,因为它的迭代速度。但是,请记住经常切换到移动设备以验证一切是否正确。
完成触摸界面,您可以调整鼠标用户的视觉密度,然后在所有其他输入上分层。将这些其他输入视为加速器——使任何更快的替代方案。重要的是要考虑用户号在使用时输入设备时的期望,并努力在您的应用中反映这一点。
二、输入
当然,仅仅调整您的应用程序外观是不够的,您还必须支持不同的用户输入。鼠标和键盘你引入了触摸设备以外的输入类型,例如滚轮、右键单击、悬停交互、选项卡遍历和键盘快捷键。
2.1、滚轮
默认情况下,滚动小部件喜欢ScrollView或ListView支持滚轮,并且由于几乎每个可滚动的自定义小部件都哦欧式是使用其中之一构建的,因此它可以与它们一起使用。
如果您需要实现自定义滚动行为,您可以是使用Listener小部件,它可以让您自定义UI对滚轮的反应。
return Listener(
onPoninterSignal: (event) {
if (event is PointerScrollEvent) print(event.scrollDelta.dy);
},
child: ListView()
);
2.2、Tab遍历和焦点交互
使用物理键盘的用户希望他们可以使用Tab快速导航您的应用程序,而有运动或视觉差异的用户通常完全依赖键盘导航。
选型卡交互有两个注意事项:焦点如何从一个小部件移动到另一个小部件,成为遍历,以及当一个小部件获得焦点时显示的视觉突出显示。
大多数内置组件,如按钮和文本字段,默认支持遍历和高亮显示。如果您有自己的要包含子遍历小部件,则可以使用该FocusableActionDetector小部件来创建自己的控件。它结合了Actions、Shortcuts、MouseRegion和Focus小部件的功能来创建一个检测器来定义操作和key绑定,并提供用于处理焦点和悬停突出显示的回调。
class _BasicActionDetectorState extendss State<BasicActionDetector> {
bool _hasFocus = false;
@override
Widget build(BuildContext context) {
return FocusableActionDetector(
onFfocusChange: (value) => setState(() => _hasFocus = value),
action: <Type, Action<Intent>>{
ActivateIntent: CallbackAction<Intent>(onInvoke: (intent) {
print('Enter or Space was pressed!');
return null;
}),
},
child: Stack(
clipBehavior: Clip.none,
children: [
// Position focuss in the negative margin for a cool effect
if (_hasFocus) {
Positioned(
left: -4,
top: -4,
bottom: -4,
right: -4,
child: _roundedBorder()
),
}
],
),
);
}
}
2.2.1、控制遍历顺序
为了更好地控制用户按下tab键时小部件所关注的顺序,您可以使用FocusTraversakGroup来定义树的部分,这些部分在tab键时被视为一个组。
例如,您可以在跳转到提交按钮之前跳转到表单中的所有字段:
return Column(
children: [
FocusTraversalGroup(
child: MyFormWithMultipleColumnsAndRows(),
),
SubmitButton(),
],
);
Flutter有几种内置的方式来遍历小部件和组,默认为RendingOrderTraversalPolicy类。此类通常运行良好,但可以使用另一个预定义TraverssalPolicy类或通过创建自定义策略来修改它。
2.3、键盘加速器
除了选项卡遍历之外,桌面和Web用户还习惯于将各种键盘快捷键绑定到特定操作。无论它是Delete用于快速删除还是Control+N用于新文档的键,请务必考虑您的用户期望的不同加速器。键盘是一种强大的输入工具,因此请尽量发挥它的效率。
根据您的目标,键盘加速器可以在Flutter中以多种方式实现。
如果你有一个像TextField和Button这样已经有焦点节点的小部件,你可以将它报错在RowKeyboardListener并监听键盘事件。
@override
Widget build(BuildContext context) {
return Focus(
onKey: (node, event) {
if (event is RawKeyDownEvent) {
print(event.logicalKey);
}
return KeyEventResult.ignored;
},
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 400),
child: TextField(
decoration: InputDecortion(
border: OutlineInputBorder(),
),
),
),
);
}
如果您想将一组快捷键应用于树的大部分,您可以使用Shortcuts小部件:
// Define a class for each type of shortcut action you want
class CreateNewItemIntent extends Intent {
const CreateNewItemIntent();
}
Widget build(BuildContext context) {
return Shortcuts(
// Bind intents to key coombinations
shortcuts: <ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.KeyN, control: true): CreateNewItemIntent(),
},
child: Action(
// Bind intents to an actial method in your code
actions: <Type, Action<Intent>>{
CreateNewItemIntent: CallbackAction<CreateItemIntent>(
onInvoke: (intent) => _createNewItem(),
).
},
// Your sub-tree must be wrapped in a focusNode, so it can take focus.
child: Focus(
autoFocos: true,
child: Container(),
),
),
);
}
该Shortcuts小部件很有用,因为它只允在该小部件树或其子项之一具有焦点并且可见时触发快捷方式。
最后一个选型是全局监听器。此监听器可用于始终开启的应用程序范围的快捷方式,或用于可以在可见时接受快捷方式的面板(无论其焦点状态如)。添加全局监听器很容易RowKeyboard:
void initState() {
super.initState();
RawKeyboard.instance.addListener(_handleKey);
}
@override
void dispose() {
RawKeyboard.instance.removeListener(_handleKey);
super.dispose();
}
要使用全局监听器检查组合键,您可以使用RawKeyboard.instance.keyPressed。例如,像下面这样的方法可以检查是否有任何提供的键被按下:
static bool isKeyDown(Set<LogicalKeyboardKey> keys) {
return keys.intersection(RawKeyboard.instance.keyPresed).isNotEmpty;
}
把这两件事放在一起,你可以在Shift+N按下时触发一个动作:
void _handleKey(event) {
if (event is RawKeyDownEvent) {
bool isShiftDown = isKeyDown({
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
});
if (isShiftDown && event.logicalKey == LogicalKeyboardKey.keyN) {
_createNewItem();
}
}
}
使用静态监听器时要注意的一个注意事项是,当用户在字段中键入内容或其关联的小部件从视图中隐藏时,你通常需要禁用它。与Shortcuts不同,RawKeyboardListener,这是您的管理责任。让您为绑定Delete/Backspace加速器时,这一点尤其重要Delete,但随后又TextFields用户可能正在输入的子项。
2.4、鼠标进入、退出和悬停
在桌面上,更改鼠标光标以指示悬停在其上的内容的功能是很常见。例如,当您将鼠标悬停在按钮上时通常会看到一个手型光标,当您将鼠标悬停在文本上会看到一个光标。
Material组件集内置了对标准按钮和文本光标的支持。要从您自己的小部件中更改光标,请使用MouseRegion:
// Show hand cursor
return MouseRegion(
cursor: SystemMouseCursors.click,
// Request focus when clicked
child: GestureDetector(
onTap: () {
Focus.of(context).requestFocus();
_submit();
},
child: Logo(showBorder: hasFocus),
),
);
MouseRegion对于创建自定义翻转和悬停效果也很有用:
return MouseRegion(
onEnter: (_) => setState(() => _isMouseOver = true),
onExit: (_) => setState(() => _isMouseOver = false),
onHover: (e) => print(e.localPosition),
child: Container(
height: 500,
color: _isMouseOver ? Colors.blue : Colors.black,
),
);
三、规范
自适应应用程序要考虑的最后一个领域的平台标注。每个平台都有自己的习语和规范;这些名义上或事实上的标准告知用户对应应用程序如何运行的期望。部分归功于网络,用户习惯了很多的定制体验,但反映这些平台标准仍然可以提供显著的好处:
- 减少认知负荷——通过匹配用户现有的心智模型,完成任务变得直观,这需要更少的思考,提高生产力,减少挫败感。
- 建立信任——当应用程序不符合他们的期望时,用户可能会变得警惕或怀疑。相反,感觉熟悉的UI可以建立用户信任并有助于提高质量感知。这通常会带来更好地应用商店评级的额外好处——这是我们都可以感激的!
3.1、考虑每个平台上的预期行为
第一步是花一些时间考虑在这个平台上预期的外观、表现或行为是什么。尝试忘记当前实施的任何限制,只设想理想的用户体验。从那里向后工作。
另一种思考这个方法是问,”这个平台的用户期望如何实现这个目标?“然后,尝试设想它如何在您的应用程序中毫无妥协地工作。
如果您不是该平台的普通用户,这可能会很困难。您可能不知道特定的习语,很容易完全错过它们。例如,Android用户可能不知道iOS上的平台约定,masOS、Linux和Windows也是如此。这些对您来说可能很微妙,但对有经验的用户来说却显而易见。
3.1.1、寻找平台倡导者
如果可能,请指派某人作为每个平台的倡导者。理想情况下,您的拥护者使用该平台作为他们的主要设备,并且可以提供高度自以为是的用户的观点。减少人数,合并角色。有一位支持Windows和Android,一位支持Linux和网络,一位支持Mac和iOS。
目标是获得持续的、知情的反馈,以便应用程序在每个平台都感觉良好。应该鼓励倡导者非常挑剔,说出他们认为与他们设备上的典型程序不同的任何内容。一个简单地例子是对话框中的默认按钮通常在Mac和Linux上位于左侧,但在Windows上位于右侧。如果您不定期使用某个平台,则很容易忽略此类细节。
重要提示:倡导者不需要是开发人员,甚至不需要全职团队成员。他们可以是设计人员、利益相关者或定期构建的外部测试人员。
3.1.2、保持独特
符合预期行为并不意味着您的应用需要使用默认组件或样式。许多最流行的平台应用程序都非常独特和自以为是用于界面,包括自定义按钮、上下文菜单和标题栏。
您越能跨平台整合样式和行为,开发和测试就越容易。诀窍是在创造强烈认同感的独特体验与尊重每个平台的规范之间取得平衡。
3.2、要考虑常见习语和规范
快速浏览一些您可能想要考虑的特定规范和习语,以及如何在Flutter中处理它们。
3.2.1、滚动条的外观和行为
桌面和移动用户期望滚动条,但他们希望它们在不同的平台上表现不同。移动用户希望只有在滚动时才出现更小的滚动条,而桌面用户通常希望他们可以单击或拖动的无所不在的更大滚动条。
Flutter带有一个内置Scrollbar小部件,它已经支持根据当前平台自适应颜色和大小。您可能想要进行的一项调整是alwaysShown在桌面平台上切换:
return Scrollbar(
thunmbVisibility: DeviceType.isDesktop,
controller: _scrollController,
child: GridView.count(
controller: _scrollController,
padding: EdgeInsets.all(Insets.extraLarge),
childAspectRatio: 1,
crossAxisCount: colCount,
children: listChildren
),
);
这种对细节的微妙关注可以让您的应用程序在给定平台上感觉更舒适。
3.2.2、多选
处理列表中的多选是跨平台存在细微差异的另一个领域:
static bool get isSpanSelectModifierDown => isKeyDown({LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight});
要对控制或命令执行平台感知检查,您可以编写如下内容:
static bool get isMultiSelectModifierDown {
bool isDoown = false;
if (Platform.isMacOS) {
isDown = isKeyDown({LogicalKeyboardKey.metaLeft, LogicalKeyboardKey.metaRight});
} else {
isDown = isKeyDown({LogicalKeyboardKey.controlLeft, LogicalKeyboardKey.controlRight});
}
return isDown;
}
键盘用户的最后一个考虑隐私是全选操作。如果您有大量可选项目的列表,您的许多键盘用户会期望他们可以使用Control+A来选择所有项目。
3.2.2.1、触摸设备
在触摸设备上,多选通常被简化,预期的行为类似于isMultiSelectModifier在桌面上按下按钮。您可以使用单击来选择或取消项目,并且通常会有一个按钮来选择全部或清除当前选择。
您如何处理不同设备上的多选取决于您的具体用例,但重要的是保护您为每个平台提供可能的最佳交互模型。
3.2.3、可选文本
网络(以及较小范围的桌面)上的一个普遍期望是可以使用鼠标光标选择大多数可见文本。当文本不可选择,网络用户往往会有不适反应。
幸运的是,小部件很容易支持这一点SelectableText:
return SelectableText('Select me!');
要支持富文本,请使用TextSpan:
return SelectableText.rich(
TextSpan(
children: [
TextSpan(text: 'Hello'),
TextSpan(text: 'Bold', style: TextStyle(fontWeight: FontWeight.bold),
],
),
);
3.3、标题栏
在现代桌面应用程序中,自定义应用程序窗口的标题、添加logo以增强品牌或上下文控制以帮助节省UI中的垂直空间试是很常见的。
这在Flutter中不直接支持,但您可以使用该
bits_dojo包来禁用本机标题栏,并且您自己的标题栏替换它们。
这个包允许您添加任何你想要的小部件,TitleBar因为它在引擎盖下使用纯Flutter小部件。当您导航到应用程序的不同部分时,这使得调整标题栏变得容易。
3.3.1、上下文菜单和工具提示
在桌面上,有几种交互表现为叠加层中显示小部件,但它们的触发、消除和定位方式有所不同:
- 上下文菜单——通常由右键单击触发,上下文菜单位于鼠标附件,通过单击任意位置、从菜单中选择一个选项或在其外部单击来关闭。
- 工具提示——通常通过鼠标悬停在交互式元素上200-400毫秒来触发,工具提示通常锚定到一个小部件(而不是鼠标位置),并在鼠标光标离开该小部件时消失。
- 弹出面板(也称为弹出面板)——与工具提示类似,弹出面板通常固定在小部件上。主要区别在于面板通常在点击事件时显示,并且它们通常不会在光标离开时隐藏自己。相反,面板通常通过在面板外部单击或按下”关闭“或”提交“按钮来关闭。
要在Flutter中显示基本工具提示,请使用内置Tooltip小部件:
return const Tooltip(
message: 'I am a Tooltip',
child: Text('Hover over the text to show a tooltip.'),
);
在编辑或选择文本时,Flutter 还提供了内置的上下文菜单。
要显示更高级的工具提示、弹出面板或创建自定义上下文菜单,您可以使用可用的包之一,也可以使用 或 自己构建Stack它Overlay。
一些可用的软件包包括:
虽然这些控件作为加速器对于触摸用户来说可能很有价值,但它们对于鼠标用户来说是必不可少的。这些用户希望右键单击内容、就地编辑内容并悬停鼠标以获取更多信息。未能满足这些期望可能会导致用户失望,或者至少会感到有些事情不太对劲。
3.3.2、水平按钮顺序
在Windows上,当显示一行按钮时,确认按钮位于行的开头(左侧)。在所有其他平台上,情况恰恰相反。确认按钮位于行尾(右侧)。
这可以在Flutter中使用Row中TextDirection属性轻松处理:
TextDirection btnDirection = DeviceType.isWindows ? TextDirection.rtl : TextDirection.ltr;
return Ron(
children: [
Spacer(),
Row(
textDirection: btnDirection,
children: [
DialogButton(
label: 'Cancel',
onPressed: () => Navigator.pop(context, false),
),
DialogButton(
label: 'Ok',
onPressed: () => Navigator.pop(context, true),
),
],
),
],
);
3.4、菜单栏
桌面应用程序的另一种常见模式是菜单栏。在 Windows 和 Linux 上,此菜单作为 Chrome 标题栏的一部分存在,而在 macOS 上,它位于主屏幕的顶部。
目前,您可以使用原型插件指定自定义菜单栏条目,但预计此功能最终会集成到主 SDK 中。
值得一提的是,在 Windows 和 Linux 上,您不能将自定义标题栏与菜单栏组合在一起。当您创建自定义标题栏时,您将完全替换本机标题栏,这意味着您也失去了集成的本机菜单栏。
如果您同时需要自定义标题栏和菜单栏,您可以通过在 Flutter 中实现它来实现,类似于自定义上下文菜单。
3.5、拖放
基于触摸和基于指针的输入的核心交互之一是拖放。尽管这两种类型的输入都需要这种交互,但在滚动可拖动项目列表时需要考虑重要的区别。
一般来说,触摸用户希望看到拖动手柄以区分可拖动区域和可滚动区域,或者通过使用长按手势启动拖动。这是因为滚动和拖动都共用一根手指进行输入。
鼠标用户有更多的输入选项。他们可以使用滚轮或滚动条来滚动,这通常消除了对专用拖动手柄的需要。如果您查看 macOS Finder 或 Windows 资源管理器,您会发现它们是这样工作的:您只需选择一个项目并开始拖动。
在 Flutter 中,您可以通过多种方式实现拖放。讨论具体的实现不在本文的范围内,但一些高级选项是:
- 直接使用
Draggable和DragTargetAPI 来获得自定义外观。 - 挂钩
onPan手势事件,并在 parent 中自己移动对象Stack。 - 使用pub.dev 上的预制列表包之一。