首页 > 解决方案 > 是否可以为 Flutter 的可访问性焦点系统强制平滑隐式滚动?

问题描述

我目前正在调整 Flutter 应用以与 Android TV 设备配合使用。这方面的文档很少,但我已经设法通过使用可访问性焦点系统使用标准遥控器进行定向导航来感受我的方式。

然而,在我的一个布局中,我使用了一个PageView包含一些项目网格的网格,为了更好的定向导航人体工程学,网格分成页面。正如预期的那样,创建一个PageViewwithallowImplicitScrolling: true允许您通过将焦点标线从网格的一侧移开来在页面之间导航。但是,任何上下文中的隐式滚动都不会伴随任何动画,只是立即转换到下一个视图位置。这最终看起来不太好。实际上,同样的问题适用于所有隐式滚动,无论是在PageViewListViewGridView还是任何滚动视图中。

Widget _buildPaginatedGrid(BuildContext build) {
    return SliverToBoxAdapter(
      child: Container(
        height: MediaQuery.of(build).size.height - gameHeroHeight(context),
        child: PageView(
          allowImplicitScrolling: true,
          controller: _pageController,
          children: [
            _buildRawGrid(build), //just display the same grid twice for testing
            _buildRawGrid(build)
          ]
        ),
      ),
    );
  }

我知道这种滚动是由对 的调用驱动的RenderObject.showOnScreen,显然持续时间为零,因此我尝试SingleChildRenderObjectWidget使用其子类创建一个RenderProxyBox覆盖该RenderObject.showOnScreen方法以强制持续时间的方法。然而,无论我把这个东西放在哪里,在每个单独的网格项周围,在网格周围,在 . 周围PageView,在隐式滚动期间都不会显示动画,事实上,我的覆盖方法甚至都没有被调用。就像调用永远不会传播到它,无论它放置在小部件层次结构中的哪个位置。

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class RenderFocusSmoothScroll extends RenderProxyBox {
  RenderFocusSmoothScroll({RenderBox child}) : super(child);
  @override
  void showOnScreen({
    RenderObject descendant,
    Rect rect,
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
  }) {
    print("showOnScreen");
    super.showOnScreen(descendant: descendant, rect: rect, duration: Duration(milliseconds: 200), curve: Curves.ease);
  }
}

class FocusSmoothScroll extends SingleChildRenderObjectWidget {
  const FocusSmoothScroll({
    Key key,
    Widget child,
  })  : super(key: key, child: child);

  @override
  RenderFocusSmoothScroll createRenderObject(BuildContext context) {
    return RenderFocusSmoothScroll();
  }

  @override
  void updateRenderObject(BuildContext context, RenderFocusSmoothScroll renderObject) {

  }
}

关于使用焦点系统的隐式滚动的文档有点缺乏,关于正确覆盖RenderObject方法以强制对这些传播调用进行自定义处理的文档也是如此,因此将不胜感激。

标签: androidflutterdart

解决方案


好吧,我想通了。原来调用showOnScreen实际上是在框架自己的FocusTraversalPolicy源代码(lib/src/widgets/focus_traversal.dart)中。我没想到,我认为焦点遍历策略专门用于建立小部件之间的方向性关系,而不是实际将它们滚动到屏幕上。我只需要覆盖特定函数的主体:

@protected
void _focusAndEnsureVisible(FocusNode node, {ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit}) {
  node.requestFocus();
  Scrollable.ensureVisible(node.context, alignment: 1.0, alignmentPolicy: alignmentPolicy);
}

该调用不ensureVisible包括持续时间,因此,目标将立即滚动到。我最终需要基本上将该模块的大部分复制到我自己的项目中,因为没有任何方法可以重定向内置ReadingOrderTraversalPolicy模块以使用该模块私有函数的不同实现。当然不理想,该功能显然不打算被覆盖,但你绝对可以。

/// Overridden for smooth scrolling.
@protected
void _focusAndEnsureVisible(FocusNode node, {ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit}) {
  node.requestFocus();
  Scrollable.ensureVisible(
    node.context, 
    alignment: 1.0, 
    alignmentPolicy: alignmentPolicy, 
    duration: Duration(milliseconds: 200), 
    curve: Curves.ease,
  );
}

因此,从该文件中复制子类和 mixins 并重命名ReadingOrderTraversalPolicySmoothReadingOrderTraversalPolicy,我最终只是将一个DefaultFocusTraversal小部件粘贴在我的顶层 builder 包装器中MaterialApp,如下所示:

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '...',
      theme: /* ... */,
      home: SplashView(),
      navigatorKey: Globals.rootNavigatorKey,
      initialRoute: '/splash',
      routes: <String, WidgetBuilder>{
        // ...
      },
      onGenerateRoute: _generateRoute,
      builder: (context, widget) => DefaultFocusTraversal(
        policy: SmoothReadingOrderTraversalPolicy(), //imported from my own copy
        child: Stack(children: [widget, /* ... */]),
      ),
    );
  }

现在隐式滚动很流畅!如果其他人遇到这个问题,我希望这会有所帮助。

感谢@pskink 促使我更深入地了解方法调用的来源,在我想深入了解遍历策略本身之前,我只是被难住了。


推荐阅读