添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

不同的设备会提供不同级别的显示密度,使得操作的命中区域也要随之变化。 Flutter 的 VisualDensity 类可以让你快速地调整整个应用的视图密度,比如在可触控设备上放大一个按钮(使其更容易被点击)。

不同的设备会提供不同级别的显示密度,使得操作的命中区域也要随之变化。 Flutter 的 VisualDensity 类可以让你快速地调整整个应用的视图密度,比如在可触控设备上放大一个按钮(使其更容易被点击)。

double densityAmt = touchMode ? 0.0 : -1.0;
VisualDensity density =
    VisualDensity(horizontal: densityAmt, vertical: densityAmt);
return MaterialApp(
  theme: ThemeData(visualDensity: density),
  home: MainAppScaffold(),
  debugShowCheckedModeBanner: false,

若想在你的视图中使用 VisualDensity,你可以向上查找:

ScreenType getFormFactor(BuildContext context) {
  // Use .shortestSide to detect device type regardless of orientation
  double deviceWidth = MediaQuery.of(context).size.shortestSide;
  if (deviceWidth > FormFactor.desktop) return ScreenType.Desktop;
  if (deviceWidth > FormFactor.tablet) return ScreenType.Tablet;
  if (deviceWidth > FormFactor.handset) return ScreenType.Handset;
  return ScreenType.Watch;

又或者,你可以对大小类型进行更深层次的抽象,并且按照从小到大的方式定义:

enum ScreenSize { Small, Normal, Large, ExtraLarge }
ScreenSize getSize(BuildContext context) {
  double deviceWidth = MediaQuery.of(context).size.shortestSide;
  if (deviceWidth > 900) return ScreenSize.ExtraLarge;
  if (deviceWidth > 600) return ScreenSize.Large;
  if (deviceWidth > 300) return ScreenSize.Normal;
  return ScreenSize.Small;

使用基于屏幕大小的分界点的最佳场景,是在应用的顶层进行尺寸决策。在需要改变视觉密度、边距或者字体大小时,定义全局的基数是最好的方式。

你也可以利用分界点重新组织顶层的 widget 结构。例如,你可以判断用户是否使用手持设备,来切换垂直或水平的布局:

bool isHandset = MediaQuery.of(context).size.width < 600;
return Flex(
    children: [Text('Foo'), Text('Bar'), Text('Baz')],
    direction: isHandset ? Axis.vertical : Axis.horizontal);

在其他的 widget 中,你也可以切换部分子级 widget:

Widget foo = LayoutBuilder(
    builder: (context, constraints) {
  bool useVerticalLayout = constraints.maxWidth < 400.0;
  return Flex(
    children: [
      Text('Hello'),
      Text('World'),
    direction: useVerticalLayout ? Axis.vertical : Axis.horizontal,

现在这个 widget 可以组装在侧边面板、弹框又或是全屏视图中,并且根据尺寸自适应布局。

bool get isMobileDevice => !kIsWeb && (Platform.isIOS || Platform.isAndroid);
bool get isDesktopDevice =>
    !kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux);
bool get isMobileDeviceOrWeb => kIsWeb || isMobileDevice;
bool get isDesktopDeviceOrWeb => kIsWeb || isDesktopDevice;

在构建 Web 平台应用时,由于 dart.io package 不支持 Web 平台,导致使用 Platform API 时会异常。所以在上面的代码中,会首先判断是否在 Web 平台,基于这个条件,在 Web 平台上永远不会调用 Platform API。

static const double xsmall = 3; static const double small = 4; static const double medium = 5; static const double large = 10; static const double extraLarge = 20; // etc class Fonts { static const String raleway = 'Raleway'; // etc class TextStyles { static const TextStyle raleway = const TextStyle( fontFamily: Fonts.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(0xFF42A5F5)); // etc

这些常量可以用来替代硬编码的值:

return Padding(
  padding: EdgeInsets.all(Insets.small),
  child: Text('Hello!', style: TextStyles.body1),

由于所有的视图都引用了相同设计系统的规范,它们通常看起来更一致且更顺畅。与其进行容易出错的搜索替换,你可以将平台对应样式值的修改集中在一处。使用共享的规则也对设计的一致性有所帮助。

常见的设计类型里,如下这些类别可以以这样的方式进行组织:

return Listener(
    onPointerSignal: (event) {
      if (event is PointerScrollEvent) print(event.scrollDelta.dy);
    child: ListView());
Tab 遍历切换和焦点交互

使用键盘的用户,可能会希望通过 Tab 键在应用中快速导航,特别是对有动效和视觉障碍的用户,他们几乎完全依赖于键盘导航。

在考虑 Tab 遍历切换时,有两点需要注意:焦点如何在 widget 之间遍历,以及 widget 聚焦时的突出显示。

大部分内置的组件,类似于按钮和输入框,都默认支持遍历和高亮。如果你想让自己的 widget 包含在遍历中,你可以利用 FocusableActionDetector 进行控制。它将 ActionsShortcutsMouseRegionFocus 的能力进行了整合,创建出一个可以定义行为和键位绑定,并且提供聚焦和悬浮高亮事件回调的 widget。

class _BasicActionDetectorState extends State<BasicActionDetector> {
  bool _hasFocus = false;
  @override
  Widget build(BuildContext context) {
    return FocusableActionDetector(
      onFocusChange: (value) => setState(() => _hasFocus = value),
      actions: <Type, Action<Intent>>{
        ActivateIntent: CallbackAction<Intent>(onInvoke: (intent) {
          print('Enter or Space was pressed!');
          return null;
      child: Stack(
        clipBehavior: Clip.none,
        children: [
          FlutterLogo(size: 100),
          // Position focus in the negative margin for a cool effect
          if (_hasFocus)
            Positioned(
                left: -4,
                top: -4,
                bottom: -4,
                right: -4,
                child: _roundedBorder())
控制遍历的顺序

想要控制用户按下 Tab 键时的 widget 切换顺序,你可以使用 FocusTraversalGroup 来指定树中的区域,作为切换时的组别。

例如,你可能想要用户逐个切换所有的输入框,最后再切换到提交按钮:

// 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 combinations
    shortcuts: <ShortcutActivator, Intent>{
      SingleActivator(LogicalKeyboardKey.keyN, control: true):
          CreateNewItemIntent(),
    child: Actions(
      // Bind intents to an actual method in your code
      actions: <Type, Action<Intent>>{
        CreateNewItemIntent: CallbackAction<CreateNewItemIntent>(
            onInvoke: (intent) => _createNewItem()),
      // Your sub-tree must be wrapped in a focusNode, so it can take focus.
      child: Focus(
        autofocus: true,
        child: Container(),

Shortcuts widget 非常有用,因为它会让 widget 树的这一分支或它的子级仅在有焦点且可见时触发快捷方式。

最后,你还可以全局添加监听。这样的监听可以用于始终需要监听,且为应用全局的快捷键,或是在任何时候(无论是否已聚焦)都接收快捷键的部分。使用 RawKeyboard 添加全局监听非常简单:

static bool isKeyDown(Set<LogicalKeyboardKey> keys) {
  return keys.intersection(RawKeyboard.instance.keysPressed).isNotEmpty;

将它们合并判断,你就可以在 Shift+N 同时按下时触发行为:

if (event is RawKeyDownEvent) { bool isShiftDown = isKeyDown({ LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight, if (isShiftDown && event.logicalKey == LogicalKeyboardKey.keyN) { _createNewItem();

使用静态的监听时有一件值得注意的事情,当用户在输入框中输入内容,或关联的 widget 从视图中隐藏时,通常需要禁用监听。与 ShortcutsRawKeyboardListener 不同,你需要自己对它们进行管理。当你在为 Delete 键构建一个删除或退格行为的监听时,需要尤其注意,因为用户可能会在 TextField 中输入内容时受到影响。

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,
平台行为习惯与规范

最后,我们需要为自适应应用考虑平台标准。每个平台都有其不同的行为习惯与规范,这些名义和事实上的标准将操作应用的方法告知了用户。在当下网络如此便利的时代,用户更倾向于更加个性化的体验,但是提供这些平台标准,依然可以带来一些显著的好处:

thumbVisibility: DeviceType.isDesktop, controller: _scrollController, child: GridView.count( controller: _scrollController, padding: EdgeInsets.all(Insets.extraLarge), childAspectRatio: 1, crossAxisCount: colCount, children: listChildren),

对这些细节的把握,可以让你的应用在对应平台上体验更为良好。

static bool get isSpanSelectModifierDown =>
    isKeyDown({LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight});

要想监测不同平台的 Control 或 Command 键,你可以编写以下的代码:

if (Platform.isMacOS) { isDown = isKeyDown( {LogicalKeyboardKey.metaLeft, LogicalKeyboardKey.metaRight}); } else { isDown = isKeyDown( {LogicalKeyboardKey.controlLeft, LogicalKeyboardKey.controlRight}); return isDown;

最后一项针对键盘用户需要考虑的是 全选 操作。如果你的列表里有很多的可选择内容,可能你的许多用户也会希望能使用 Control+A 选中所有内容。

在触屏设备上,多选操作通常会被简化,与在桌面上按下了 isMultiSelectModifier(多选按钮)的行为类似。

在不同设备上处理多选操作,取决于你的用例是否有区分,但更重要的是为各个平台提供最好的交互模式。

children: [ TextSpan(text: 'Hello'), TextSpan(text: 'Bold', style: TextStyle(fontWeight: FontWeight.bold)),

在现代的桌面应用程序中,经常会有定制应用窗口的标题栏、添加 Logo 或者其他控制的需求,能节省界面对于垂直空间的占用。

return const Tooltip(
  message: 'I am a Tooltip',
  child: Text('Hover over the text to show a tooltip.'),

Flutter 同时也为编辑和选择文字提供了内置的上下文菜单。

若你想显示更高级的提示、悬浮面板或自定义的上下文菜单,你可以使用已有的 package,或利用 StackOverlay 进行构建。

可以使用的 package 包括:

TextDirection btnDirection =
    DeviceType.isWindows ? TextDirection.rtl : TextDirection.ltr;
return Row(
  children: [
    Spacer(),
      textDirection: btnDirection,
      children: [
        DialogButton(
            label: 'Cancel',
            onPressed: () => Navigator.pop(context, false)),
        DialogButton(
            label: 'Ok', onPressed: () => Navigator.pop(context, true)),