09_flutter吸顶上推组件2.0

09_flutter吸顶上推组件2.0

之前实现过一个吸顶上推的效果,但是只能在ViewPort最顶部进行吸顶,并且在当前RenderObject中去干涉了其他RenderObject的绘制,这不是一个好的解决方案,本文将使用自定义自绘组件的方式,同时挂载header和sliver作为一个RenderObject的子级,统一处理header和sliver的布局和绘制,通过改变header的起始绘制位置,来实现吸顶上推效果。

一.先上效果
二.挂载header组件

定义Widget,通过header传入要吸顶的部分(RenderBox),通过sliver传入内容部分(RenderSliver)。

dart 复制代码
class SliverStickyBar extends RenderObjectWidget {
  final Widget? header;
  final Widget? sliver;

  const SliverStickyBar({
    super.key,
    this.header,
    this.sliver
  });

  @override
  RenderObjectElement createElement() {
    throw UnimplementedError();
  }

  @override
  RenderObject createRenderObject(BuildContext context) {
    throw UnimplementedError();
  }
}

///使用
CustomScrollView(
  slivers: [
    SliverPrototypeBuilder(
      prototype: Padding(
        padding: const EdgeInsets.only(bottom: 48),
        child: Image.asset(
          "assets/user.jpeg",
          fit: BoxFit.fitWidth,
        ),
      ),
      sliverBuilder: (context, mainAxisChildExtent, crossAxisChildExtent) {
        return SliverAppBar(
          pinned: true,
          collapsedHeight: kToolbarHeight,
          expandedHeight: mainAxisChildExtent - MediaQuery.paddingOf(context).top,
          centerTitle: true,
          title: Builder(builder: (context) {
            final FlexibleSpaceBarSettings? settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
            bool isScrolledUnder = settings?.isScrolledUnder ?? false;
            Color? titleColor = isScrolledUnder ? Theme.of(context).appBarTheme.foregroundColor:Colors.white;
            return Text(
              "首页",
              style: TextStyle(
                color: titleColor
              ),
            );
          }),
          backgroundColor: MaterialStateColor.resolveWith((states) {
            if(states.contains(MaterialState.scrolledUnder)) {
              return Theme.of(context).appBarTheme.backgroundColor ?? Colors.white;
            }
            return Colors.transparent;
          }),
          flexibleSpace: FlexibleSpaceBar(
            collapseMode: CollapseMode.pin,
            background: InkResponse(
              onTap: () {
                debugPrint("click");
              },
              child: Padding(
                padding: const EdgeInsets.only(bottom: 48),
                child: Image.asset(
                  "assets/user.jpeg",
                  fit: BoxFit.fitWidth,
                ),
              ),
            ),
          ),
          bottom: const TabBar(
            tabs: [
              Tab(
                text: "tab1",
              ),
              Tab(
                text: "tab2",
              )
            ],
          )
        );
      }
    ),

    SliverStickyBar(
      header: InkWell(
        onTap: () {
          debugPrint("Section1 click");
        },
        child: Container(
          height: 44,
          color: Colors.red,
          alignment: Alignment.centerLeft,
          padding: const EdgeInsets.symmetric(horizontal: 10),
          child: const Text("Section1", style: TextStyle(color: Colors.black),),
        ),
      ),
      sliver: SliverPadding(
        padding: const EdgeInsets.all(5),
        sliver: SliverList.list(
          children: List<Widget>.generate(10, (index) {
            return InkWell(
              onTap: () {
                debugPrint("click content item $index");
              },
              child: Container(
                height: 100,
                color: Colors.red,
                margin: const EdgeInsets.all(5),
                alignment: Alignment.center,
                child: Text(
                  "item $index",
                  style: const TextStyle(
                    color: Colors.white
                  ),
                ),
              ),
            );
          })
        ),
      ),
    ),

    SliverStickyBar(
      header: InkWell(
        onTap: () {
          debugPrint("Section2 click");
        },
        child: Container(
          height: 44,
          color: Colors.green,
          alignment: Alignment.centerLeft,
          padding: const EdgeInsets.symmetric(horizontal: 10),
          child: const Text("Section2", style: TextStyle(color: Colors.white),),
        ),
      ),
      sliver: SliverPadding(
        padding: const EdgeInsets.all(5),
        sliver: SliverList.list(
          children: List<Widget>.generate(6, (index) {
            return InkWell(
              onTap: () {
                debugPrint("click content item $index");
              },
              child: Container(
                height: 100,
                color: Colors.green,
                margin: const EdgeInsets.all(5),
                alignment: Alignment.center,
                child: Text(
                  "item $index",
                  style: const TextStyle(
                    color: Colors.white
                  ),
                ),
              ),
            );
          })
        ),
      ),
    ),

    SliverStickyBar(
      header: InkWell(
        onTap: () {
          debugPrint("Section3 click");
        },
        child: Container(
          height: 44,
          color: Colors.blue,
          alignment: Alignment.centerLeft,
          padding: const EdgeInsets.symmetric(horizontal: 10),
          child: const Text("Section3", style: TextStyle(color: Colors.white),),
        ),
      ),
      sliver: SliverPadding(
        padding: const EdgeInsets.all(5),
        sliver: SliverList.list(
          children: List<Widget>.generate(10, (index) {
            return InkWell(
              onTap: () {
                debugPrint("click content item $index");
              },
              child: Container(
                height: 100,
                color: Colors.blue,
                margin: const EdgeInsets.all(5),
                alignment: Alignment.center,
                child: Text(
                  "item $index",
                  style: const TextStyle(
                    color: Colors.white
                  ),
                ),
              ),
            );
          })
        ),
      )
    ),
  ],
)

