Flutter Scrollable 中ViewPort滚动原理

本文介绍了Flutter中的可滚动组件Scrollable和Viewport的基础概念,并通过代码示例展示了Sliver组件在滚动列表中的应用。文章深入到源码层面,分析了RenderViewport如何根据offset参数进行滚动布局,以及当offset变化时如何触发组件的重新布局。通过对RenderViewport的performLayout方法的解析,揭示了列表滚动的内部工作原理。
摘要由CSDN通过智能技术生成

关于Flutter Sliver组件内容可以参考下面这位博主博客,写的已经非常好了,这里就不再赘述。

38、Flutter之 可滚动组件简介_flutter 可滑动_风雨「83」的博客-CSDN博客

通过阅读上面的博客,我们已经知道了Scrollable和Viewport基础概念,接下来跟随作者一起,结合Flutter源码分析下ViewPort是如何滚动。

先看一个简单的demo

MaterialApp(
      home: Scaffold(
        body: Center(
            child: Container(
                height: 300,
                child: Scrollable(
                  viewportBuilder: (BuildContext context, ViewportOffset position) {
                    return Viewport(
                      offset: position,
                      slivers: [
                        SliverToBoxAdapter(
                          child: Container(
                            width: 100,
                            height: 100,
                            color: Colors.lightBlue,
                          ),
                        ),
                        SliverToBoxAdapter(
                          child: Container(
                            width: 100,
                            height: 100,
                            color: Colors.pink,
                          ),
                        ),
                        SliverToBoxAdapter(
                          child: Container(
                            width: 100,
                            height: 100,
                            color: Colors.green,
                          ),
                        ),
                        SliverToBoxAdapter(
                          child: Container(
                            width: 100,
                            height: 100,
                            color: Colors.black,
                          ),
                        ),
                        SliverToBoxAdapter(
                          child: Container(
                            width: 100,
                            height: 100,
                            color: Colors.red,
                          ),
                        )
                      ],
                    );
                  },
                ))),
      ),
    )

就是一个简单的滚动列表,可以上下滚动。

 上面是一个简单的滚动列表,当手指触摸屏幕时,内容随之发上移动(滚动)。

下面我们从Flutter刷新机制来梳理下列表里面的组件是在什么时机,由谁触发重新布局的(组件的位移就是重新布局的体现)。

class Viewport extends MultiChildRenderObjectWidget {
  
  ......

  @override
  RenderViewport createRenderObject(BuildContext context) {
    return RenderViewport(
      axisDirection: axisDirection,
      crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
      anchor: anchor,
      offset: offset,
      cacheExtent: cacheExtent,
      cacheExtentStyle: cacheExtentStyle,
      clipBehavior: clipBehavior,
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderViewport renderObject) {
    renderObject
      ..axisDirection = axisDirection
      ..crossAxisDirection = crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection)
      ..anchor = anchor
      ..offset = offset
      ..cacheExtent = cacheExtent
      ..cacheExtentStyle = cacheExtentStyle
      ..clipBehavior = clipBehavior;
  }

Viewport 继承MultiChildRenderObjectWidget,与之对应的RenderObject是RenderViewport,相关布局逻辑肯定是在RenderViewport 内部实现。

class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentData> {
  
  RenderViewport({
    super.axisDirection,
    required super.crossAxisDirection,
    required super.offset,
    double anchor = 0.0,
    List<RenderSliver>? children,
    RenderSliver? center,
    super.cacheExtent,
    super.cacheExtentStyle,
    super.clipBehavior,
  })

这里我们重点关注offset这个参数(其他参数不是本篇内容考虑的范围),这个参数在刚刚Viewport 里面赋值,这个参数是重点,后面会用到。

我们知道RenderViewport管理一个List<RenderSliver> 列表,列表内容滚动就是该父组件对子组件列表重新布局的结果,我们直接找到其performLayou方法

  @override
  void performLayout() {
    ......
    int count = 0;
    do {
      assert(offset.pixels != null);
      correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
      if (correction != 0.0) {
        offset.correctBy(correction);
      } else {
        if (offset.applyContentDimensions(
              math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
              math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
           )) {
          break;
        }
      }
      count += 1;
    } while (count < _maxLayoutCycles);
    ......
    }());
  }

performLayout方法中 

correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);

是我们关注的重点。

  double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
    ......
    // positive scroll offsets
    return layoutChildSequence(
      child: center,
      scrollOffset: math.max(0.0, -centerOffset),
      overlap: leadingNegativeChild == null ? math.min(0.0, -centerOffset) : 0.0,
      layoutOffset: centerOffset >= mainAxisExtent ? centerOffset: reverseDirectionRemainingPaintExtent,
      remainingPaintExtent: forwardDirectionRemainingPaintExtent,
      mainAxisExtent: mainAxisExtent,
      crossAxisExtent: crossAxisExtent,
      growthDirection: GrowthDirection.forward,
      advance: childAfter,
      remainingCacheExtent: forwardDirectionRemainingCacheExtent,
      cacheOrigin: clampDouble(centerOffset, -_calculatedCacheExtent!, 0.0),
    );
  }

