1. 缘起

注: 本文有 Blibli 视频版,食用效果愈加:www.bilibili.com/video/BV11p…

在桌面端中,有时候需求在宽度区域过窄时,同时支撑水平缓竖直双向滑动。比方 AndroidStudio 的文件树和编辑器区域,当宽度较窄时,水平方向经过拖拽底部翻滚条来翻滚视口

师于源码 |  Flutter 区域视口双向滑动

在之前一向想完成这种效果,惋惜未能完成,因为两个双向的 ScrollBar 同时存在会产生冲突,会出现一些交互上的问题。直到最近在玩 Flutter DevTools, 在 Debugger 面板中惊讶地发现,这个代码面板不便是我苦苦追求的 区域视口双向滑动 吗?!


可谓踏破铁鞋无觅处,得来全不费工夫。因为我是知道的:

Flutter DevTools 的 Web 界面是 Flutter 项目,而且是由官方维护的开源项目 devtools。

既然是开源的,从代码中得到 Debugger 面板代码区域,视口双向滑动的完成方式就有可行性。当你手中握有源码,而且其间有你十分需求的功用,那手撕它就会变得十分风趣,下面一起来看看吧。

师于源码 |  Flutter 区域视口双向滑动


2. DevTools 代码区域相关源码分析

Flutter DevTools 有几个功用页签,界面相关的代码在 screens 文件夹中,其间每个文件夹对应一个功用,今天的主角是 debugger 中的代码区域。

师于源码 |  Flutter 区域视口双向滑动


将代码 clone 到本地便利检查,其间很明显有个 codeview.dart,很可能便是咱们的目标文件。根据 Web 的界面,能够很快定位到对应代码完成的方位,从这儿能够看出 Flutter DevTools 的开源项目分包还是十分好的。

师于源码 |  Flutter 区域视口双向滑动

认识一个源码中的某个组件,特别是 StatelessWidgetStatfulWidget,能够从组件的构建逻辑开始看起,因为这是组合型组件逻辑的中心。

翻开文件后,能够经过 AndroidStudio 的 Structure 页签,快速把握当前文件中的类型结构信息。比方看到 _CodeViewState 的结构,找到 build 方法,双击就能够跳转到对应的源码方位。

师于源码 |  Flutter 区域视口双向滑动


如下构建逻辑中,当代码非空时,会经过 buildCodeArea 方法创立代码面板区域。到这儿,就离真相越来越近了, buildCodeArea 方法中很可能便是区域视口双向滑动完成的场所。

师于源码 |  Flutter 区域视口双向滑动


持续检查,能够发现如下的中心代码:其间 tag1tag2 处有两个 Scrollbar,分别代表竖直和水平方向的翻滚条。竖直方向上的滑动控制器是 textController ,在 tag3 处和 Lines 组件 绑定,也便是说 Lines 是一个竖直翻滚的可滑动组件;水平方向上的滑动控制器是 horizontalController,在 tag4 处和 SingleChildScrollView 组件 绑定,支撑横向的翻滚。

师于源码 |  Flutter 区域视口双向滑动

Widget contentBuilder(_, ScriptRef? script) {
  if (lines.isNotEmpty) {
    return DefaultTextStyle(
      style: theme.fixedFontStyle,
      child: Scrollbar( //-> ::tag1::
        key: CodeView.debuggerCodeViewVerticalScrollbarKey,
        controller: textController,
        thumbVisibility: true,
        // Only listen for vertical scroll notifications (ignore those
        // from the nested horizontal SingleChildScrollView):
        notificationPredicate: (ScrollNotification notification) =>
            notification.depth == 1,  //-> ::tag6::
        child: ValueListenableBuilder<StackFrameAndSourcePosition?>(
          valueListenable: widget.debuggerController?.selectedStackFrame ??
              const FixedValueListenable<StackFrameAndSourcePosition?>(
                null,
              ),
               ///略... 
                Expanded(
                  child: LayoutBuilder(
                    builder: (context, constraints) {
                      final double fileWidth = calculateTextSpanWidth(
                        findLongestTextSpan(lines),
                      );
                      return Scrollbar( //-> ::tag2::
                        key:
                            CodeView.debuggerCodeViewHorizontalScrollbarKey,
                        thumbVisibility: true,
                        controller: horizontalController,
                        child: SingleChildScrollView( //-> ::tag4::
                          scrollDirection: Axis.horizontal,
                          controller: horizontalController,
                          child: SizedBox(
                            height: constraints.maxHeight,
                            width: math.max(  //-> ::tag5::
                              constraints.maxWidth,
                              fileWidth,
                            ),
                            child: Lines(
                              height: constraints.maxHeight,
                              codeViewController: widget.codeViewController, //-> ::tag3::
                              scrollController: textController,
                              ///...

上面的两个Scrollbar、滑动控制器和滑动视口是双向滑动的中心,但并没有什么难点。除此之外,最难的一点是核算出内容宽度的临界值,也便是说,当束缚的宽度尺度小于哪个值时,答应进行拖拽滑动。因为假如宽度够大,是没必要拖拽滑动的。

这儿很明显,当面板的宽度束缚小于文字的最大宽度时,需求经过翻滚来检查宽度之外的视图。所以在 tag5 处,通 过 SizedBox 组件对水平方向的组件施加紧束缚,让内容宽度不小于 fileWidth 。也便是说,当面板区域小于fileWidth 之后,也便是宽度束缚过小, 水平方向的 SingleChildScrollView 组件就会发挥效能。

下面来介绍一下,源码中怎么核算最长文本宽度的。完成因为 debugger 功用需求支撑单行的调试,以及点击方法时进行跳转。代码是作为行列表数据存在的,Lines 组件经过 ListView 对数据进行烘托。所以想要得到最长的一行文字,只需求找到最长一行的文字,并核算其宽度即可。也便是下面的 findLongestTextSpancalculateTextSpanWidth 方法。

师于源码 |  Flutter 区域视口双向滑动

其间文本宽度的核算,能够经过 TextPainter 来处理,对应的代码如下:

/// Returns the width in pixels of the [span].
double calculateTextSpanWidth(TextSpan? span) {
  final textPainter = TextPainter(
    text: span,
    textAlign: TextAlign.left,
    textDirection: TextDirection.ltr,
  )..layout();
  return textPainter.width;
}

最后一点,也是最主要的一点需求处理。也有因为这一点,之前一向没能完成区域视口双向滑动的功用。下面是在竖直方向上 ScrollBar 构造时存在的一行代码:能够只监听竖直翻滚的告诉,疏忽水平方翻滚向告诉。否则竖直方向滑动条展示的时机会有问题。

师于源码 |  Flutter 区域视口双向滑动


3.经过小事例提取精华

因为 debugger 代码面板中涉及到其他许多东西,这儿来精简一下,做个区域视口双向滑动的最小事例,来便利大家了解和运用。如下所示,蓝色区域内有一行文字,当窗口宽度缩小到文本溢出时,底部会呈现滑动条支撑水平滑动:

这儿先总结一下完成区域视口的双向翻滚的步骤:

  1. 需求两个可滑动的视口: SingleChildScrollView/ListView/CustomScrollview/GridView 等。
  2. 需求两个 Scrollbar 用于控制视口滑动,而且指定 ScrollController, 关联 [滑动视口] 和 [滑动条]。
  3. 束缚水平方向的宽度,核算内容区尺度宽度值,使小于该尺度时,答应水平滑动。
  4. 竖直方向的 Scrollbar#notificationPredicate 返回 notification.depth == 1。 用于禁用水平方向呼应翻滚监听。

下面看一下事例的代码完成:其间六处的 tag 和上面一致。tag3tag4 处是预备两个可滑动视口,这儿简略期间运用 SingleChildScrollView,其他滑动组件都能够。 tag1tag1 处是给出两个 Scrollbar,并绑定对应方向上的的滑动控制器; tag5 处对水平方向宽度束缚的处理; tag6 处对竖直方向翻滚条进行处理。

class ColorTextArea extends StatefulWidget {
  const ColorTextArea({super.key});
  @override
  State<ColorTextArea> createState() => _ColorTextAreaState();
}
class _ColorTextAreaState extends State<ColorTextArea> {
  final ScrollController _hCtrl = ScrollController();
  final ScrollController _vCtrl = ScrollController();
  @override
  Widget build(BuildContext context) {
    String text = '张风捷特烈@编程之王: https://github.com/toly1994328';
    const TextStyle style =  TextStyle(fontSize: 24,fontFamily: 'aldk',color: Colors.white, letterSpacing: 1);
    return Scrollbar( //-> ::tag1::
      thumbVisibility: true,
      //-> ::tag6::
      notificationPredicate: (ScrollNotification notification) => notification.depth == 1, 
      key: const Key('debuggerCodeViewVerticalScrollbarKey'),
      controller: _vCtrl,
      child: LayoutBuilder(
        builder:(context, constraints){
          final double boxHeight = 2500;
          double boxWidth = calculateText(text,style);
          return Scrollbar( //-> ::tag2::
            key: const Key('debuggerCodeViewHorizontalScrollbarKey'),
            thumbVisibility: true,
            controller: _hCtrl,
            child: SingleChildScrollView(//-> ::tag4::
                controller: _hCtrl, 
                scrollDirection: Axis.horizontal,
                child: SizedBox(
                    height: constraints.maxHeight,
                    width: max(boxWidth, constraints.maxWidth), //-> ::tag5::
                    child: SingleChildScrollView(
                        controller: _vCtrl, //-> ::tag3::
                        child: Container(
                          color: Colors.blue,
                          height: boxHeight,
                          child: Text( text ,style: style,),
                        )
                    ))
            ),
          );
        } ,
      ),
    );
  }
  /// 核算文字宽度
  double calculateText(String text,TextStyle style) {
    final TextPainter textPainter = TextPainter(
      textAlign: TextAlign.left,
      textDirection: TextDirection.ltr,
      text: TextSpan(text: text,style: style)
    );
    textPainter.layout();
    return textPainter.width;
  }
  @override
  void dispose() {
    _hCtrl.dispose();
    _vCtrl.dispose();
    super.dispose();
  }
}

这样,Flutter 区域视口双向滑动的功用就从 Flutter DevTools 源码中扒出来了,然后共享给大家,这个功用在桌面端中是十分十分必要的。也期望大家在开源项目中遇到某些自己渴望的功用,也能够静下心来撕一撕,从源码中学习,师于源码。 那本文就到这儿,谢谢观看 ~


结尾:

笔者将在 bilibli 不定期发布一些视频教程,欢迎大家能够重视和支撑。另外大众号也能够重视一下,也会发布一些文章。

bilibli 账号: 张风捷特烈
大众号: 编程之王