定义RenderObject,提供一个header属性,由Element来初始化挂载headersliver对应的RenderObjectsliver对应的RenderObject会被挂载到child属性上。

dart 复制代码
class _RenderSliverStickyBar extends RenderSliver with RenderSliverHelpers {

  _RenderSliverStickyBar({
    RenderSliver? child,
    RenderObject? header
  }) {
    this.header = header as RenderBox?;
    this.child = child;
  }

  RenderBox? _header;
  RenderBox? get header => _header;
  set header(RenderBox? value) {
    if (_header != null) {
      dropChild(_header!);
    }
    _header = value;
    if (_header != null) {
      adoptChild(_header!);
    }
  }
  
  RenderSliver? _child;
  RenderSliver? get child => _child;
  set child(RenderSliver? value) {
    if (_child != null) {
      dropChild(_child!);
    }
    _child = value;
    if (_child != null) {
      adoptChild(_child!);
    }
  }
  
  double get headerExtent {
    if (header == null) {
      return 0.0;
    }
    assert(header!.hasSize);
    switch (constraints.axis) {
      case Axis.vertical:
        return header!.size.height;
      case Axis.horizontal:
        return header!.size.width;
    }
  }
  
  @override
  void setupParentData(RenderObject child) {
    if (child.parentData is! SliverPhysicalParentData) {
      child.parentData = SliverPhysicalParentData();
    }
  }

    @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    if (_header != null) {
      _header!.attach(owner);
    }
    if (_child != null) {
      _child!.attach(owner);
    }
  }

  @override
  void detach() {
    super.detach();
    if (_header != null) {
      _header!.detach();
    }
    if (_child != null) {
      _child!.detach();
    }
  }

  @override
  void redepthChildren() {
    if (_header != null) {
      redepthChild(_header!);
    }
    if (_child != null) {
      redepthChild(_child!);
    }
  }

  @override
  void visitChildren(RenderObjectVisitor visitor) {
    if (_header != null) {
      visitor(_header!);
    }
    if (_child != null) {
      visitor(_child!);
    }
  }
  
  @override
  void applyPaintTransform(covariant RenderObject child, Matrix4 transform) {
    final childParentData = child.parentData as SliverPhysicalParentData;
    childParentData.applyPaintTransform(transform);
  }

  @override
  void performLayout() {
    if(header == null && child == null) {
      geometry = SliverGeometry.zero;
      return;
    }
    
    if (header != null) {
      header!.layout(
        constraints.asBoxConstraints(),
        parentUsesSize: true,
      );
    }
  }
}

定义Element,初始化SliverStickyBar传入的headersliver对应的RenderObject

dart 复制代码
class _SliverStickyBarRenderObjectElement extends RenderObjectElement {
  _SliverStickyBarRenderObjectElement(SliverStickyBar super.widget);

  @override
  SliverStickyBar get widget => super.widget as SliverStickyBar;

  Element? _header;
  static final Object _headerSlot = Object();

  Element? _child;

  @override
  void visitChildren(ElementVisitor visitor) {
    if (_header != null) {
      visitor(_header!);
    }
    if (_child != null) {
      visitor(_child!);
    }
  }

  @override
  void forgetChild(Element child) {
    super.forgetChild(child);
    if (child == _header) {
      _header = null;
    }
    if (child == _child) {
      _child = null;
    }
  }

  @override
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    _header = updateChild(_header, widget.header, _headerSlot);
    _child = updateChild(_child, widget.sliver, null);
  }

  @override
  void update(SliverStickyBar newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _header = updateChild(_header, widget.header, _headerSlot);
    _child = updateChild(_child, widget.sliver, null);
  }

  @override
  void insertRenderObjectChild(RenderObject child, Object? slot) {
    final renderObject = this.renderObject as _RenderSliverStickyBar;
    if (slot == _headerSlot) {
      renderObject.header = child as RenderBox?;
    } else {
      renderObject.child = child as RenderSliver?;
    }
    assert(renderObject == this.renderObject);
  }

  @override
  void moveRenderObjectChild(RenderObject child, Object? slot, Object? newSlot) {
    assert(false);
  }

  @override
  void removeRenderObjectChild(RenderObject child, Object? slot) {
    final renderObject = this.renderObject as _RenderSliverStickyBar;
    if (renderObject.header == child) {
      renderObject.header = null;
    } else {
      renderObject.child = null;
    }
    assert(renderObject == this.renderObject);
  }
}

WidgetRenderObjectElement关联上

dart 复制代码
class SliverStickyBar extends RenderObjectWidget {
  final Widget? header;
  final Widget? sliver;

  const SliverStickyBar({
    super.key,
    this.header,
    this.sliver
  });

  @override
  RenderObjectElement createElement() => _SliverStickyBarRenderObjectElement(this);

  @override
  RenderObject createRenderObject(BuildContext context) => _RenderSliverStickyBar();
}

此时,header并不会被绘制出来,和我们直接使用SliverList没有任何区别,因此,之后我们需要修改_RenderSliverStickyBar重新对headersliver进行布局和绘制。

三.确定应该传给child的SliverConstraints,先不考虑吸顶
  • 确定scrollOffset

    scrollOffset是当前Sliver划出屏幕的偏移量大小,取值不能小于0。

    所以,传递给child约束的scrollOffset为:

    dart 复制代码
    math.max(0, constraints.scrollOffset - headerExtent);
  • 确定cacheOrigin

    Viewport会按照cachExtent的值分成两份与渲染区域,一份在上边,一份在下边

    cacheOrigin是当前Sliver在预渲染区域中的起点,取值范围[-Viewport.cachExtent, 0]。

    所以,传递给child约束的cacheOrigin为:

    dart 复制代码
    math.min(0, constraints.cacheOrigin + headerExtent);
  • 确定remainingPaintExtent

    remainingPaintExtent是当前Sliver在Viewport中的最大可绘制的大小,取值范围[0, constraints.viewportMainAxisExtent]。

    所以传递给child约束的remainingPaintExtent为:

    dart 复制代码
    double headerPaintExtent = calculatePaintOffset(constraints, from: 0.0, to: headerExtent);
    double remainingPaintExtent = constraints.remainingPaintExtent - headerPaintExtent;
  • 确定remainingCacheExtent

    remainingCacheExtent是当前Sliver预渲染区域大小,取值范围为[constraints.viewportMainAxisExtent, constraints.viewportMainAxisExtent + 2*Viewport.cachExtent]。

    所以传递给child约束的remainingCacheExtent为:

    dart 复制代码
    double headerCacheExtent = calculateCacheOffset(constraints, from: 0.0, to: headerExtent);
    double remainingCacheExtent = constraints.remainingCacheExtent - headerCacheExtent;
  • 由此可确定child的layout过程,得到child的布局信息childLayoutGeometry

    dart 复制代码
    void performLayout() {
      ...
    
      if(child != null) {
        final double headerPaintExtent = calculatePaintOffset(constraints, from: 0.0, to: headerExtent);
        final double headerCacheExtent = calculateCacheOffset(constraints, from: 0.0, to: headerExtent);
        child!.layout(constraints.copyWith(
          scrollOffset: math.max(0, constraints.scrollOffset - headerExtent),
          cacheOrigin: math.min(0, constraints.cacheOrigin + headerExtent),
          remainingPaintExtent: constraints.remainingPaintExtent - headerPaintExtent,
          remainingCacheExtent: constraints.remainingCacheExtent - headerCacheExtent
        ), parentUsesSize: true);
        final childLayoutGeometry = child!.geometry!;
        if (childLayoutGeometry.scrollOffsetCorrection != null) {
          geometry = SliverGeometry(
            scrollOffsetCorrection: childLayoutGeometry.scrollOffsetCorrection,
          );
          return;
        }
        ...
      }
      
      ...
    }
