前言
为了解决 TabBarView
嵌套滚动,一年前写了 extended_tabs,当时主要利用的是 OverscrollNotification 通知,将滚动传递给父 TabBarView
。但是这种机制会导致,滚动事件未结束之前,子 TabBarView
是没法获取到滚动事件,所以感觉会卡一下。
后来做图片缩放手势的时候,发现 PageView
会跟手势冲突,所以我去掉 PageView
的手势,使用 RawGestureDetector
监听来控制图片缩放以及PageView
的手势,详细内容大家可以看以往一篇介绍, https://juejin.cn/post/6844903814324027400#heading-6 。
其实要解决 TabBarView
嵌套滚动的问题,我们也可以把手势由自己掌控。网页例子
代码时间
获取父和子
void _updateAncestor() {
if (_ancestor != null) {
_ancestor._child = null;
_ancestor = null;
}
if (widget.link) {
_ancestor = context.findAncestorStateOfType<_ExtendedTabBarViewState>();
_ancestor?._child = this;
}
}
让 PageView
失去滚动
首先我们需要让 PageView
失去作用,很简单,我们给它加一个 NeverScrollableScrollPhysics
。 下面代码中_defaultScrollPhysics 是一个 NeverScrollableScrollPhysics
void _updatePhysics() {
_physics = _defaultScrollPhysics.applyTo(widget.physics == null
? const PageScrollPhysics().applyTo(const ClampingScrollPhysics())
: const PageScrollPhysics().applyTo(widget.physics));
if (widget.physics == null) {
_canMove = true;
} else {
_canMove = widget.physics.shouldAcceptUserOffset(_testPageMetrics);
}
}
增加手势监听
下面这块代码其实就是 ScrollableState
里面的源码, 地址 https://github.com/flutter/flutter/blob/63062a64432cce03315d6b5196fda7912866eb37/packages/flutter/lib/src/widgets/scrollable.dart#L499
void _initGestureRecognizers([ExtendedTabBarView oldWidget]) {
if (oldWidget == null ||
oldWidget.scrollDirection != widget.scrollDirection ||
oldWidget.physics != widget.physics) {
if (_canMove) {
switch (widget.scrollDirection) {
case Axis.vertical:
_gestureRecognizers = {
VerticalDragGestureRecognizer:
GestureRecognizerFactoryWithHandlers<
VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
(VerticalDragGestureRecognizer instance) {
instance
..onDown = _handleDragDown
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel
..minFlingDistance = widget.physics?.minFlingDistance
..minFlingVelocity = widget.physics?.minFlingVelocity
..maxFlingVelocity = widget.physics?.maxFlingVelocity;
},
),
};
break;
case Axis.horizontal:
_gestureRecognizers = {
HorizontalDragGestureRecognizer:
GestureRecognizerFactoryWithHandlers<
HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(),
(HorizontalDragGestureRecognizer instance) {
instance
..onDown = _handleDragDown
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel
..minFlingDistance = widget.physics?.minFlingDistance
..minFlingVelocity = widget.physics?.minFlingVelocity
..maxFlingVelocity = widget.physics?.maxFlingVelocity;
},
),
};
break;
}
}
}
}
在 build
方法中将返回的 Widget
用 RawGestureDetector
包裹起来
if (_canMove) {
result = RawGestureDetector(
gestures: _gestureRecognizers,
behavior: HitTestBehavior.opaque,
child: result,
);
}
return result;
处理手势
- 手势事件用
_hold
和_drag
将position
紧密联系了起来,代码比较简单,在 down, start, update, end, cancel 事件中做出相应的处理,这样就可以将手势传递给position
。
Drag _drag;
ScrollHoldController _hold;
void _handleDragDown(DragDownDetails details) {
assert(_drag == null);
assert(_hold == null);
_hold = position.hold(_disposeHold);
}
void _handleDragStart(DragStartDetails details) {
// It's possible for _hold to become null between _handleDragDown and
// _handleDragStart, for example if some user code calls jumpTo or otherwise
// triggers a new activity to begin.
assert(_drag == null);
_drag = position.drag(details, _disposeDrag);
assert(_drag != null);
assert(_hold == null);
}
void _handleDragUpdate(DragUpdateDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_drag?.update(details);
}
void _handleDragEnd(DragEndDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_drag?.end(details);
assert(_drag == null);
}
void _handleDragCancel() {
// _hold might be null if the drag started.
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_hold?.cancel();
_drag?.cancel();
assert(_hold == null);
assert(_drag == null);
}
void _disposeHold() {
_hold = null;
}
void _disposeDrag() {
_drag = null;
}
在 extended_tabs 的实现中,我们主要需要考虑,当 update (hold 和 start 没法区分手势的方向) 的时候发现没法拖动了(到达最小值和最大值), 父 和 子 TabBarView 的状态。
在 _handleAncestorOrChild 方法中,我们分别取判断 父和子 是否满足能够滚动的条件。
1.delta 左右
2.extentAfter == 0 达到最大
3.extentBefore == 0 达到最小
void _handleDragUpdate(DragUpdateDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_handleAncestorOrChild(details, _ancestor);
_handleAncestorOrChild(details, _child);
_drag?.update(details);
}
bool _handleAncestorOrChild(
DragUpdateDetails details, _ExtendedTabBarViewState state) {
if (state?._position != null) {
final double delta = widget.scrollDirection == Axis.horizontal
? details.delta.dx
: details.delta.dy;
if ((delta < 0 &&
_position.extentAfter == 0 &&
state._position.extentAfter != 0) ||
(delta > 0 &&
_position.extentBefore == 0 &&
state._position.extentBefore != 0)) {
if (state._drag == null && state._hold == null) {
state._handleDragDown(null);
}
if (state._drag == null) {
state._handleDragStart(DragStartDetails(
globalPosition: details.globalPosition,
localPosition: details.localPosition,
sourceTimeStamp: details.sourceTimeStamp,
));
}
state._handleDragUpdate(details);
return true;
}
}
return false;
}
- 最后在 end, canel 事件中也对 父和子 做出来对应操作即可。
void _handleDragEnd(DragEndDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_ancestor?._drag?.end(details);
_child?._drag?.end(details);
_drag?.end(details);
assert(_drag == null);
}
void _handleDragCancel() {
// _hold might be null if the drag started.
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_ancestor?._hold?.cancel();
_ancestor?._drag?.cancel();
_child?._hold?.cancel();
_child?._drag?.cancel();
_hold?.cancel();
_drag?.cancel();
assert(_hold == null);
assert(_drag == null);
}
使用
dependencies:
flutter:
sdk: flutter
extended_tabs: any
色块指示器
TabBar(
indicator: ColorTabIndicator(Colors.blue),
labelColor: Colors.black,
tabs: [
Tab(text: "Tab0"),
Tab(text: "Tab1"),
],
controller: tabController,
)
嵌套滚动
/// 如果开启,当当前TabBarView不能滚动的时候,会去查看父和子TabBarView是否能滚动,
/// 如果能滚动就会直接滚动父和子
/// 默认开启
final bool link;
ExtendedTabBarView(
children: [
List("Tab000"),
List("Tab001"),
List("Tab002"),
List("Tab003"),
],
controller: tabController2,
link: true,
)
滚动方向
/// 滚动方向
/// 默认为水平滚动
final Axis scrollDirection;
Row(
children: [
ExtendedTabBar(
indicator: const ColorTabIndicator(Colors.blue),
labelColor: Colors.black,
scrollDirection: Axis.vertical,
tabs: const [
ExtendedTab(
text: 'Tab0',
scrollDirection: Axis.vertical,
),
ExtendedTab(
text: 'Tab1',
scrollDirection: Axis.vertical,
),
],
controller: tabController,
),
Expanded(
child: ExtendedTabBarView(
children: [
const ListWidget(
'Tab1',
scrollDirection: Axis.horizontal,
),
const ListWidget(
'Tab1',
scrollDirection: Axis.horizontal,
),
],
controller: tabController,
scrollDirection: Axis.vertical,
),
)
],
)
缓存大小
/// 缓存页面的个数
/// 默认为0
/// 如果设置为1,那么意味内存里面有两页
final int cacheExtent;
ExtendedTabBarView(
children: [
List("Tab000"),
List("Tab001"),
List("Tab002"),
List("Tab003"),
],
controller: tabController2,
cacheExtent: 1,
)
结语
2020年只剩下2周,这是一个不普通的一年,很庆幸的是周围的人都安安全全的。亲眼见证了这么多,有时候感觉能健康快乐地写代码就很不错了。很多时候,只要肯用心,不管再难的问题,不管是工作学习还是生活上的,相信我们都会克服的。
爱Flutter,爱糖果,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果[图片上传失败...(image-1755ff-1607958437913)]QQ群:181398081
最最后放上Flutter Candies全家桶,真香。
[图片上传失败...(image-773835-1607958437913)]