robert-luoqing / flutter_list_view

MIT License
47 stars 17 forks source link

Touch position are incorrect #11

Closed HuangZiquan closed 8 months ago

HuangZiquan commented 2 years ago

Hi, The touch position of the item is incorrect,when items not out of page.

edit in your chat example :

import 'dart:math';
import 'dart:ui';

import 'package:flutter/cupertino.dart';
import 'package:flutter_list_view/flutter_list_view.dart';
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';

enum MessageType {
  sent,
  receive,
  tag,
}

class ChatModel {
  ChatModel({required this.id, required this.msg, required this.type});

  int id;
  String msg;
  MessageType type;
}

class Chat extends StatefulWidget {
  const Chat({Key? key}) : super(key: key);

  @override
  _ChatState createState() => _ChatState();
}

class _ChatState extends State<Chat> {
  int currentId = 0;
  List<ChatModel> messages = [];
  final myController = TextEditingController();
  final listViewController = FlutterListViewController();
  final refreshController = RefreshController(initialRefresh: false);

  /// Using init index to control load first messages
  int initIndex = 0;
  double initOffset = 0.0;
  bool initOffsetBasedOnBottom = true;
  int forceToExecuteInitIndex = 0;

  // Fire refresh temp variable
  double prevScrollOffset = 0;

  List<FlutterListViewItemPosition>? itemPositions;
  double listviewHeight = 0;

  @override
  void initState() {
    _loadMessages();
    listViewController.addListener(() {
      const torrentDistance = 40;
      var offset = listViewController.offset;
      if (offset <= torrentDistance && prevScrollOffset > torrentDistance) {
        if (!refreshController.isRefresh) {
          refreshController.requestRefresh();
        }
      }

      prevScrollOffset = offset;
    });

    listViewController.sliverController.onPaintItemPositionsCallback = (widgetHeight, positions) {
      itemPositions = positions;
      listviewHeight = widgetHeight;
    };

    super.initState();
  }

  /// It is mockup to load messages from server
  _loadMessages() async {
    await Future.delayed(const Duration(milliseconds: 100));
    var prevTimes = Random().nextInt(20) + 1;
    // for (var i = 0; i < prevTimes; i++) {
    //   _insertReceiveMessage("The demo also show how to reverse a list in\r\n" *
    //       (Random().nextInt(4) + 1));
    // }
    _insertTagMessage("Last readed");
    var nextTimes = Random().nextInt(20) + 1;
    // for (var i = 0; i < nextTimes; i++) {
    //   _insertReceiveMessage("The demo also show how to reverse a list in\r\n" *
    //       (Random().nextInt(4) + 1));
    // }
    _insertSendMessage("If message more than two screens and scroll over 80px, the scroll not move if a message coming or you input a message");
    _insertSendMessage("It resoved the problem which is when you read a message while a lot of messages coming");
    _insertSendMessage("You can't focus the message content what you read");
    _insertSendMessage("The demo also show how to reverse a list in the controll");
    _insertSendMessage("When reverse the list, the item still show on top of list if the messages didn't fill full screen");

    initIndex = messages.length - prevTimes - 1;
    print("--------------------$initIndex");

    setState(() {});
  }

  _insertSendMessage(String msg, {bool appendToTailer = false}) {
    if (appendToTailer) {
      messages.add(ChatModel(id: ++currentId, msg: msg.trim(), type: MessageType.sent));
    } else {
      messages.insert(0, ChatModel(id: ++currentId, msg: msg.trim(), type: MessageType.sent));
    }
  }

  _insertReceiveMessage(String msg, {bool appendToTailer = false}) {
    if (appendToTailer) {
      messages.add(ChatModel(id: ++currentId, msg: msg.trim(), type: MessageType.receive));
    } else {
      messages.insert(0, ChatModel(id: ++currentId, msg: msg.trim(), type: MessageType.receive));
    }
  }

  _insertTagMessage(String msg) {
    messages.insert(0, ChatModel(id: ++currentId, msg: msg.trim(), type: MessageType.tag));
  }

  _mockToReceiveMessage() {
    var times = Random().nextInt(4) + 1;
    for (var i = 0; i < times; i++) {
      _insertReceiveMessage("The demo also show how to reverse a list in\r\n" * (Random().nextInt(4) + 1));
    }
    setState(() {});
  }