四.根据child的布局信息确定自身的布局信息,先不考虑吸顶
  • 确定scrollExtent

    scrollExtent是当前Sliver主轴方向上的固有高度,不会随滚动发生变化。

    所以自身布局信息的scrollExtent等于child布局信息的scrollExtent加上header在主轴方向上的尺寸,为:

    dart 复制代码
    childLayoutGeometry.scrollExtent + headerExtent;
  • 确定maxPaintExtent

    maxPaintExtent是当前Sliver在主轴方向上的最大可绘制大小。取值不能大于constraints.remainingPaintExtent。

    所以自身布局信息的maxPaintExtent等于child布局信息的maxPaintExtent加上header在主轴方向上的尺寸,为:

    dart 复制代码
    math.min(childLayoutGeometry.maxPaintExtent + headerExtent, constraints.remainingPaintExtent);
  • 确定paintExtent

    paintExtent是当前Sliver在可视区域内主轴方向上的绘制大小。滚动到顶部后,开始变小,直到等于 0,并且不能大于maxPaintExtent

    所以自身布局信息的paintExtent等于child布局信息的paintExtent加上header在可见区域内的绘制大小,为:

    dart 复制代码
    math.min(childLayoutGeometry.paintExtent + headerPaintExtent, maxPaintExtent);
  • 确定layoutExtent

    layoutExtent是当前Sliver的top到下一个Sliver的top之间的距离,取值不能大于paintExtent。

    所以自身布局信息的layoutExtent等于child布局信息的layoutExtent加上header在可见区域内的绘制大小,为:

    dart 复制代码
    math.min(childLayoutGeometry.layoutExtent + headerPaintExtent, paintExtent);
  • 确定cacheExtent

    cacheExtent是当前Sliver在预渲染区域中占用的大小,取值不能大于constraints.remainingCacheExtent

    所以自身布局信息的cacheExtent等于child布局信息的cacheExtent加上header在预渲染区域内占用的大小,为:

    dart 复制代码
    math.min(childLayoutGeometry.cacheExtent + headerCacheExtent, constraints.remainingCacheExtent);
  • 确定hitTestExtent

    hitTestExtent是当前Sliver点击范围在主轴方向的高度。

    所以自身布局信息的hitTestExtent等于child布局信息的hitTestExtent加上header在可见区域内的绘制大小,为:

    dart 复制代码
    childLayoutGeometry.hitTestExtent + headerPaintExtent;
  • 由此可确定自身的布局信息geometry

    dart 复制代码
    void performLayout() {
      ...
    
      final double headerPaintExtent = calculatePaintOffset(constraints, from: 0.0, to: headerExtent);
      final double headerCacheExtent = calculateCacheOffset(constraints, from: 0.0, to: headerExtent);
      if(child == null) {
        geometry = SliverGeometry(
          scrollExtent: headerExtent,
          maxPaintExtent: headerExtent,
          paintExtent: headerPaintExtent,
          cacheExtent: headerCacheExtent,
          hitTestExtent: headerPaintExtent
        );
      } else {
        ...
        final double maxPaintExtent = math.min(childLayoutGeometry.maxPaintExtent + headerExtent, constraints.remainingPaintExtent);
        final double paintExtent = math.min(childLayoutGeometry.paintExtent + headerPaintExtent, maxPaintExtent);
        geometry = SliverGeometry(
          scrollExtent: childLayoutGeometry.scrollExtent + headerExtent,
          maxPaintExtent: maxPaintExtent,
          paintExtent: paintExtent,
          layoutExtent: math.min(childLayoutGeometry.layoutExtent + headerPaintExtent, paintExtent),
          cacheExtent: math.min(childLayoutGeometry.cacheExtent + headerCacheExtent, constraints.remainingCacheExtent),
          hitTestExtent: childLayoutGeometry.hitTestExtent + headerPaintExtent,
          hasVisualOverflow: childLayoutGeometry.hasVisualOverflow,
        );
      }
      
      ...
    }
五.确定paintOffset并保存到 parentData 中,待paint时使用,先不考虑吸顶。
  • 确定child相对于ViewPort起始位置的paintOffset

    主轴方向为downright时,以down为例,最后的paintOffset需要在原有基础上加上headerPaintExtent:

    主轴方向为upleft时,以up为例,最后的paintOffset与原来的一样:

    dart 复制代码
    void setChildParentData(RenderObject child, SliverConstraints constraints, SliverGeometry geometry) {
      final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
      final double headerPaintExtent = calculatePaintOffset(constraints, from: 0.0, to: headerExtent);
      
      switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
        case AxisDirection.up:
          childParentData.paintOffset = Offset.zero;
        case AxisDirection.right:
          childParentData.paintOffset = Offset(headerPaintExtent, 0.0);
        case AxisDirection.down:
          childParentData.paintOffset = Offset(0.0, headerPaintExtent);
        case AxisDirection.left:
          childParentData.paintOffset = Offset.zero;
      }
    }
  • 确定header相对于ViewPort起始位置的paintOffset

    主轴方向为downright时,以down为例,当前Sliver滚动出ViewPort多少,header的绘制起始位置就在原有基础上减多少,所以为-constraints.scrollOffset

    主轴方向为upleft时,以up为例,header绘制的起始位置需要在原有基础上加上:paintExtent + scrollOffset - headerExtent

    dart 复制代码
    void setHeaderParentData(RenderObject header, SliverConstraints constraints, SliverGeometry geometry) {
      final SliverPhysicalParentData headerParentData = header.parentData! as SliverPhysicalParentData;
      switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
        case AxisDirection.up:
          headerParentData.paintOffset = Offset(0.0, geometry.paintExtent + constraints.scrollOffset - headerExtent);
        case AxisDirection.right:
          headerParentData.paintOffset = Offset(-constraints.scrollOffset, 0.0);
        case AxisDirection.down:
          headerParentData.paintOffset = Offset(0.0, -constraints.scrollOffset);
        case AxisDirection.left:
          headerParentData.paintOffset = Offset(geometry.paintExtent + constraints.scrollOffset - headerExtent, 0.0);
      }
    }
  • 最后调用setChildParentDatasetHeaderParentDatapaintOffset并保存到 parentData 中。

    dart 复制代码
    void performLayout() {
      ...
      final double headerPaintExtent = calculatePaintOffset(constraints, from: 0.0, to: headerExtent);
      final double headerCacheExtent = calculateCacheOffset(constraints, from: 0.0, to: headerExtent);
      if(child == null) {
        geometry = SliverGeometry(
          scrollExtent: headerExtent,
          maxPaintExtent: headerExtent,
          paintExtent: headerPaintExtent,
          cacheExtent: headerCacheExtent,
          hitTestExtent: headerPaintExtent
        );
      } else {
        ...
        setChildParentData(child!, constraints, geometry!);
      }
    
      if(header != null) {
        setHeaderParentData(header!, constraints, geometry!);
      }
    }
六.绘制childheader
  • 重写paint方法

    dart 复制代码
    @override
    void paint(PaintingContext context, Offset offset) {
      if (geometry!.visible) {
        if (child != null && child!.geometry!.visible) {
          final childParentData = child!.parentData as SliverPhysicalParentData;
          context.paintChild(child!, offset + childParentData.paintOffset);
        }
        if(header != null) {
          final headerParentData = header!.parentData as SliverPhysicalParentData;
          context.paintChild(header!, offset + headerParentData.paintOffset);
        }
      }
    }
