前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >师于源码 | Flutter 区域视口双向滑动

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

作者头像
张风捷特烈
发布2023-08-10 14:34:55
4450
发布2023-08-10 14:34:55
举报
theme: cyanosis
1. 缘起

注: 本文有 Blibli 视频版,食用效果更加: https://www.bilibili.com/video/BV11p4y137Cy/

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

162.gif
162.gif

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

163.gif
163.gif

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

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

既然是开源的,从代码中得到 Debugger 面板代码区域,视口双向滑动的实现方式就有可行性。当你手中握有源码,并且其中有你非常需要的功能,那手撕它就会变得非常有趣,下面一起来看看吧。

image.png
image.png

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

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

image.png
image.png

将代码 clone 到本地方便查看,其中很明显有个 codeview.dart,很可能就是我们的目标文件。根据 Web 的界面,可以很快定位到对应代码实现的位置,从这里可以看出 Flutter DevTools 的开源项目分包还是非常好的。

image.png
image.png

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

打开文件后,可以通过 AndroidStudio 的 Structure 页签,快速掌握当前文件中的类型结构信息。比如看到 _CodeViewState 的结构,找到 build 方法,双击就可以跳转到对应的源码位置。

image.png
image.png

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

image.png
image.png

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

image.png
image.png
代码语言:javascript
复制
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 方法。

image.png
image.png

其中文本宽度的计算,可以通过 TextPainter 来处理,对应的代码如下:

代码语言:javascript
复制
/// 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 构造时存在的一行代码:可以只监听竖直滚动的通知,忽略水平方滚动向通知。否则竖直方向滑动条展示的时机会有问题。

image.png
image.png

3.通过小案例提取精华

由于 debugger 代码面板中涉及到其他很多东西,这里来精简一下,做个区域视口双向滑动的最小案例,来方便大家理解和使用。如下所示,蓝色区域内有一行文字,当窗口宽度缩小到文本溢出时,底部会呈现滑动条支持水平滑动:

164.gif
164.gif

这里先总结一下实现区域视口的双向滚动的步骤:

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

下面看一下案例的代码实现:其中六处的 tag 和上面一致。tag3tag4 处是准备两个可滑动视口,这里简单期间使用 SingleChildScrollView,其他滑动组件都可以。 tag1tag1 处是给出两个 Scrollbar,并绑定对应方向上的的滑动控制器; tag5 处对水平方向宽度约束的处理; tag6 处对竖直方向滚动条进行处理。

代码语言:javascript
复制
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 源码中扒出来了,然后分享给大家,这个功能在桌面端中是非常非常必要的。也希望大家在开源项目中遇到某些自己渴望的功能,也可以静下心来撕一撕,从源码中学习,师于源码。 那本文就到这里,谢谢观看 ~

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-08-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 缘起
  • 2. DevTools 代码区域相关源码分析
  • 3.通过小案例提取精华
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档