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中对滑动到边界添加效果。

flutter ios阻止返回 flutter滑动返回_flutter

 

而在BouncingScrollPhysics(类似IOS回弹效果)的源码中,我们看到,applyPhysicsToUserOffset被修改了,因为ios的回弹需要对整个页面进行位移,所以需要改变widget的位置,但是applyBoundaryConditions没有变动,因为ios的回弹不需要类似波纹效果等。

flutter ios阻止返回 flutter滑动返回_flutter_02

flutter ios阻止返回 flutter滑动返回_经验分享_03

 

二、重写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);
        }
      }
    }),
  ),
)