七.实现吸顶上推。
  • 确定吸顶情况下,header的起始绘制位置。以AxisDirection.down为例:

    所以,吸顶情况下,header的起始位置需要在原有基础上加上:

    dart 复制代码
    math.min(constraints.overlap, geometry.scrollExtent - headerExtent - constraints.scrollOffset)

    所以有:

    dart 复制代码
    void setHeaderParentData(RenderObject header, SliverConstraints constraints, SliverGeometry geometry) {
      final SliverPhysicalParentData headerParentData = header.parentData! as SliverPhysicalParentData;
      ///不吸顶时
      // final double headerOrigin = -constraints.scrollOffset;
      final double headerOrigin = math.min(constraints.overlap, geometry.scrollExtent - headerExtent - constraints.scrollOffset);
      switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
        case AxisDirection.up:
          headerParentData.paintOffset = Offset(0.0, geometry.paintExtent - headerOrigin - headerExtent);
        case AxisDirection.right:
          headerParentData.paintOffset = Offset(headerOrigin, 0.0);
        case AxisDirection.down:
          headerParentData.paintOffset = Offset(0.0, headerOrigin);
        case AxisDirection.left:
          headerParentData.paintOffset = Offset(geometry.paintExtent - headerOrigin - headerExtent, 0.0);
      }
    }
  • 确定传给child约束的overlap

    overlap是上一个 sliver 覆盖当前 sliver 的大小

    dart 复制代码
    child!.layout(constraints.copyWith(
      ...
      overlap: math.min(headerExtent, constraints.scrollOffset) + constraints.overlap,
    ), parentUsesSize: true);
  • 确定自身布局信息的maxScrollObstructionExtent

    dart 复制代码
    geometry = SliverGeometry(
      ...
      maxScrollObstructionExtent: headerExtent
    );
  • 至此,吸顶以及上推效果完成。

八.修复headerchild的点击事件
dart 复制代码
@override
double childMainAxisPosition(covariant RenderObject child) {
  if (child == this.child) {
    return calculatePaintOffset(constraints, from: 0.0, to: headerExtent);
  }
  if(child == header) {
    final double headerOrigin = math.min(constraints.overlap, geometry!.scrollExtent - headerExtent - constraints.scrollOffset);
    return headerOrigin;
  }
  return 0.0;
}

@override
bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) {
  assert(geometry!.hitTestExtent > 0.0);
  if(header != null && mainAxisPosition <= (constraints.overlap + headerExtent)) {
    final didHitHeader = hitTestBoxChild(
      BoxHitTestResult.wrap(SliverHitTestResult.wrap(result)),
      header!,
      mainAxisPosition: mainAxisPosition,
      crossAxisPosition: crossAxisPosition,
    );

    return didHitHeader;
  } else if (child != null) {
    return child!.hitTest(
      result,
      mainAxisPosition: mainAxisPosition - childMainAxisPosition(child!),
      crossAxisPosition: crossAxisPosition
    );
  }
  return false;
}
九.完整代码
dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:math' as math;

class SliverStickyBar extends RenderObjectWidget {
  final Widget? header;
  final Widget? sliver;

  const SliverStickyBar({
    super.key,
    this.header,
    this.sliver
  });

  @override
  RenderObjectElement createElement() => _SliverStickyBarRenderObjectElement(this);

  @override
  RenderObject createRenderObject(BuildContext context) => _RenderSliverStickyBar();
}

class _SliverStickyBarRenderObjectElement extends RenderObjectElement {
  _SliverStickyBarRenderObjectElement(SliverStickyBar super.widget);

  @override
  SliverStickyBar get widget => super.widget as SliverStickyBar;

  Element? _header;
  static final Object _headerSlot = Object();

  Element? _child;

  @override
  void visitChildren(ElementVisitor visitor) {
    if (_header != null) {
      visitor(_header!);
    }
    if (_child != null) {
      visitor(_child!);
    }
  }

  @override
  void forgetChild(Element child) {
    super.forgetChild(child);
    if (child == _header) {
      _header = null;
    }
    if (child == _child) {
      _child = null;
    }
  }

  @override
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    _header = updateChild(_header, widget.header, _headerSlot);
    _child = updateChild(_child, widget.sliver, null);
  }

  @override
  void update(SliverStickyBar newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _header = updateChild(_header, widget.header, _headerSlot);
    _child = updateChild(_child, widget.sliver, null);
  }

  @override
  void insertRenderObjectChild(RenderObject child, Object? slot) {
    final renderObject = this.renderObject as _RenderSliverStickyBar;
    if (slot == _headerSlot) {
      renderObject.header = child as RenderBox?;
    } else {
      renderObject.child = child as RenderSliver?;
    }
    assert(renderObject == this.renderObject);
  }

  @override
  void moveRenderObjectChild(RenderObject child, Object? slot, Object? newSlot) {
    assert(false);
  }

  @override
  void removeRenderObjectChild(RenderObject child, Object? slot) {
    final renderObject = this.renderObject as _RenderSliverStickyBar;
    if (renderObject.header == child) {
      renderObject.header = null;
    } else {
      renderObject.child = null;
    }
    assert(renderObject == this.renderObject);
  }
}

