Skip to content

当列表数据很少时isShrinkWrap为true时展开折叠内容isShrinkWrap变为false,standby就无法固定位置了 #141

@LiGGz

Description

@LiGGz

问题描述:

版本: 1.26.1

import` 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:scrollview_observer/scrollview_observer.dart';

class MessageItem {
  final String title;
  bool isExpand;
  final String content;

  MessageItem(
      {required this.title, required this.content, this.isExpand = false});
}

class ScrollListPage extends StatefulWidget {
  const ScrollListPage({super.key});

  @override
  State<ScrollListPage> createState() => _scrollListPage();
}

class _scrollListPage extends State<ScrollListPage>
    with WidgetsBindingObserver {
  final ScrollController scrollController = ScrollController();
  late ListObserverController observerController;
  late ChatScrollObserver chatObserver;

  List<MessageItem> messages = [
    MessageItem(title: "这是第二条内容", content: "这是第二条内容", isExpand: false),
    MessageItem(
      isExpand: false,
      title: '''这条消息第一个展开内容很多长展开有问题,位置不能固定''',
      content:
          '''Identify the cause of the user's headache and provide possible solutions or recommendations.</summary>[Query vital signs data to check for any abnormalities that could be associated with headaches.]:- bodyTemperatures:Data is not available for 2025-01-06 - 2025-01-13. Here is the last valid data on 2024-11-18 17:23:00: 37.78 °CYou can kindly remind the user to synchronize the data.- restingHeartRates:Data is not available for 2025-01-06 - 2025-01-13. Here is the last valid data on 2024-11-18 17:25:00: 30.0 BPMYou can kindly remind the user to synchronize the data.- bloodGlucoses:Data is not available for 2025-01-06 - 2025-01-13. Here is the last valid data on 2024-11-18 17:23:00: 100.0 mg/dLYou can kindly remind the user to synchronize the data.- systolicPressures:Data is not available for 2025-01-06 - 2025-01-13. Here is the last valid data on 2024-11-18 17:24:00: 100.0 mmHgYou can kindly remind the user to synchronize the data.- diastolicPressures:Data is not available for 2025-01-06 - 2025-01-13. Here is the last valid data on 2024-11-18 17:24:00: 60.0 mmHgYou can kindly remind the user to synchronize the data.- oxygenSaturations:Data is not available for 2025-01-06 - 2025-01-13. Here is the last valid data on 2024-11-18 17:24:00: 1.0 %You can kindly remind the user to synchronize the data.- hrvDatas:Data is not available for 2025-01-06 - 2025-01-13. Here is the last valid data on 2024-11-18 17:25:00: 30.0 msYou can kindly remind the user to synchronize the data.- heartRates:Data is not available for 2025-01-06 - 2025-01-13. Here is the last valid data on 2024-11-18 20:22:00: 101.0 BPMYou can kindly remind the user to synchronize the data.- respiratoryRates:Data is not available for 2025-01-06 - 2025-01-13. Here is the last valid data on 2024-11-18 17:20:00: 12.0 BPMYou can kindly remind the user to synchronize the data.Reference: Apple Health[Retrieve heart-related metrics to identify any cardiovascular factors contributing to headaches.]:- hrvDatas:Data is not available for 2025-01-06 - 2025-01-13. Here is the last valid data on 2024-11-18 17:25:00: 30.0 msYou can kindly remind the user to synchronize the data.- heartRates:Data is not available for 2025-01-06 - 2025-01-13. Here is the last valid data on 2024-11-18 20:22:00: 101.0 BPMYou can kindly remind the user to synchronize the data.- restingHeartRates:Data is not available for 2025-01-06 - 2025-01-13. Here is the last valid data on 2024-11-18 17:25:00: 30.0 BPMYou can kindly remind the user to synchronize the data.Reference: Apple Health[Retrieve respiratory health metrics to assess any breathing-related issues linked to headaches.]:- respiratoryRates:Data is not available for 2025-01-06 - 2025-01-13. Here is the last valid data on 2024-11-18 17:20:00: 12.0 BPMYou can kindly remind the user to synchronize the data.- oxygenSaturations:Data is not available for 2025-01-06 - 2025-01-13. Here is the last valid data on 2024-11-18 17:24:00: 1.0 %You can kindly remind the user to synchronize the data.Reference: Apple Health[Query lab results for indicators related to metabolic or systemic conditions that might cause headaches.]:No related information was found. Please verify if the relevant data has been uploaded or synchronized''',
    ),
  ];

  @override
  void initState() {
    super.initState();
    initChatObserver();
  }

  @override
  void dispose() {
    observerController.controller?.dispose();
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeMetrics() {
    super.didChangeMetrics();

    chatObserver.observeSwitchShrinkWrap();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
          Widget resultWidget = EasyRefresh.builder(
            footer: const ClassicFooter(
              position: IndicatorPosition.above,
              infiniteOffset: null,
            ),
            onLoad: () async {
              await Future.delayed(const Duration(seconds: 2));
            },
            childBuilder: (context, physics) {
              var scrollViewPhysics = physics.applyTo(
                ChatObserverClampingScrollPhysics(
                  observer: chatObserver,
                ),
              );

              // 新增的代码
              var listViewPhysics =
                  physics.applyTo(ChatObserverClampingScrollPhysics(
                observer: chatObserver,
              ));
              if (chatObserver.isShrinkWrap) {
                listViewPhysics = const NeverScrollableScrollPhysics()
                    .applyTo(listViewPhysics);
              }

              Widget resultWidget = ListView.separated(
                physics: listViewPhysics,
                padding: const EdgeInsets.only(
                  left: 10,
                  right: 10,
                  top: 15,
                  bottom: 15,
                ),
                shrinkWrap: chatObserver.isShrinkWrap,
                reverse: true,
                controller: scrollController,
                cacheExtent: 200 * messages.length + 200,
                itemBuilder: ((context, index) {
                  return _itemBuilder(context, index);
                }),
                separatorBuilder: (_, __) {
                  return Container(height: 20);
                },
                itemCount: messages.length,
              );

              if (chatObserver.isShrinkWrap) {
                resultWidget = SingleChildScrollView(
                  reverse: true,
                  physics: scrollViewPhysics,
                  child: Container(
                    alignment: Alignment.topCenter,
                    height: constraints.maxHeight + 0.001,
                    child: resultWidget,
                  ),
                );
              }
              return resultWidget;
            },
          );

          resultWidget = ListViewObserver(
            controller: observerController,
            child: resultWidget,
          );
          resultWidget = Align(
            alignment: Alignment.topCenter,
            child: resultWidget,
          );
          return resultWidget;
        },
      ),
    );
  }

  Widget _itemBuilder(BuildContext context, int index) {
    MessageItem item = messages[index];
    print("## _itemBuilder :$index ${item.isExpand}");
    return Container(
      margin: const EdgeInsets.only(bottom: 10),
      width: 150,
      color: Colors.grey[200],
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          ExpandBox(
            childContent: item.content,
            isExpanded: item.isExpand,
            callback: (bool isExpand) {
              item.isExpand = isExpand;
              expandCb(index);
            },
            index: index,
          ),
          Text(item.title),
        ],
      ),
    );
  }

  void initChatObserver() {
    observerController = ListObserverController(
      controller: scrollController,
    )..cacheJumpIndexOffset = false;

    chatObserver = ChatScrollObserver(observerController)
      // ..fixedPositionOffset = -1
      ..fixedPositionOffset = 5
      ..toRebuildScrollViewCallback = () {
        setState(() {});
      };

    chatObserver.standby(
      changeCount: messages.length,
      mode: ChatScrollObserverHandleMode.specified,
      refIndexType: ChatScrollObserverRefIndexType.itemIndex,
    );
  }

  void expandCb(int index) {
    final isLastItem = index == messages.length - 1;
    // 默认是对比上一个 item 来定位
    // 最后一个 item,没有上一个可以对比,则对比自身
    // 此处对比自身无意义,因为由 customAdjustPosition 去完全控制保持位置的计算,忽略即可。
    final refItemIndex = isLastItem ? index : index + 1;
    chatObserver.standby(
      mode: ChatScrollObserverHandleMode.specified,
      refIndexType: ChatScrollObserverRefIndexType.itemIndex,
      refItemIndex: refItemIndex,
      refItemIndexAfterUpdate: refItemIndex,
      customAdjustPosition: (model) {
        // 仅处理最后一个 item 的情况
        if (!isLastItem) return null;
        // 使用 变化前后的底部间距差 来保持位置
        final delta =
            model.newPosition.extentAfter - model.oldPosition.extentAfter;
        return model.adjustPosition + delta;
      },
      customAdjustPositionDelta: (model) {
        // 仅处理不是最后一个 item 的情况
        if (isLastItem) return null;
        // 以 变化前后的 item 偏移差 来保持位置
        final adjustPosition = model.adjustPosition;
        final delta = model.currentItemModel.layoutOffset -
            model.observer.refItemLayoutOffset;
        if (delta < 0) {
          // 收起
          // 因消息的高度太大,在收起时,Flutter 内部对列表的偏移计算有问题
          // 所以这里调整计算方式,改为:视口的当前偏移量 - 内容变化量
          // 注:减去 adjustPosition,是因为保持位置功能的内部会加上 adjustPosition
          return model.currentItemModel.viewportPixels + delta - adjustPosition;
        }
        // 展开
        return delta;
      },
    );
  }
}

class ExpandBox extends StatefulWidget {
  final String childContent;
  final Function(bool)? callback;
  final int index;
  final bool isExpanded;

  const ExpandBox({
    super.key,
    required this.childContent,
    this.callback,
    required this.index,
    required this.isExpanded,
  });

  @override
  State<ExpandBox> createState() => _ExpandBoxState();
}

class _ExpandBoxState extends State<ExpandBox> {
  bool isExpanded = false;

  @override
  void initState() {
    super.initState();
    isExpanded = widget.isExpanded;
    print("## initState $isExpanded");
  }

  @override
  Widget build(
    BuildContext context,
  ) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        ElevatedButton(
            onPressed: () {
              final value = !isExpanded;
              widget.callback!(value);
              setState(() {
                isExpanded = value;
              });
            },
            child: isExpanded ? const Text('收起') : const Text('展开')),
        if (isExpanded) Text(widget.childContent),
        if (isExpanded) const Text('Expanded content'),
      ],
    );
  }
}

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions