У меня есть прокручиваемый ListView, где количество элементов может меняться динамически. Каждый раз, когда новый элемент добавляется в конец списка, я хотел бы программно прокрутить ListView до конца. (например, что-то вроде списка сообщений чата, в который можно добавлять новые сообщения)

Я предполагаю, что мне нужно будет создать ScrollController в моем State объекте и передать его вручную конструктору ListView, чтобы я мог позже вызватьanimateTo () / jumpTo () метод на контроллере. Однако, поскольку я не могу легко определить максимальное смещение прокрутки, кажется невозможным просто выполнить операцию типа scrollToEnd () (тогда как я могу легко передать 0.0, чтобы заставить его прокрутить до исходное положение).

Есть простой способ добиться этого?

Использование reverse: true не является идеальным решением для меня, потому что я хотел бы, чтобы элементы были выровнены вверху, когда есть только небольшое количество элементов, которые помещаются в ListView область просмотра.

Ответы (11)

Если вы используете термоусадочную ListView с reverse: true, прокрутка до 0,0 сделает то, что вы хотите.

import 'dart:collection';

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Example',
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State {
  List _messages = [new Text('hello'), new Text('world')];
  ScrollController _scrollController = new ScrollController();

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Center(
        child: new Container(
          decoration: new BoxDecoration(backgroundColor: Colors.blueGrey.shade100),
          width: 100.0,
          height: 100.0,
          child: new Column(
            children: [
              new Flexible(
                child: new ListView(
                  controller: _scrollController,
                  reverse: true,
                  shrinkWrap: true,
                  children: new UnmodifiableListView(_messages),
                ),
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        child: new Icon(Icons.add),
        onPressed: () {
          setState(() {
            _messages.insert(0, new Text("message ${_messages.length}"));
          });
          _scrollController.animateTo(
            0.0,
            curve: Curves.easeOut,
            duration: const Duration(milliseconds: 300),
          );
        }
      ),
    );
  }
}

, чтобы получить идеальные результаты, я объединил ответы Колина Джексона и CopsOnRoad следующим образом:

_scrollController.animateTo(
    _scrollController.position.maxScrollExtent,
    curve: Curves.easeOut,
    duration: const Duration(milliseconds: 500),
 );

listViewScrollController.animateTo(listViewScrollController.position.maxScrollExtent) is the simplest way.

Я столкнулся с этой проблемой, когда использовал виджет StreamBuilder для получения данных из моей базы данных. Я помещаю WidgetsBinding.instance.addPostFrameCallback поверх метода виджета build, и он не прокручивается до конца. Я исправил это следующим образом:

...
StreamBuilder(
  stream: ...,
  builder: (BuildContext context, AsyncSnapshot snapshot) {
    // Like this:
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_controller.hasClients) {
        _controller.jumpTo(_controller.position.maxScrollExtent);
      } else {
        setState(() => null);
      }
     });

     return PutYourListViewHere
}),
...

Я пробовал это с _controller.animateTo, но похоже, что это не сработало.

Хотя все ответы дают желаемый эффект, мы должны сделать здесь некоторые улучшения.

  • Во-первых, в большинстве случаев (говоря об автоматической прокрутке) использование postFrameCallbacks бесполезно, потому что некоторые вещи могут быть отрисованы после вложения ScrollController (созданного методом attach), контроллер будет прокручивать до последней известной ему позиции, и эта позиция не может быть последней на ваш взгляд.

  • Использование reverse: true должно быть хорошим трюком для «хвоста» содержимого, но физика будет обратной, поэтому, когда вы пытаетесь вручную переместить полосу прокрутки, вы должны переместить ее на противоположную сторону -> ПЛОХОЙ UX.

  • Использование таймеров - очень плохая практика при разработке графических интерфейсов -> таймер - это своего рода вирус, когда он используется для обновления / создания графических артефактов.

В любом случае, говоря о вопросе, правильный способ выполнить задачу - использовать метод jumpTo с методом hasClients в качестве защиты.

Прикреплены ли какие-либо объекты ScrollPosition к ScrollController с помощью метода присоединения. Если это false, то элементы, которые взаимодействуют с ScrollPosition, такие как position, offset, animateTo и jumpTo, не должны вызываться

Говоря в коде, просто сделайте что-нибудь вроде этого:

if (_scrollController.hasClients) {
    _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}

В любом случае этого кода все еще недостаточно, метод будет запущен, даже если прокручиваемый элемент не находится в конце экрана, поэтому, если вы вручную перемещаете полосу, метод сработает и будет выполнена автопрокрутка.

Мы можем сделать лучше, с помощью слушателя и пары bool будет нормально.
Я использую этот метод, чтобы визуализировать в SelectableText значение CircularBuffer размером 100000, и содержимое продолжает обновляться правильно, автопрокрутка выполняется очень плавно, и нет проблем с производительностью даже для очень очень длинного содержимого. Возможно, как кто-то сказал в других ответах, метод animateTo мог бы быть более плавным и настраиваемым, поэтому не стесняйтесь попробовать.

  • Прежде всего объявите эти переменные:
ScrollController _scrollController = new ScrollController();
bool _firstAutoscrollExecuted = false;
bool _shouldAutoscroll = false;
  • Тогда создадим метод автопрокрутки:
void _scrollToBottom() {
    _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
  • Далее нам понадобится слушатель:
void _scrollListener() {
    _firstAutoscrollExecuted = true;

    if (_scrollController.hasClients && _scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
        _shouldAutoscroll = true;
    } else {
        _shouldAutoscroll = false;
    }
}
  • Зарегистрируйте его в initState:
@override
void initState() {
    super.initState();
    _scrollController.addListener(_scrollListener);
}
  • Удалите слушателя в dispose:
@override
void dispose() {
    _scrollController.removeListener(_scrollListener);
    super.dispose();
}
  • Затем запускайте _scrollToBottom, исходя из вашей логики и потребностей, в вашем setState:
setState(() {
    if (_scrollController.hasClients && _shouldAutoscroll) {
        _scrollToBottom();
    }

    if (!_firstAutoscrollExecuted && _scrollController.hasClients) {
         _scrollToBottom();
    }
});

ПОЯСНЕНИЕ

  • Мы сделали простой метод: _scrollToBottom (), чтобы избежать повторений кода;
  • Мы создали _scrollListener () и прикрепили его к _scrollController в initState -> будет запущен после первого запуска полосы прокрутки. переехать. В этом слушателе мы обновляем значение логического значения _shouldAutoscroll, чтобы понять, находится ли полоса прокрутки внизу экрана.
  • Мы удалили слушателя в dispose, чтобы не делать бесполезных вещей после удаления виджета.
  • В нашем setState, когда мы уверены, что _scrollController прикреплен и находится внизу (проверка значения shouldAutoscroll), мы можем вызвать _scrollToBottom ().
    В то же время только для 1-го выполнения мы принудительно замыкаем _scrollToBottom () на значение _firstAutoscrollExecuted.

вы можете использовать это, где 0,09 * высота - высота строки в списке, а _controller определяется следующим образом _controller = ScrollController ();

(BuildContext context, int pos) {
    if(pos != 0) {
        _controller.animateTo(0.09 * height * (pos - 1), 
                              curve: Curves.easeInOut,
                              duration: Duration(milliseconds: 1400));
    }
}

в зависимости от этого ответа Я создал этот класс, просто отправьте свой scroll_controller, и если вы хотите противоположное направление, используйте параметр reversed

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

class ScrollService {
  static scrollToEnd(
      {required ScrollController scrollController, reversed = false}) {
    SchedulerBinding.instance!.addPostFrameCallback((_) {
      scrollController.animateTo(
        reverced
            ? scrollController.position.minScrollExtent
            : scrollController.position.maxScrollExtent,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOut,
      );
    });
  }
}
_controller.jumpTo(_controller.position.maxScrollExtent);
_controller.animateTo(_controller.position.maxScrollExtent);

Эти вызовы не работают для списка элементов с динамическим размером. В то время, когда вы вызываете jumpTo (), мы не знаем, какой длины будет список, поскольку все элементы являются переменными и строятся лениво при прокрутке списка вниз.

Возможно, это не самый разумный способ, но в крайнем случае вы можете сделать следующее:

Future scrollToBottom(ScrollController scrollController) async {
  while (scrollController.position.pixels != scrollController.position.maxScrollExtent) {
    scrollController.jumpTo(scrollController.position.maxScrollExtent);
    await SchedulerBinding.instance!.endOfFrame;
  }
}

Для этого используйте ScrollController.jumpTo () или ScrollController.animateTo ().

Пример:

final _controller = ScrollController();

@override
Widget build(BuildContext context) {
  
  // After 1 second, it takes you to the bottom of the ListView
  Timer(
    Duration(seconds: 1),
    () => _controller.jumpTo(_controller.position.maxScrollExtent),
  );

  return ListView.builder(
    controller: _controller,
    itemCount: 50,
    itemBuilder: (_, __) => ListTile(title: Text('ListTile')),
  );
}

Если вы хотите плавную прокрутку, то вместо использования jumpTo выше используйте

_controller.animateTo(
  _controller.position.maxScrollExtent,
  duration: Duration(seconds: 1),
  curve: Curves.fastOutSlowIn,
);

Не помещайте widgetBinding в initstate, вместо этого вам нужно поместить его в метод, который извлекает ваши данные из базы данных. например, вот так. Если поместить в initstate, scrollcontroller не будет подключаться ни к какому списку.

    Future> fetchMessage() async {

    var res = await Api().getData("message");
    var body = json.decode(res.body);
    if (res.statusCode == 200) {
      List messages = [];
      var count=0;
      for (var u in body) {
        count++;
        Message message = Message.fromJson(u);
        messages.add(message);
      }
      WidgetsBinding.instance
          .addPostFrameCallback((_){
        if (_scrollController.hasClients) {
          _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
        }
      });
      return messages;
    } else {
      throw Exception('Failed to load album');
    }
   }

У меня так много проблем с попыткой использовать контроллер прокрутки для перехода в конец списка, что я использую другой подход.

Вместо создания события для отправки списка в конец я меняю свою логику на использование перевернутого списка.

Итак, каждый раз, когда у меня появляется новый элемент, я просто вставляю его в начало списка.

// add new message at the begin of the list 
list.insert(0, message);
// ...

// pull items from the database
list = await bean.getAllReversed(); // basically a method that applies a descendent order

// I remove the scroll controller
new Flexible(
  child: new ListView.builder(
    reverse: true, 
    key: new Key(model.count().toString()),
    itemCount: model.count(),
    itemBuilder: (context, i) => ChatItem.displayMessage(model.getItem(i))
  ),
),

2022 WebDevInsider