class _RenderSliverStickyBar extends RenderSliver with RenderSliverHelpers {

  _RenderSliverStickyBar({
    RenderSliver? child,
    RenderObject? header
  }) {
    this.header = header as RenderBox?;
    this.child = child;
  }

  RenderBox? _header;
  RenderBox? get header => _header;
  set header(RenderBox? value) {
    if (_header != null) {
      dropChild(_header!);
    }
    _header = value;
    if (_header != null) {
      adoptChild(_header!);
    }
  }

  RenderSliver? _child;
  RenderSliver? get child => _child;
  set child(RenderSliver? value) {
    if (_child != null) {
      dropChild(_child!);
    }
    _child = value;
    if (_child != null) {
      adoptChild(_child!);
    }
  }

  double get headerExtent {
    if (header == null) {
      return 0.0;
    }
    assert(header!.hasSize);
    switch (constraints.axis) {
      case Axis.vertical:
        return header!.size.height;
      case Axis.horizontal:
        return header!.size.width;
    }
  }

  @override
  void setupParentData(RenderObject child) {
    if (child.parentData is! SliverPhysicalParentData) {
      child.parentData = SliverPhysicalParentData();
    }
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    if (_header != null) {
      _header!.attach(owner);
    }
    if (_child != null) {
      _child!.attach(owner);
    }
  }

  @override
  void detach() {
    super.detach();
    if (_header != null) {
      _header!.detach();
    }
    if (_child != null) {
      _child!.detach();
    }
  }

  @override
  void redepthChildren() {
    if (_header != null) {
      redepthChild(_header!);
    }
    if (_child != null) {
      redepthChild(_child!);
    }
  }

  @override
  void visitChildren(RenderObjectVisitor visitor) {
    if (_header != null) {
      visitor(_header!);
    }
    if (_child != null) {
      visitor(_child!);
    }
  }

  @override
  void applyPaintTransform(covariant RenderObject child, Matrix4 transform) {
    final childParentData = child.parentData as SliverPhysicalParentData;
    childParentData.applyPaintTransform(transform);
  }

  @override
  void performLayout() {
    if(header == null && child == null) {
      geometry = SliverGeometry.zero;
      return;
    }

    if (header != null) {
      header!.layout(
        constraints.asBoxConstraints(),
        parentUsesSize: true,
      );
    }

    final double headerPaintExtent = calculatePaintOffset(constraints, from: 0.0, to: headerExtent);
    final double headerCacheExtent = calculateCacheOffset(constraints, from: 0.0, to: headerExtent);
    if(child == null) {
      geometry = SliverGeometry(
          scrollExtent: headerExtent,
          maxPaintExtent: headerExtent,
          paintExtent: headerPaintExtent,
          cacheExtent: headerCacheExtent,
          hitTestExtent: headerPaintExtent
      );
    } else {
      child!.layout(constraints.copyWith(
        scrollOffset: math.max(0, constraints.scrollOffset - headerExtent),
        cacheOrigin: math.min(0, constraints.cacheOrigin + headerExtent),
        remainingPaintExtent: constraints.remainingPaintExtent - headerPaintExtent,
        remainingCacheExtent: constraints.remainingCacheExtent - headerCacheExtent,
        overlap: math.min(headerExtent, constraints.scrollOffset) + constraints.overlap,
      ), parentUsesSize: true);
      final childLayoutGeometry = child!.geometry!;
      if (childLayoutGeometry.scrollOffsetCorrection != null) {
        geometry = SliverGeometry(
          scrollOffsetCorrection: childLayoutGeometry.scrollOffsetCorrection,
        );
        return;
      }

      final double maxPaintExtent = math.min(childLayoutGeometry.maxPaintExtent + headerExtent, constraints.remainingPaintExtent);
      final double paintExtent = math.min(childLayoutGeometry.paintExtent + headerPaintExtent, maxPaintExtent);
      geometry = SliverGeometry(
          scrollExtent: childLayoutGeometry.scrollExtent + headerExtent,
          maxPaintExtent: maxPaintExtent,
          paintExtent: paintExtent,
          layoutExtent: math.min(childLayoutGeometry.layoutExtent + headerPaintExtent, paintExtent),
          cacheExtent: math.min(childLayoutGeometry.cacheExtent + headerCacheExtent, constraints.remainingCacheExtent),
          hitTestExtent: childLayoutGeometry.hitTestExtent + headerPaintExtent,
          hasVisualOverflow: childLayoutGeometry.hasVisualOverflow,
          maxScrollObstructionExtent: headerExtent
      );

      setChildParentData(child!, constraints, geometry!);
    }

    if(header != null) {
      setHeaderParentData(header!, constraints, geometry!);
    }
  }

