1. 缘起
注: 本文有 Blibli 视频版,食用效果愈加:www.bilibili.com/video/BV11p…
在桌面端中,有时候需求在宽度区域过窄时,同时支撑水平缓竖直双向滑动。比方 AndroidStudio 的文件树和编辑器区域,当宽度较窄时,水平方向经过拖拽底部翻滚条来翻滚视口。
在之前一向想完成这种效果,惋惜未能完成,因为两个双向的 ScrollBar 同时存在会产生冲突,会出现一些交互上的问题。直到最近在玩 Flutter DevTools, 在 Debugger 面板中惊讶地发现,这个代码面板不便是我苦苦追求的 区域视口双向滑动
吗?!
可谓踏破铁鞋无觅处,得来全不费工夫。因为我是知道的:
Flutter DevTools 的 Web 界面是 Flutter 项目,而且是由官方维护的开源项目 devtools。
既然是开源的,从代码中得到 Debugger 面板代码区域,视口双向滑动的完成方式就有可行性。当你手中握有源码,而且其间有你十分需求的功用,那手撕它就会变得十分风趣,下面一起来看看吧。
2. DevTools 代码区域相关源码分析
Flutter DevTools 有几个功用页签,界面相关的代码在 screens
文件夹中,其间每个文件夹对应一个功用,今天的主角是 debugger
中的代码区域。
将代码 clone 到本地便利检查,其间很明显有个 codeview.dart
,很可能便是咱们的目标文件。根据 Web 的界面,能够很快定位到对应代码完成的方位,从这儿能够看出 Flutter DevTools 的开源项目分包还是十分好的。
认识一个源码中的某个组件,特别是 StatelessWidget
或 StatfulWidget
,能够从组件的构建逻辑开始看起,因为这是组合型组件逻辑的中心。
翻开文件后,能够经过 AndroidStudio 的 Structure 页签,快速把握当前文件中的类型结构信息。比方看到 _CodeViewState
的结构,找到 build 方法,双击就能够跳转到对应的源码方位。
如下构建逻辑中,当代码非空时,会经过 buildCodeArea 方法创立代码面板区域。到这儿,就离真相越来越近了, buildCodeArea 方法中很可能便是区域视口双向滑动完成的场所。
持续检查,能够发现如下的中心代码:其间 tag1
和 tag2
处有两个 Scrollbar
,分别代表竖直和水平方向的翻滚条。竖直方向上的滑动控制器是 textController
,在 tag3
处和 Lines 组件
绑定,也便是说 Lines 是一个竖直翻滚的可滑动组件;水平方向上的滑动控制器是 horizontalController
,在 tag4
处和 SingleChildScrollView 组件
绑定,支撑横向的翻滚。
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
对数据进行烘托。所以想要得到最长的一行文字,只需求找到最长一行的文字,并核算其宽度即可。也便是下面的 findLongestTextSpan
和 calculateTextSpanWidth
方法。
其间文本宽度的核算,能够经过 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 构造时存在的一行代码:能够只监听竖直翻滚的告诉,疏忽水平方翻滚向告诉。否则竖直方向滑动条展示的时机会有问题。
3.经过小事例提取精华
因为 debugger 代码面板中涉及到其他许多东西,这儿来精简一下,做个区域视口双向滑动的最小事例,来便利大家了解和运用。如下所示,蓝色区域内有一行文字,当窗口宽度缩小到文本溢出时,底部会呈现滑动条支撑水平滑动:
这儿先总结一下完成区域视口的双向翻滚的步骤:
- 需求两个可滑动的视口: SingleChildScrollView/ListView/CustomScrollview/GridView 等。
- 需求两个 Scrollbar 用于控制视口滑动,而且指定 ScrollController, 关联 [滑动视口] 和 [滑动条]。
- 束缚水平方向的宽度,核算内容区尺度宽度值,使小于该尺度时,答应水平滑动。
- 竖直方向的 Scrollbar#notificationPredicate 返回 notification.depth == 1。 用于禁用水平方向呼应翻滚监听。
下面看一下事例的代码完成:其间六处的 tag 和上面一致。tag3
和 tag4
处是预备两个可滑动视口,这儿简略期间运用 SingleChildScrollView,其他滑动组件都能够。 tag1
和 tag1
处是给出两个 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 账号: 张风捷特烈
大众号: 编程之王