  _sendMessage() {
    if (myController.text.isNotEmpty) {
      if (messages.isNotEmpty) {
        listViewController.sliverController.jumpToIndex(0);
      }
      setState(() {
        _insertSendMessage(myController.text);
      });

      myController.text = "";
    }
  }

  void _onRefresh() async {
    print("------------------------------------_onRefresh");
    await Future.delayed(const Duration(milliseconds: 2000));

    var newMessgeLength = 20;

    for (var i = 0; i < newMessgeLength; i++) {
      _insertReceiveMessage("The demo also show how to reverse a list in\r\n" * (Random().nextInt(4) + 1));
    }

    var firstIndex = newMessgeLength;
    var firstOffset = 0.0;
    if (itemPositions != null && itemPositions!.isNotEmpty) {
      var firstItemPosition = itemPositions![0];
      firstIndex = firstItemPosition.index + newMessgeLength;
      firstOffset = listviewHeight - firstItemPosition.offset - firstItemPosition.height;
    }

    initIndex = firstIndex;
    initOffsetBasedOnBottom = false;
    forceToExecuteInitIndex++;
    initOffset = firstOffset;
    refreshController.refreshCompleted();
    setState(() {});
  }

  void _onLoading() async {
    await Future.delayed(const Duration(milliseconds: 1000));

    for (var i = 0; i < 50; i++) {
      _insertReceiveMessage("The demo also show how to append message\r\n" * (Random().nextInt(4) + 1), appendToTailer: true);
    }

    if (mounted) setState(() {});
    refreshController.loadComplete();
  }

  _renderItem(int index) {
    var msg = messages[index];
    if (msg.type == MessageType.tag) {
      return Align(
        alignment: Alignment.center,
        child: Padding(
          padding: const EdgeInsets.all(10.0),
          child: Container(
            decoration: const BoxDecoration(color: Colors.grey, borderRadius: BorderRadius.all(Radius.circular(5))),
            child: Padding(
              padding: const EdgeInsets.all(10.0),
              child: Text(
                msg.msg,
                style: const TextStyle(fontSize: 14.0, color: Colors.white),
              ),
            ),
          ),
        ),
      );
    } else {
      return Align(
        alignment: msg.type == MessageType.sent ? Alignment.centerRight : Alignment.centerLeft,
        child: Padding(
          padding: const EdgeInsets.all(10.0),
          child: GestureDetector(
            onTap: () {
              print(msg.msg);
            },
            child: Container(
              decoration: BoxDecoration(
                  color: msg.type == MessageType.sent ? Colors.blue : Colors.green,
                  borderRadius: msg.type == MessageType.sent
                      ? const BorderRadius.only(topLeft: Radius.circular(20), bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20))
                      : const BorderRadius.only(topRight: Radius.circular(20), bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20))),
              child: Padding(
                padding: const EdgeInsets.all(10.0),
                child: Text(
                  msg.msg,
                  style: const TextStyle(fontSize: 14.0, color: Colors.white),
                ),
              ),
            ),
          ),
        ),
      );
    }
  }

  _renderList() {
    return ScrollConfiguration(
      behavior: ScrollConfiguration.of(context).copyWith(dragDevices: {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
      }),
      child: SmartRefresher(
          enablePullDown: false,
          enablePullUp: false,
          header: CustomHeader(
            completeDuration: const Duration(milliseconds: 0),
            builder: (context, mode) {
              Widget body;
              if (mode == RefreshStatus.idle) {
                body = const Text("Pull up load prev msg");
              } else if (mode == RefreshStatus.refreshing) {
                body = const CupertinoActivityIndicator();
              } else if (mode == RefreshStatus.failed) {
                body = const Text("Load Failed!Click retry!");
              } else if (mode == RefreshStatus.canRefresh) {
                body = const Text("Release to load more");
              } else {
                body = const Text("No more Data");
              }
              if (mode == RefreshStatus.completed) {
                return Container();
              } else {
                return RotatedBox(
                  quarterTurns: 2,
                  child: SizedBox(
                    height: 55.0,
                    child: Center(child: body),
                  ),
                );
              }
            },
          ),
          // const WaterDropHeader(),
          footer: CustomFooter(
            builder: (context, mode) {
              Widget body;
              if (mode == LoadStatus.idle) {
                body = const Text("Pull down to load more message");
              } else if (mode == LoadStatus.loading) {
                body = const CupertinoActivityIndicator();
              } else if (mode == LoadStatus.failed) {
                body = const Text("Load Failed!Click retry!");
              } else if (mode == LoadStatus.canLoading) {
                body = const Text("Release to load more");
              } else {
                body = const Text("No more Data");
              }
              return SizedBox(
                height: 55.0,
                child: Center(child: body),
              );
            },
          ),
          controller: refreshController,
          onRefresh: _onRefresh,
          onLoading: _onLoading,
          child: FlutterListView(
              reverse: true,
              controller: listViewController,
              delegate: FlutterListViewDelegate((BuildContext context, int index) => _renderItem(index),
                  childCount: messages.length,
                  onItemKey: (index) => messages[index].id.toString(),
                  keepPosition: true,
                  keepPositionOffset: 40,
                  initIndex: initIndex,
                  initOffset: initOffset,
                  initOffsetBasedOnBottom: initOffsetBasedOnBottom,
                  forceToExecuteInitIndex: forceToExecuteInitIndex,
                  firstItemAlign: FirstItemAlign.end))),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text("Chat"),
          actions: [
            TextButton(
                onPressed: _mockToReceiveMessage,
                child: const Text(
                  "Mock To Receive",
                  style: TextStyle(color: Colors.white),
                ))
          ],
        ),
        resizeToAvoidBottomInset: true,
        body: GestureDetector(
            onTap: () {
              FocusScopeNode currentFocus = FocusScope.of(context);
              if (!currentFocus.hasPrimaryFocus) {
                currentFocus.unfocus();
              }
            },
            behavior: HitTestBehavior.opaque,
            child: SafeArea(
              child: Padding(
                padding: const EdgeInsets.only(bottom: 8.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Expanded(flex: 1, child: _renderList()),
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 20.0),
                      child: Row(children: [
                        Expanded(
                          child: TextField(
                            controller: myController,
                          ),
                        ),
                        ElevatedButton(onPressed: _sendMessage, child: const Text("Send"))
                      ]),
                    )
                  ],
                ),
              ),
            )));
  }

  @override
  void dispose() {
    myController.dispose();
    listViewController.dispose();
    refreshController.dispose();
    super.dispose();
  }
}
robert-luoqing commented 2 years ago

@HuangZiquan Thanks for your feedback, the issue have been fixed in flutter_list_view: ^1.1.16. Please update it.

The issue caused by firstItemAlign to FirstItemAlign.end

Thanks.

HuangZiquan commented 2 years ago

In the same example, I added a GlobalKey to the item, and I found that the global coordinate was incorrect.

import 'dart:math';
import 'dart:ui';

import 'package:flutter/cupertino.dart';
import 'package:flutter_list_view/flutter_list_view.dart';
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';

enum MessageType {
  sent,
  receive,
  tag,
}

class ChatModel {
  ChatModel({required this.id, required this.msg, required this.type});

  int id;
  String msg;
  MessageType type;
}

class Chat extends StatefulWidget {
  const Chat({Key? key}) : super(key: key);

  @override
  _ChatState createState() => _ChatState();
}

class _ChatState extends State<Chat> {
  int currentId = 0;
  List<ChatModel> messages = [];
  final myController = TextEditingController();
  final listViewController = FlutterListViewController();
  final refreshController = RefreshController(initialRefresh: false);

  /// Using init index to control load first messages
  int initIndex = 0;
  double initOffset = 0.0;
  bool initOffsetBasedOnBottom = true;
  int forceToExecuteInitIndex = 0;

  // Fire refresh temp variable
  double prevScrollOffset = 0;

  List<FlutterListViewItemPosition>? itemPositions;
  double listviewHeight = 0;

  @override
  void initState() {
    _loadMessages();
    listViewController.addListener(() {
      const torrentDistance = 40;
      var offset = listViewController.offset;
      if (offset <= torrentDistance && prevScrollOffset > torrentDistance) {
        if (!refreshController.isRefresh) {
          refreshController.requestRefresh();
        }
      }

      prevScrollOffset = offset;
    });

    listViewController.sliverController.onPaintItemPositionsCallback = (widgetHeight, positions) {
      itemPositions = positions;
      listviewHeight = widgetHeight;
    };

    super.initState();
  }

