Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

消息内部有折叠、展开的时候消息位置固定,当展开的内容很长的时候 standby 位置固定无效 #112

Closed
wksmile opened this issue Jan 23, 2025 · 3 comments
Assignees

Comments

@wksmile
Copy link

wksmile commented Jan 23, 2025

大佬你好,我按照
Originally posted by @wksmile in #103 的方法确实能实现展开位置固定,但是有一个问题,就是当展开的内容很长超过一屏的时候展开收起位置就不能固定了。

如图,展开很长位置固定失效。

Image

写个一个 demo 问题复现代码如下:
scroll_list_page.dart 文件

import './expand_item.dart';

class MessageItem {
  final String content;
  final List<String> children;

  const MessageItem({required this.content, required this.children});
}

@RoutePage()
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(
        content:
            '''监测**   - **尽可能上传最近的数据**,例如体温、心率血压、呼吸频率等,这有助于更精确地分析头痛原因。   - 记录头痛的发生时间、频率和持续时间,以及是否伴随其他症状。#### **4. 改善睡眠**   - 保持规律睡眠时间,晚上尽量避免在屏幕前时间过长。   - 可在睡前喝些温牛奶,或通过泡脚及放松呼吸训练,帮助身体进入更深的睡眠状态。---### **需要特别关注的情况**如果头痛伴有以下症状,请及时反馈或联系医生:- 持续加剧的头痛,并伴有明显的视力模糊、呕吐或晕厥。- 任何异常变化,比如剧烈恶心、无法言语或肢体无力。请根据建议逐步调节,如果头痛持续存在或频繁加重,及时向我反馈最新的症状与监测数据!''',
        children: [
          '''onize 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''',
        ]),
    MessageItem(
        content:
            '''这条消息第一个展开内容很多长展开有问题,位置不能固定''',
        children: [
          '''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''',
          '''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''',
          '''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''',
        ]),
    MessageItem(content: '''步数数据!''', children: [
      '''A relevant data has been uploaded or synchronized''',
    ]),
    MessageItem(content: '''步数数据!''', children: [
      '''A relevant data has been uploaded or synchronized''',
    ]),
    MessageItem(content: '''步数数据!''', children: [
      '''A relevant data has been uploaded or synchronized''',
    ]),
    MessageItem(content: '''步数数据!''', children: [
      '''A relevant data has been uploaded or synchronized''',
    ]),
    MessageItem(content: '''步数数据!''', children: [
      '''A relevant data has been uploaded or synchronized''',
    ]),
    MessageItem(content: '''步数数据!''', children: [
      '''A relevant data has been uploaded or synchronized''',
    ]),
    MessageItem(content: '''步数数据!''', children: [
      '''A relevant data has been uploaded or synchronized''',
    ]),
    MessageItem(content: '''步数数据!''', children: [
      '''A relevant data has been uploaded or synchronized''',
    ]),
    MessageItem(content: '''步数数据!''', children: [
      '''A relevant data has been uploaded or synchronized''',
    ]),
    MessageItem(content: '''步数数据!''', children: [
      '''A relevant data has been uploaded or synchronized''',
    ]),
    MessageItem(content: '''步数数据!''', children: [
      '''A relevant data has been uploaded or synchronized''',
    ]),
    MessageItem(content: '''步数数据!''', children: [
      '''A 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(
        backgroundColor: Colors.white,
        resizeToAvoidBottomInset: true,
        body: Align(
          alignment: Alignment.topCenter,
          child: ListViewObserver(
            controller: observerController,
            child: ListView.builder(
              physics: ChatObserverClampingScrollPhysics(observer: chatObserver),
              itemCount: messages.length,
              reverse: true,
              controller: scrollController, // 设置 ScrollController
              shrinkWrap: chatObserver.isShrinkWrap,
              padding: const EdgeInsets.only(left: 16, right: 16, top: 20, bottom: 10),
              itemBuilder: (context, index) {
                return _itemBuilder(
                  context,
                  index,
                );
              },
            ),
          ),
        ));
  }

  Widget _itemBuilder(BuildContext context, int index) {
    MessageItem item = messages[index];
    return Container(
        margin: EdgeInsets.only(bottom: 10),
        width: 150,
        color: Colors.grey[200],
        child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
          ...item.children.map((e) => ExpandBox(childContent: e, callback: expandCb, index: index)),
          Text(item.content),
        ]));
  }

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

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

  void expandCb(int index) {
    // 对比上一个 item 来定位
    final refItemIndex = index + 1;
    chatObserver.standby(
      mode: ChatScrollObserverHandleMode.specified,
      refIndexType: ChatScrollObserverRefIndexType.itemIndex,
      refItemIndex: refItemIndex,
      refItemIndexAfterUpdate: refItemIndex,
    );
  }
}

expand_item.dart 文件


class ExpandBox extends StatefulWidget {
  final String childContent;
  final Function? callback;
  final int index;

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

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

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

  @override
  Widget build(
    BuildContext context,
  ) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        ElevatedButton(
            onPressed: () {
              setState(() {
                isExpanded = !isExpanded;
              });
              widget.callback!(widget.index);
            },
            child: isExpanded ? Text('收起') : Text('展开')),
        if (isExpanded) Text(widget.childContent),
        if (isExpanded) Text('Expanded content'),
      ],
    );
  }
}
@wksmile wksmile changed the title 一条消息存在多个折叠的时候怎么保持展开收起时候位置固定 一条消息折叠展开的时候位置固定,当展开的内容很长的时候 standby 位置固定无效 Jan 23, 2025
@wksmile wksmile changed the title 一条消息折叠展开的时候位置固定,当展开的内容很长的时候 standby 位置固定无效 消息内部有折叠、展开的时候消息位置固定,当展开的内容很长的时候 standby 位置固定无效 Jan 23, 2025
@LinXunFeng
Copy link
Member

问题的原因与 #111 (comment) 一致,请适度控制消息的长度或者 cacheExtent 的值,使被参照的 item 保持渲染,以确保该功能正常使用。

另外一些优化:

  • 最后的一条消息的【展开/收起】也支持保持位置
  • 处理中间内容太长的 item 【收起】时列表抖动

请升级至 1.25.0 版本,并结合如下代码调整

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;
    },
  );
}

@wksmile
Copy link
Author

wksmile commented Feb 5, 2025

感谢!按照以上方式解决了我的问题。
但是customAdjustPosition 、 customAdjustPositionDelta 这两个属性的区别没太弄明白,希望能帮解释一下。

@LinXunFeng
Copy link
Member

customAdjustPosition

  • 被调用的前提: 当前列表视图满足需要保持位置的条件
  • 作用:用于完全自定义列表视图新的偏移量,一般在没有参照 item 的情况下使用

// Customize the adjustPosition.
double? customAdjustPosition = observer.customAdjustPosition?.call(
ChatScrollObserverCustomAdjustPositionModel(
oldPosition: oldPosition,
newPosition: newPosition,
isScrolling: isScrolling,
velocity: velocity,
adjustPosition: adjustPosition,
observer: observer,
),
);
if (customAdjustPosition != null) {
_handlePositionCallback(ChatScrollObserverHandlePositionResultModel(
type: ChatScrollObserverHandlePositionType.keepPosition,
mode: observer.innerMode,
changeCount: observer.changeCount,
));
return customAdjustPosition;
}

如你的示例代码中的最后一个 item,当其高度发生变化时无可参照 item,但是视口底部距离 (extentAfter ) 是变化的,且知道差值是多少,这个时候就得使用使用 customAdjustPosition 回调来自定义列表视图新的偏移量,以达到保持位置的效果。

customAdjustPositionDelta

  • 被调用的前提:
    • 当前列表视图满足需要保持位置的条件
    • 有参照 item 且被渲染
  • 作用:用于在列表视图新的偏移量的基本上做调整,一般在有参照 item 的情况下使用

final model = observer.observeRefItem();
if (model == null) {
_handlePositionCallback(ChatScrollObserverHandlePositionResultModel(
type: ChatScrollObserverHandlePositionType.none,
mode: observer.innerMode,
changeCount: observer.changeCount,
));
return adjustPosition;
}
_handlePositionCallback(ChatScrollObserverHandlePositionResultModel(
type: ChatScrollObserverHandlePositionType.keepPosition,
mode: observer.innerMode,
changeCount: observer.changeCount,
));
// Customize the delta of the adjustPosition.
double? customDelta = observer.customAdjustPositionDelta?.call(
ChatScrollObserverCustomAdjustPositionDeltaModel(
oldPosition: oldPosition,
newPosition: newPosition,
isScrolling: isScrolling,
velocity: velocity,
adjustPosition: adjustPosition,
observer: observer,
currentItemModel: model,
),
);
// Calculate the final delta.
//
// If the customDelta is not null, use the customDelta.
// Otherwise, use the layoutOffset minus innerRefItemLayoutOffset to get
// the difference in the leading offset of the item.
final delta =
customDelta ?? (model.layoutOffset - observer.innerRefItemLayoutOffset);
return adjustPosition + delta;

该回调在 #103 中被加入,允许在新偏移量的基础上去调整以达到保持位置的功能,返回 null 则按库中逻辑走,使用上可以看之前的例子 #103 (comment)

在上述代码中用来处理收起时 adjustPosition 计算异常的情况,当然,你可以继续选择 customAdjustPosition,看个人喜好了。

注:上述代码中 model.adjustPositionFlutter 内部计算出来的列表视图的新偏移量。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants