Flutter项目开发中,使用了 TabBar+ExtendedTabBarView 实现了页面左右滑动的功能。
正常的使用,手势滑动结束的效果是根据ExtendedTabBarView的参数physics控制的,physics有NeverScrollablePhysics(不可滚动)、BouncingScrollPhysics(类似IOS回弹效果)、ClampingScrollPhysics(默认Android上的水波纹效果)、AlwaysScrollableScrollPhysics等多种回弹效果。
不过在使用中,出现一个新需求,那就是有多个TabBar的时候,ExtendedTabBarView页面从左往右滑动到最后一个View的后,再继续右滑,可以让当前页面消失,或者滚动到另外一个页面。这个时候就需要我们重新自定义physics的参数,也就是ScrollPhysics的滑动回弹效果中,监听最后的滑动的结束。
一、首先我们要理解ScrollPhysics
ScrollPhysics本身是如何执行回弹效果等一系列处理的呢,查看源码发现,其中有以下几种方法
ScrollPhysics applyTo(ScrollPhysics ancestor) {
return ScrollPhysics(parent: buildParent(ancestor));
}
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
if (parent == null)
return offset;
return parent.applyPhysicsToUserOffset(position, offset);
}
double applyBoundaryConditions(ScrollMetrics position, double value) {
if (parent == null)
return 0.0;
return parent.applyBoundaryConditions(position, value);
}
Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
if (parent == null)
return null;
return parent.createBallisticSimulation(position, velocity);
}
自定义一个ScrollPhysics,监听applyPhysicsToUserOffset返回的offset会发现,offset返回的数据,告诉我们的就是widget的移动的相对位置。createBallisticSimulation这个方法中,显示的是触发边界的动画的展示。那是如何触发边界条件的呢,这个要就配合applyBoundaryConditions这个方法,对边界条件进行判断。
查看ClampingScrollPhysics(默认Android上的水波纹效果)的源码,applyBoundaryConditions中对返回的value,position.pixels和position.minScrollExtent进行了多重判断,有兴趣大家可以打印出日志,显示不同的值
水波纹效果会让view无法滑动,但是有波纹效果,所有这个方法不会对applyPhysicsToUserOffset的位移进行操作,而是在applyBoundaryConditions中对滑动到边界添加效果。
而在BouncingScrollPhysics(类似IOS回弹效果)的源码中,我们看到,applyPhysicsToUserOffset被修改了,因为ios的回弹需要对整个页面进行位移,所以需要改变widget的位置,但是applyBoundaryConditions没有变动,因为ios的回弹不需要类似波纹效果等。
二、重写ScrollPhysics,实现滑动到边缘的监听
有了以上的理解,我们就可以开始重写ScrollPhysics了。
其实实现很简单,首先我们要确定,滑动到边缘是否需要波纹等效果。我的项目里面是用到了水波纹,所以我重写了ClampingScrollPhysics这个类里面的applyPhysicsToUserOffset方法。
其实我们只需要获取到applyPhysicsToUserOffset里面返回的offset,因为这个offset滑动超出边界的时候,返回的值为负数,当offset为负值的时候,我们只需要判断当前的页面的pageIndex,就能确定是左边活动到边界,还是右边滑动到边界。比如左右滑动有三个view,那么(pageIndex==0&&offset<-10)的时候,说明左滑到了边界;(pageIndex==2&&offset<-10)说明右滑到边界。
代码如下:
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/physics.dart';
import 'dart:math' as math;
class CustomScrollPhysics extends ScrollPhysics {
final Function(double offset) overCallBack;
const CustomScrollPhysics({this.overCallBack, ScrollPhysics parent})
: super(parent: parent);
@override
CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
return CustomScrollPhysics(
overCallBack: this.overCallBack, parent: buildParent(ancestor));
}
@override
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
//print('applyPhysicsToUserOffset: ' + offset.toString());
if (overCallBack != null) {
overCallBack(offset);
}
return super.applyPhysicsToUserOffset(position, offset);
}
@override
double applyBoundaryConditions(ScrollMetrics position, double value) {
assert(() {
if (value == position.pixels) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'$runtimeType.applyBoundaryConditions() was called redundantly.'),
ErrorDescription(
'The proposed new position, $value, is exactly equal to the current position of the '
'given ${position.runtimeType}, ${position.pixels}.\n'
'The applyBoundaryConditions method should only be called when the value is '
'going to actually change the pixels, otherwise it is redundant.'),
DiagnosticsProperty<ScrollPhysics>(
'The physics object in question was', this,
style: DiagnosticsTreeStyle.errorProperty),
DiagnosticsProperty<ScrollMetrics>(
'The position object in question was', position,
style: DiagnosticsTreeStyle.errorProperty)
]);
}
return true;
}());
if (value < position.pixels &&
position.pixels <= position.minScrollExtent) // underscroll
return value - position.pixels;
if (position.maxScrollExtent <= position.pixels &&
position.pixels < value) // overscroll
return value - position.pixels;
if (value < position.minScrollExtent &&
position.minScrollExtent < position.pixels) // hit top edge
return value - position.minScrollExtent;
if (position.pixels < position.maxScrollExtent &&
position.maxScrollExtent < value) // hit bottom edge
return value - position.maxScrollExtent;
return 0.0;
}
@override
Simulation createBallisticSimulation(
ScrollMetrics position, double velocity) {
final Tolerance tolerance = this.tolerance;
if (position.outOfRange) {
double end;
if (position.pixels > position.maxScrollExtent)
end = position.maxScrollExtent;
if (position.pixels < position.minScrollExtent)
end = position.minScrollExtent;
assert(end != null);
return ScrollSpringSimulation(
spring,
position.pixels,
end,
math.min(0.0, velocity),
tolerance: tolerance,
);
}
if (velocity.abs() < tolerance.velocity) return null;
if (velocity > 0.0 && position.pixels >= position.maxScrollExtent)
return null;
if (velocity < 0.0 && position.pixels <= position.minScrollExtent)
return null;
return ClampingScrollSimulation(
position: position.pixels,
velocity: velocity,
tolerance: tolerance,
);
}
}
调用的时候,只需要如下调用判断就可以
List<Widget> titleName = [];
List<Widget> pages = [];
TabController tabController;
int _currentTabIndex = 0; //当前页面的位置
@override
void initState() {
super.initState();
titleName.add('页面1');
titleName.add('页面2');
pages.add(MyPage());//自定义的界面
pages.add(MyPage());//自定义的界面
///具体数据的展示
tabController = TabController(
length: titleName.length, vsync: this, initialIndex: this.initialIndex);
///回调监听
tabController.addListener(() => _onTabChanged());
}
///页面切换进入
_onTabChanged() {
_currentTabIndex = tabController.index;
if (tabController.index.toDouble() == tabController.animation.value) {
///滑动和点击都会进去
} else {
///只有点击进入
}
}
///build中的一个widget,自己写到build中
///overCallBack的回调返回,处理滑动到边界
///当前页面只有两个,所以判断页面在第二页时,超出边界销毁当前widget
Expanded(
child: ExtendedTabBarView(
children: pages,
controller: tabController,
physics: CustomScrollPhysics(overCallBack: (offset) {
print('位移=' + offset.toString());
if (_currentTabIndex == 1) {
if (offset < -10) {
Navigator.pop(context);
}
}
}),
),
)