自定义CustomScrollView实现页面的滚动联动

avatar
@智云健康

因为NestedScrollView自定义下拉刷新组件有问题,上自身的一些问题,不能达到目前的业务要求,所以参考NestedScrollView 自己实现了一个.来实现scrollView内ListView的滚动联动和下拉刷新自定义的一个组件. 先看效果

image.png

image.png

image.png

image.png 主要实现思路共大家参考,大家也可以直接下载来使用

添加滚动协调类customScrollCoordinator,包含主视图、子视图scrollController,和主视图Position的引用. 在滚动时自定义Position跟据自身状态来调用协调类applyUserOffset方法,来实现主页面滚动和子页面滚动的响应者切换. 具体实现方法: 自定义customScrollController类,加入协调类引用,

class CustomScrollController extends ScrollController {
  CustomScrollController(
    this.coordinator, {
    double initialScrollOffset = 0.0,

    /// 每次滚动完成时,请使用 [PageStorage] 保存当前滚动 [offset] ,如果重新创建了此
    /// 控制器的可滚动内容,则将其还原。
    ///
    /// 如果将此属性设置为false,则永远不会保存滚动偏移量,
    /// 并且始终使用 [initialScrollOffset] 来初始化滚动偏移量。如果为 true(默认值),
    /// 则第一次创建控制器的可滚动对象时将使用初始滚动偏移量,因为尚无要还原的滚动偏移量。
    /// 随后,将恢复保存的偏移,并且忽略[initialScrollOffset]。
    ///
    /// 也可以看看:
    ///  * [PageStorageKey],当同一路径中出现多个滚动条时,应使用 [PageStorageKey]
    ///    来区分用于保存滚动偏移量的 [PageStorage] 位置。
    bool keepScrollOffset = true,

    /// [toString] 输出中使用的标签。帮助在调试输出中标识滚动控制器实例。
    String debugLabel,
  })  : assert(initialScrollOffset != null),
        assert(keepScrollOffset != null),
        _initialScrollOffset = initialScrollOffset,
        super(keepScrollOffset: keepScrollOffset, debugLabel: debugLabel);

  final CustomScrollCoordinator coordinator;

主要就是为了添加coordinator引用 然后将方法createScrollPosition方法重写,改为返回自定义的customScrollPosition类,并传入协调类

@override
CustomScrollPosition createScrollPosition(
  ScrollPhysics physics,
  ScrollContext context,
  ScrollPosition oldPosition,
) {
  return CustomScrollPosition(
    coordinator: coordinator,
    physics: physics,
    context: context,
    initialPixels: initialScrollOffset,
    keepScrollOffset: keepScrollOffset,
    oldPosition: oldPosition,
    debugLabel: debugLabel,
  );
}

自定义scrollController完成,该类主要就是为了关联协调类和自定义position 第二部: 自定义协调类 customScrollCoordinator 该类管理scrollController,主要进行滚动的切换和滑动惯性的延续


///获取主页面滑动控制器
CustomScrollController mainScrollController([double initialOffset = 0.0]) {
  assert(initialOffset != null, initialOffset >= 0);
  _customScrollController = CustomScrollController(this, debugLabel: debugLabel, initialScrollOffset: initialOffset);
  return _customScrollController;
}

///创建并获取子滑动控制器
CustomScrollController newChildScrollController([String debugLabel = 'innerSliding']) {
  return CustomScrollController(this, debugLabel: debugLabel);
}

切换手势的响应方法,进行滚动响应的切换

/// 子部件滑动数据协调
/// [delta]滑动距离
/// [userScrollDirection]用户滑动方向
/// [position]被滑动的子部件的位置信息
void applyUserOffset(double delta,
    [ScrollDirection userScrollDirection, CustomScrollPosition position]) {
  if (userScrollDirection == ScrollDirection.reverse) {
    /// 当用户滑动方向是向上滑动
    updateUserScrollDirection(_customScrollPosition, userScrollDirection);
    final innerDelta = _customScrollPosition.applyClampedDragUpdate(delta);
    if (innerDelta != 0.0) {
      updateUserScrollDirection(position, userScrollDirection);
      position.applyFullDragUpdate(innerDelta);
    }
  } else {
    /// 当用户滑动方向是向下滑动
    // print('用户下拉 position${position.pixels}');
    // print('_customScrollController.position.maxScrollExtent :${_customScrollController.position.maxScrollExtent}');
    // print('_customScrollController.position.pixels :${_customScrollController.position.pixels}');
    bool listviewAtTop = position.pixels == 0;
    if (listviewAtTop) {
      ///listview到顶了,滚动main
      updateUserScrollDirection(position, userScrollDirection);
      final outerDelta = position.applyClampedDragUpdate(delta);
      if (outerDelta != 0.0) {
        updateUserScrollDirection(_customScrollPosition, userScrollDirection);
        _customScrollPosition.applyFullDragUpdate(outerDelta);
      }
    } else {
      ///滚动listview
    }
  }
}

惯性的处理:

/// 以特定的速度开始一个物理驱动的模拟,该模拟确定 [pixels] 位置。
///
/// 此方法遵从 [ScrollPhysics.createBallisticSimulation],通常在当前位置超出范围时
/// 提供滑动模拟,而在当前位置超出范围但具有非零速度时提供摩擦模拟。
///
/// 速度应以 逻辑像素/秒 为单位。
void goBallistic(double velocity,{bool scrollInner = false}){
  if (scrollInner) {
    //需要滚动内部
    // print('内部 收到滚动:$velocity 子scroll:${currentScrollController.position}');
    currentScrollController.position.goBallistic(velocity);
  } else {
    //需要滚动外部
    // print('外部 收到滚动:$velocity 子scroll:${currentScrollController.position}');
    _customScrollPosition.goBallistic(velocity);
  }
}

至此,协调类的主要方法完成 3,接下来实现自定义Position类 该类为主要数据获得的类,主滚动和子滚动的滚动事件都有该类处理,然后交给协调类进行滚动事件的切换. 首先需要把协调类引用进来

class CustomScrollPosition extends ScrollPosition
    implements ScrollActivityDelegate {
  CustomScrollPosition({
    @required ScrollPhysics physics,
    @required ScrollContext context,
    double initialPixels = 0.0,
    bool keepScrollOffset = true,
    ScrollPosition oldPosition,
    String debugLabel,
    @required this.coordinator,
  }

然后处理滑动的滚动事件,是交给协调类切换还是自身滚动由此方法处理

/// 当手指滑动时,该方法会获取到滑动距离。
///
/// [delta] 滑动距离,正增量表示下滑,负增量向上滑。
///
/// 我们需要把子部件的滑动数据交给协调器处理,主部件无干扰。
@override
void applyUserOffset(double delta) {
  coordinator.coordinatorScrolling();
  final ScrollDirection userScrollDirection = delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse;
  bool listviewAtTop = pixels == 0;
  if (debugLabel != coordinator.debugLabel) {
    //内部滚动
    if(userScrollDirection == ScrollDirection.reverse){
      //用户向上滑动
      return coordinator.applyUserOffset(delta, userScrollDirection, this);
    }else{
      //用户向下滑动
      if(listviewAtTop){
        return coordinator.applyUserOffset(delta, userScrollDirection, this);
      }
    }
  }
  updateUserScrollDirection(userScrollDirection);
  setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta));
}

还有一个滚动惯性的处理,该方法控制滚动的惯性滚动事件由谁处理

/// 以特定的速度开始一个物理驱动的模拟,该模拟确定 [pixels] 位置。
/// 此方法遵从 [ScrollPhysics.createBallisticSimulation],该方法通常在当前位置超出
/// 范围时提供滑动模拟,而在当前位置超出范围但具有非零速度时提供摩擦模拟。
/// 速度应以逻辑像素/秒为单位。
@override
void goBallistic(double velocity, [bool fromCoordinator = false]) {
  bool mainAtTop = coordinator.mainScrollViewAtTop();
  bool listviewAtTop = pixels == 0;
  if (debugLabel == coordinator.debugLabel) {
    //主拖动
  } else {
    //子拖动
    if (velocity >= 0) {
      //上拉
      if (!mainAtTop) {
        print('1');
        goIdle();
        return coordinator.goBallistic(velocity);
      }
    } else {
      //下拉
      if (listviewAtTop) {
        print('2');
        goIdle();
        return coordinator.goBallistic(velocity);
      }
    }
  }
  assert(pixels != null);
  final Simulation simulation = physics.createBallisticSimulation(this, velocity);
  if (simulation != null) {
    beginActivity(BallisticScrollActivity(this, simulation, context.vsync));
  } else {
    goIdle();
  }
}

对应的有个处理惯性速度的方法,比如速度很快的到边界了,接下来的滚动由谁继承和响应

@override
void goIdle() {
  double velocity = 0;
  if(activity != null) {
    if (debugLabel == coordinator.debugLabel) {
      //主滚动结束,但是有剩余速度
      if (activity.velocity != 0) {
        velocity = activity.velocity;
      }
    } else {
      if (activity.velocity != 0) {
        velocity = activity.velocity;
      }
    }
  }
  beginActivity(IdleScrollActivity(this));
  if(velocity != 0){
    if(debugLabel == coordinator.debugLabel){
      // print('主list滚动结束-剩余转子list滚动:$velocity');
      coordinator.goBallistic(velocity,scrollInner: true);
    }else{
      // print('子list滚动结束-剩余转主list滚动');
      coordinator.goBallistic(velocity);
    }
  }
}

主要实现的思路和方法已经完成. 该方式可以完美的配合

EasyRefresh

的下拉刷新等操作 配合

CustomScrollView

实现滚动联动等多种效果 使用方法:

CustomScrollCoordinator _scrollCoordinator = CustomScrollCoordinator();
CustomScrollController _mainScrollController;
_mainScrollController = _scrollCoordinator.mainScrollController();
_scrollCoordinator.coordinatorScrolling = (){
  floatingBallKey.currentState.scrollEvent();
};
CustomScrollView(
  controller: _mainScrollController,
  physics: physics,
  slivers: []

如果有多个子滚动需要在listView切换时指定当前滚动的controller

_scrollCoordinator.currentScrollController = currentInnerScrollController;

具体实现详情可下载 download.csdn.net/download/an…