  /// It is mockup to load messages from server
  _loadMessages() async {
    await Future.delayed(const Duration(milliseconds: 100));
    var prevTimes = Random().nextInt(20) + 1;
    // for (var i = 0; i < prevTimes; i++) {
    //   _insertReceiveMessage("The demo also show how to reverse a list in\r\n" *
    //       (Random().nextInt(4) + 1));
    // }
    _insertTagMessage("Last readed");
    var nextTimes = Random().nextInt(20) + 1;
    // for (var i = 0; i < nextTimes; i++) {
    //   _insertReceiveMessage("The demo also show how to reverse a list in\r\n" *
    //       (Random().nextInt(4) + 1));
    // }
    _insertSendMessage("If message more than two screens and scroll over 80px, the scroll not move if a message coming or you input a message");
    _insertSendMessage("It resoved the problem which is when you read a message while a lot of messages coming");
    _insertSendMessage("You can't focus the message content what you read");
    _insertSendMessage("The demo also show how to reverse a list in the controll");
    _insertSendMessage("When reverse the list, the item still show on top of list if the messages didn't fill full screen");

    initIndex = messages.length - prevTimes - 1;
    print("--------------------$initIndex");

    setState(() {});
  }

  _insertSendMessage(String msg, {bool appendToTailer = false}) {
    if (appendToTailer) {
      messages.add(ChatModel(id: ++currentId, msg: msg.trim(), type: MessageType.sent));
    } else {
      messages.insert(0, ChatModel(id: ++currentId, msg: msg.trim(), type: MessageType.sent));
    }
  }

  _insertReceiveMessage(String msg, {bool appendToTailer = false}) {
    if (appendToTailer) {
      messages.add(ChatModel(id: ++currentId, msg: msg.trim(), type: MessageType.receive));
    } else {
      messages.insert(0, ChatModel(id: ++currentId, msg: msg.trim(), type: MessageType.receive));
    }
  }

  _insertTagMessage(String msg) {
    messages.insert(0, ChatModel(id: ++currentId, msg: msg.trim(), type: MessageType.tag));
  }

  _mockToReceiveMessage() {
    var times = Random().nextInt(4) + 1;
    for (var i = 0; i < times; i++) {
      _insertReceiveMessage("The demo also show how to reverse a list in\r\n" * (Random().nextInt(4) + 1));
    }
    setState(() {});
  }

  _sendMessage() {
    if (myController.text.isNotEmpty) {
      if (messages.isNotEmpty) {
        listViewController.sliverController.jumpToIndex(0);
      }
      setState(() {
        _insertSendMessage(myController.text);
      });

      myController.text = "";
    }
  }

  void _onRefresh() async {
    print("------------------------------------_onRefresh");
    await Future.delayed(const Duration(milliseconds: 2000));

    var newMessgeLength = 20;

    for (var i = 0; i < newMessgeLength; i++) {
      _insertReceiveMessage("The demo also show how to reverse a list in\r\n" * (Random().nextInt(4) + 1));
    }

    var firstIndex = newMessgeLength;
    var firstOffset = 0.0;
    if (itemPositions != null && itemPositions!.isNotEmpty) {
      var firstItemPosition = itemPositions![0];
      firstIndex = firstItemPosition.index + newMessgeLength;
      firstOffset = listviewHeight - firstItemPosition.offset - firstItemPosition.height;
    }

    initIndex = firstIndex;
    initOffsetBasedOnBottom = false;
    forceToExecuteInitIndex++;
    initOffset = firstOffset;
    refreshController.refreshCompleted();
    setState(() {});
  }

  void _onLoading() async {
    await Future.delayed(const Duration(milliseconds: 1000));

    for (var i = 0; i < 50; i++) {
      _insertReceiveMessage("The demo also show how to append message\r\n" * (Random().nextInt(4) + 1), appendToTailer: true);
    }

    if (mounted) setState(() {});
    refreshController.loadComplete();
  }