我们找到了layoutChildSequence方法,从方法名就能知道,这个方法功能就是对子组件列表按照顺序重新布局的。

  @protected
  double layoutChildSequence({
    required RenderSliver? child,
    required double scrollOffset,
    required double overlap,
    required double layoutOffset,
    required double remainingPaintExtent,
    required double mainAxisExtent,
    required double crossAxisExtent,
    required GrowthDirection growthDirection,
    required RenderSliver? Function(RenderSliver child) advance,
    required double remainingCacheExtent,
    required double cacheOrigin,
  }) {
    ......

    while (child != null) {

    ......

      child.layout(SliverConstraints(
        axisDirection: axisDirection,
        growthDirection: growthDirection,
        userScrollDirection: adjustedUserScrollDirection,
        scrollOffset: sliverScrollOffset,
        precedingScrollExtent: precedingScrollExtent,
        overlap: maxPaintOffset - layoutOffset,
        remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset),
        crossAxisExtent: crossAxisExtent,
        crossAxisDirection: crossAxisDirection,
        viewportMainAxisExtent: mainAxisExtent,
        remainingCacheExtent: math.max(0.0, remainingCacheExtent + cacheExtentCorrection),
        cacheOrigin: correctedCacheOrigin,
      ), parentUsesSize: true);

    ......

      // move on to the next child
      child = advance(child);
    }

    // we made it without a correction, whee!
    return 0.0;
  }

 这个方法就是在不停循环获取下一个child,直到最后一个需要布局的child组件。

大体流程就是这样的,细心的你一定发现,上面没有说明组件是如何根据手指滑动滚动的,也就是如何触发RenderViewport 执行performLayout方法。

不要急,下面就来说明这一点。

还记得上面提到的offset这个参数吗?重头戏就是它。

  set offset(ViewportOffset value) {
    assert(value != null);
    if (value == _offset) {
      return;
    }
    if (attached) {
      _offset.removeListener(markNeedsLayout);
    }
    _offset = value;
    if (attached) {
      _offset.addListener(markNeedsLayout);
    }
    // We need to go through layout even if the new offset has the same pixels
    // value as the old offset so that we will apply our viewport and content
    // dimensions.
    markNeedsLayout();
  }

当上面我们给RenderViewport 配置offset参数是,offset是一个ChangeNotifer ,可以添加变化监听

abstract class ViewportOffset extends ChangeNotifier {
  /// Default constructor.
  ///
  /// Allows subclasses to construct this object directly.
  ViewportOffset();
......
}

当offset 只有的变量更新后,通知监听它的回调函数markNeedsLayout,这里就回到了Flutter组件渲染大流程里面了。

这个流程下来是不是非常巧妙,希望大家阅读完后,也能写出如此巧妙的架构。

Flutter ,`Scrollable.ensureVisible()` 是一个用于滚动到指定位置并显示其内容的方法,通常用于滚动视图(如 `ListView` 或 `ScrollView`)。默认情况下,这个过程并不包含动画效果。如果你想要添加平滑的动画过渡,可以自定义滚动行为,并结合一些动画库,比如 `AnimatedBuilder` 和 `Tween`。 下面是一个简单的例子,展示了如何给 `ensureVisible()` 添加一个淡入淡出(Fade-in/fade-out)动画: ```dart import 'package:flutter/material.dart'; import 'package:flutter/animation.dart'; class CustomScrollable extends StatefulWidget { @override _CustomScrollableState createState() => _CustomScrollableState(); } class _CustomScrollableState extends State<CustomScrollable> { final ScrollController _scrollController = ScrollController(); // 使用动画关键帧(Tween) final Animation<double> _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _scrollController.position, curve: Curves.easeInOut), ); void _ensureVisible(int index) { _scrollController.animateTo( index * 100, // 假设每个item高度为100 duration: Duration(milliseconds: 500), curve: Curves.easeInOutQuart, // 设置动画曲线 intraFrameCallback: (pos) { setState(() { _fadeAnimation.value = pos; // 根据滚动位置更新动画进度 }); }, ).whenComplete(() { // 淡入完成后的处理... }); } @override Widget build(BuildContext context) { return ListView.builder( controller: _scrollController, itemCount: 10, itemBuilder: (context, index) { return Container( height: 100, child: Center(child: Text('Item $index')), visibility: _fadeAnimation.value > 0.5 ? Visibility.visible : Visibility.hidden, ); }, itemBuilder: (context, index) { if (_scrollController.position.isFirst || _scrollController.position.isLast) return Container(); // 防止首尾滚动时出现闪烁 return GestureDetector( onTap: () { _ensureVisible(index); }, child: Container(), ); }, ); } } ``` 在这个示例,我们创建了一个 `_fadeAnimation` 来控制渐变透明度。当滚动到指定位置时,会根据 `_scrollController` 的位置更新 `_fadeAnimation`,使其从完全透明变为完全可见。`intraFrameCallback` 参数用于实时更新元素的可见状态。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值