如何在 Flutter 中实现“滚动小部件内的小部件的子部件,其作用类似于粘性标题”?

Bar*_* S. 5 dart dart-pub flutter

我正在尝试找到一种方法来实现一种功能,在该功能中,在水平可滚动列表中,有一些我称之为 P 的小部件(在图中表示为 P1、P2 和 P3)及其子部件 C(分别表示为 C1、C2 和 C3)。当用户水平滚动列表时,我希望 P 内的 C 充当粘性标题,直到它们到达其父级的边界。

如果描述和图表还不够,我很抱歉,我会尽力澄清任何不清楚的地方。

问题示意图

当我正在考虑一种方法来实现这一点时,我似乎找不到合理的解决方案。另外,如果有一个软件包可以帮助解决这个问题,我将非常感谢任何建议。

Say*_*d J 4

我不确定你的照片,但这也许是你想要的?

在此输入图像描述

在此输入图像描述

我们的工具:

  1. BuildOwners -> 在重建之前测量小部件的大小,
  2. NotificationListeners -> 基于 ScrollNotification 触发重建。我使用有状态的 Widget,但您可以将其调整为 ValueNotifier 并使用 ValueListenableBuilder 构建贴纸。
  3. ListView.Builder -> 实际上你可以用任何类型的 Scrollable 替换它,我们只需要监听滚动事件。

它是如何运作的?

很简单:我们需要知道Pdx Offset,检查Coffset 是否小于P,然后使用该值来调整inPositioned的x 。并将其钳制为最大值CStack(P.width)

double _calculateStickerXPosition(
      {required double px, required double cx, required double cw}) {
    if (cx < px) {
      return widget.stickerHorizontalPadding + (px - cx).clamp(0.0, cw - (widget.stickerHorizontalPadding*2));
    }

    return widget.stickerHorizontalPadding;
  }
Run Code Online (Sandbox Code Playgroud)

完整代码:

主.dart:

import 'dart:ui';
import 'package:flutter/material.dart';

import 'scrollable_sticker.dart';




void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {

    return  MaterialApp(
      // i use chrome to test it, so igrone this
      scrollBehavior: const MaterialScrollBehavior().copyWith(
        dragDevices: {
          PointerDeviceKind.mouse,
          PointerDeviceKind.touch,
          PointerDeviceKind.stylus,
          PointerDeviceKind.unknown
        },
      ),
      home: const MyWidget(),
    );
  }
}

class MyWidget extends StatelessWidget {
  const MyWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.symmetric(vertical: 20.0),
        child: ScrollableSticker(
            children: List.generate(10, (index) => Container(
              width: 500,
              decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(10.0),
                  border: Border.all(color: Colors.orange)),
              child: const Padding(
                padding: EdgeInsets.symmetric(vertical: 50.0, horizontal: 50.0),
                child: Text(
                  "P1",
                  textDirection: TextDirection.ltr,
                ),
              ),
            )),
            stickerBuilder: (index) => Container(
              decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(10), color: Colors.red),
              child: Padding(
                padding: const EdgeInsets.all(10.0),
                child: Text(
                  'C$index',
                ),
              ),
            )),
      ),
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

可滚动_sticker.dart :

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class ScrollableSticker extends StatefulWidget {
  final List<Widget> children;
  final Widget Function(int index) stickerBuilder;
  final double stickerHorizontalPadding;
  const ScrollableSticker(
      {Key? key,
      required this.children,
      required this.stickerBuilder,
      this.stickerHorizontalPadding = 10.0})
      : super(key: key);

  @override
  State<ScrollableSticker> createState() => _ScrollableStickerState();
}

class _ScrollableStickerState extends State<ScrollableSticker> {
  late List<GlobalKey> _keys;
  late GlobalKey _parentKey;

  @override
  void initState() {
    super.initState();
    _keys = List.generate(widget.children.length, (index) => GlobalKey());
    _parentKey = GlobalKey();
  }

  @override
  Widget build(BuildContext context) {
    return NotificationListener<ScrollNotification>(
      onNotification: (sc) {
        setState(() {});
        return true;
      },
      child: ListView.builder(
        key: _parentKey,
        scrollDirection: Axis.horizontal,
        itemCount: widget.children.length,
        itemBuilder: (context, index) {
          final itemSize = measureWidget(Directionality(
              textDirection: TextDirection.ltr, child: widget.children[index]));
          final stickerSize = measureWidget(Directionality(
              textDirection: TextDirection.ltr,
              child: widget.stickerBuilder(index)));
          final BuildContext? itemContext = _keys[index].currentContext;
          double x = widget.stickerHorizontalPadding;
          if (itemContext != null) {
            final pcontext = _parentKey.currentContext;
            Offset? pOffset;
            if (pcontext != null) {
              RenderObject? obj = pcontext.findRenderObject();
              if (obj != null) {
                final prb = obj as RenderBox;
                pOffset = prb.localToGlobal(Offset.zero);
              }
            }
            final obj = itemContext.findRenderObject();
            if (obj != null) {
              final rb = obj as RenderBox;
              final cx = rb.localToGlobal(pOffset ?? Offset.zero).dx;
              x = _calculateStickerXPosition(
                  px: pOffset != null ? pOffset.dx : 0.0,
                  cx: cx,
                  cw: (itemSize.width - stickerSize.width));
            }
          }
          return SizedBox(
            key: _keys[index],
            height: itemSize.height,
            width: itemSize.width,
            child: Stack(
              children: [
                widget.children[index],
                Positioned(
                    top: itemSize.height / 2,
                    left: x,
                    child: FractionalTranslation(
                        translation: const Offset(0.0, -0.5),
                        child: widget.stickerBuilder(index)))
              ],
            ),
          );
        },
      ),
    );
  }

  double _calculateStickerXPosition(
      {required double px, required double cx, required double cw}) {
    if (cx < px) {
      return widget.stickerHorizontalPadding +
          (px - cx).clamp(0.0, cw - (widget.stickerHorizontalPadding * 2));
    }

    return widget.stickerHorizontalPadding;
  }
}

Size measureWidget(Widget widget) {
  final PipelineOwner pipelineOwner = PipelineOwner();
  final MeasurementView rootView = pipelineOwner.rootNode = MeasurementView();
  final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager());
  final RenderObjectToWidgetElement<RenderBox> element =
      RenderObjectToWidgetAdapter<RenderBox>(
    container: rootView,
    debugShortDescription: '[root]',
    child: widget,
  ).attachToRenderTree(buildOwner);
  try {
    rootView.scheduleInitialLayout();
    pipelineOwner.flushLayout();
    return rootView.size;
  } finally {
    // Clean up.
    element.update(RenderObjectToWidgetAdapter<RenderBox>(container: rootView));
    buildOwner.finalizeTree();
  }
}

class MeasurementView extends RenderBox
    with RenderObjectWithChildMixin<RenderBox> {
  @override
  void performLayout() {
    assert(child != null);
    child!.layout(const BoxConstraints(), parentUsesSize: true);
    size = child!.size;
  }

  @override
  void debugAssertDoesMeetConstraints() => true;
}
Run Code Online (Sandbox Code Playgroud)