  _renderItem(int index) {
    var msg = messages[index];
    if (msg.type == MessageType.tag) {
      return Align(
        alignment: Alignment.center,
        child: Padding(
          padding: const EdgeInsets.all(10.0),
          child: Container(
            decoration: const BoxDecoration(color: Colors.grey, borderRadius: BorderRadius.all(Radius.circular(5))),
            child: Padding(
              padding: const EdgeInsets.all(10.0),
              child: Text(
                msg.msg,
                style: const TextStyle(fontSize: 14.0, color: Colors.white),
              ),
            ),
          ),
        ),
      );
    } else {
      final itemKey = GlobalKey();
      return Align(
        alignment: msg.type == MessageType.sent ? Alignment.centerRight : Alignment.centerLeft,
        child: Padding(
          padding: const EdgeInsets.all(10.0),
          child: GestureDetector(
            onTap: () {
              print(msg.msg);
            },
            onLongPress: () {
              final itemRenderBox = itemKey.currentContext!.findRenderObject() as RenderBox;
              print(itemRenderBox.localToGlobal(Offset.zero));
            },
            child: Container(
              key: itemKey,
              decoration: BoxDecoration(
                  color: msg.type == MessageType.sent ? Colors.blue : Colors.green,
                  borderRadius: msg.type == MessageType.sent
                      ? const BorderRadius.only(topLeft: Radius.circular(20), bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20))
                      : const BorderRadius.only(topRight: Radius.circular(20), bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20))),
              child: Padding(
                padding: const EdgeInsets.all(10.0),
                child: Text(
                  msg.msg,
                  style: const TextStyle(fontSize: 14.0, color: Colors.white),
                ),
              ),
            ),
          ),
        ),
      );
    }
  }

  _renderList() {
    return ScrollConfiguration(
      behavior: ScrollConfiguration.of(context).copyWith(dragDevices: {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
      }),
      child: SmartRefresher(
          enablePullDown: false,
          enablePullUp: false,
          header: CustomHeader(
            completeDuration: const Duration(milliseconds: 0),
            builder: (context, mode) {
              Widget body;
              if (mode == RefreshStatus.idle) {
                body = const Text("Pull up load prev msg");
              } else if (mode == RefreshStatus.refreshing) {
                body = const CupertinoActivityIndicator();
              } else if (mode == RefreshStatus.failed) {
                body = const Text("Load Failed!Click retry!");
              } else if (mode == RefreshStatus.canRefresh) {
                body = const Text("Release to load more");
              } else {
                body = const Text("No more Data");
              }
              if (mode == RefreshStatus.completed) {
                return Container();
              } else {
                return RotatedBox(
                  quarterTurns: 2,
                  child: SizedBox(
                    height: 55.0,
                    child: Center(child: body),
                  ),
                );
              }
            },
          ),
          // const WaterDropHeader(),
          footer: CustomFooter(
            builder: (context, mode) {
              Widget body;
              if (mode == LoadStatus.idle) {
                body = const Text("Pull down to load more message");
              } else if (mode == LoadStatus.loading) {
                body = const CupertinoActivityIndicator();
              } else if (mode == LoadStatus.failed) {
                body = const Text("Load Failed!Click retry!");
              } else if (mode == LoadStatus.canLoading) {
                body = const Text("Release to load more");
              } else {
                body = const Text("No more Data");
              }
              return SizedBox(
                height: 55.0,
                child: Center(child: body),
              );
            },
          ),
          controller: refreshController,
          onRefresh: _onRefresh,
          onLoading: _onLoading,
          child: FlutterListView(
              reverse: true,
              controller: listViewController,
              delegate: FlutterListViewDelegate((BuildContext context, int index) => _renderItem(index),
                  childCount: messages.length,
                  onItemKey: (index) => messages[index].id.toString(),
                  keepPosition: true,
                  keepPositionOffset: 40,
                  initIndex: initIndex,
                  initOffset: initOffset,
                  initOffsetBasedOnBottom: initOffsetBasedOnBottom,
                  forceToExecuteInitIndex: forceToExecuteInitIndex,
                  firstItemAlign: FirstItemAlign.end))),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text("Chat"),
          actions: [
            TextButton(
                onPressed: _mockToReceiveMessage,
                child: const Text(
                  "Mock To Receive",
                  style: TextStyle(color: Colors.white),
                ))
          ],
        ),
        resizeToAvoidBottomInset: true,
        body: GestureDetector(
            onTap: () {
              FocusScopeNode currentFocus = FocusScope.of(context);
              if (!currentFocus.hasPrimaryFocus) {
                currentFocus.unfocus();
              }
            },
            behavior: HitTestBehavior.opaque,
            child: SafeArea(
              child: Padding(
                padding: const EdgeInsets.only(bottom: 8.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Expanded(flex: 1, child: _renderList()),
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 20.0),
                      child: Row(children: [
                        Expanded(
                          child: TextField(
                            controller: myController,
                          ),
                        ),
                        ElevatedButton(onPressed: _sendMessage, child: const Text("Send"))
                      ]),
                    )
                  ],
                ),
              ),
            )));
  }

  @override
  void dispose() {
    myController.dispose();
    listViewController.dispose();
    refreshController.dispose();
    super.dispose();
  }
}