  void setChildParentData(RenderObject child, SliverConstraints constraints, SliverGeometry geometry) {
    final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
    final double headerPaintExtent = calculatePaintOffset(constraints, from: 0.0, to: headerExtent);

    switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
      case AxisDirection.up:
        childParentData.paintOffset = Offset.zero;
      case AxisDirection.right:
        childParentData.paintOffset = Offset(headerPaintExtent, 0.0);
      case AxisDirection.down:
        childParentData.paintOffset = Offset(0.0, headerPaintExtent);
      case AxisDirection.left:
        childParentData.paintOffset = Offset.zero;
    }
  }

  void setHeaderParentData(RenderObject header, SliverConstraints constraints, SliverGeometry geometry) {
    final SliverPhysicalParentData headerParentData = header.parentData! as SliverPhysicalParentData;
    ///不吸顶时
    // final double headerOrigin = -constraints.scrollOffset;
    final double headerOrigin = math.min(constraints.overlap, geometry.scrollExtent - headerExtent - constraints.scrollOffset);
    switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
      case AxisDirection.up:
        headerParentData.paintOffset = Offset(0.0, geometry.paintExtent - headerOrigin - headerExtent);
      case AxisDirection.right:
        headerParentData.paintOffset = Offset(headerOrigin, 0.0);
      case AxisDirection.down:
        headerParentData.paintOffset = Offset(0.0, headerOrigin);
      case AxisDirection.left:
        headerParentData.paintOffset = Offset(geometry.paintExtent - headerOrigin - headerExtent, 0.0);
    }
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (geometry!.visible) {
      if (child != null && child!.geometry!.visible) {
        final childParentData = child!.parentData as SliverPhysicalParentData;
        context.paintChild(child!, offset + childParentData.paintOffset);
      }
      if(header != null) {
        final headerParentData = header!.parentData as SliverPhysicalParentData;
        context.paintChild(header!, offset + headerParentData.paintOffset);
      }
    }
  }

  @override
  double childMainAxisPosition(covariant RenderObject child) {
    if (child == this.child) {
      return calculatePaintOffset(constraints, from: 0.0, to: headerExtent);
    }
    if(child == header) {
      final double headerOrigin = math.min(constraints.overlap, geometry!.scrollExtent - headerExtent - constraints.scrollOffset);
      return headerOrigin;
    }
    return 0.0;
  }

  @override
  bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) {
    assert(geometry!.hitTestExtent > 0.0);
    if(header != null && mainAxisPosition <= (constraints.overlap + headerExtent)) {
      final didHitHeader = hitTestBoxChild(
        BoxHitTestResult.wrap(SliverHitTestResult.wrap(result)),
        header!,
        mainAxisPosition: mainAxisPosition,
        crossAxisPosition: crossAxisPosition,
      );

      return didHitHeader;
    } else if (child != null) {
      return child!.hitTest(
        result,
        mainAxisPosition: mainAxisPosition - childMainAxisPosition(child!),
        crossAxisPosition: crossAxisPosition
      );
    }
    return false;
  }
}
相关推荐
YJlio15 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
冬奇Lab16 小时前
Android系统启动流程深度解析:从Bootloader到Zygote的完整旅程
android·源码阅读
微祎_17 小时前
Flutter for OpenHarmony:链迹 - 基于Flutter的会话级快速链接板极简实现方案
flutter
微祎_17 小时前
Flutter for OpenHarmony:魔方计时器开发实战 - 基于Flutter的专业番茄工作法应用实现与交互设计
flutter·交互
泓博17 小时前
Android中仿照View selector自定义Compose Button
android·vue.js·elementui
zhangphil18 小时前
Android性能分析中trace上到的postAndWait
android
十里-19 小时前
vue2的web项目打包成安卓apk包
android·前端
p***199419 小时前
MySQL——内置函数
android·数据库·mysql
兆子龙20 小时前
我成了🤡, 因为不想看广告,花了40美元自己写了个鸡肋挂机脚本
android·javascript
儿歌八万首21 小时前
Android 全局监听神器:registerActivityLifecycleCallbacks 解析
android·kotlin·activity