Програмне прокручування до кінця ListView


98

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

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

Чи є простий спосіб цього досягти?

Використання reverse: trueдля мене не є ідеальним рішенням, тому що я хотів би, щоб елементи були вирівняні вгорі, коли в області перегляду є лише невелика кількість елементів ListView.

Відповіді:


96

Якщо ви використовуєте упакували 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<MyHomePage> {
  List<Widget> _messages = <Widget>[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),
          );
        }
      ),
    );
  }
}

1
Це зробило фокус. У моєму конкретному випадку мені довелося використовувати вкладені стовпці, щоб розмістити ListView у розгорнутому віджеті, але основна ідея така ж.
yyoon

19
Я прийшов шукати рішення, просто хотів зазначити, що інша відповідь може бути кращим способом досягти цього - stackoverflow.com/questions/44141148/…
Анімеш Джейн

6
Я згоден, що відповідь (яку я також написав), безумовно, краща.
Коллін Джексон,

1
@Dennis Так, щоб ListView починався в зворотному порядку, який знаходиться знизу.
CopsOnRoad

1
Відповідь від @CopsOnRoad виглядає краще, і ця відповідь більше нагадує хакерство, ніж рішення.
Рупак А Нелліат,

118

Використання 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,
);

4
Щодо бази даних Firebase у реальному часі я досі зміг уникнути 250 мс (що, мабуть, дійсно довго). Цікаво, як низько ми можемо піти? Проблема полягає у тому, що maxScrollExtent потрібно дочекатися завершення побудови віджета з додаванням нового елемента. Як профілювати середню тривалість від зворотного виклику до завершення побудови?
SacWebDeveloper

2
Чи вважаєте ви, що це буде належним чином працювати з використанням конструктора потоків для отримання даних з Firebase? Також додаєте повідомлення чату на кнопку надсилання? Також, якщо Інтернет буде повільним, це спрацює? що-небудь краще, якщо ви можете запропонувати. Дякую.
Jay Mungara

@SacWebDeveloper, яким був ваш підхід до цієї проблеми та Firebase?
Ідріс Стек

@IdrisStack Насправді 250 мс, очевидно, недостатньо довгий, оскільки іноді стрибок відбувається до того, як відбудеться оновлення. Я думаю, кращим підходом було б порівняти довжину списку після оновлення та перейти до нижньої частини, якщо довжина на одну більша.
SacWebDeveloper

Дякую @SacWebDeveloper, ваш підхід працює, можливо, я можу задати вам ще одне запитання в контексті Firestore, це може відвернути тему. Qn. Чому коли я надсилаю повідомлення комусь, список не прокручується автоматично донизу, чи є спосіб прослухати повідомлення та прокрутити донизу?
Ідріс Стек

13

listViewScrollController.animateTo(listViewScrollController.position.maxScrollExtent) це найпростіший спосіб.


1
Привіт Джеку, дякую за вашу відповідь, я вважаю, що я написав те саме, тому немає переваги додавати те саме рішення знову IMHO.
CopsOnRoad

1
Я використовував цей підхід, але вам потрібно зачекати, поки це зробить кілька мс, тому що перед запуском animateTo (...) потрібно зробити візуалізацію екрана. Можливо, ми маємо хороший спосіб використовувати хороші практики, щоб зробити це без злому.
Ренан Коельо

1
Це правильна відповідь. Він прокрутить ваш список донизу, якщо ви +100 у поточному рядку. наприклад listViewScrollController.position.maxScrollExtent + 100;
Різван Ансар,

1
Натхненно @RenanCoelho, я загорнув цей рядок коду в таймер із затримкою в 100 мілісекунд.
ymerdrengene

5

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

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

2

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

Замість того, щоб створювати подію для надсилання списку внизу, я міняю свою логіку, використовуючи зворотний список.

Отже, кожного разу, коли у мене з’являється новий предмет, я просто роблю його при вставці вгорі списку.

// 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))
  ),
),

2
Єдина проблема, яка в мене виникає з цим, полягає в тому, що будь-які смуги прокрутки працюють назад. Коли я прокручую вниз, вони рухаються вгору.
ThinkDigital

1

Не розміщуйте widgetBinding у initstate, натомість вам потрібно помістити його в метод, який отримує ваші дані з бази даних. наприклад, ось так. Якщо його вставити в initstate, scrollcontrollerвін не буде доданий до жодного списку.

    Future<List<Message>> fetchMessage() async {

    var res = await Api().getData("message");
    var body = json.decode(res.body);
    if (res.statusCode == 200) {
      List<Message> 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');
    }
   }

-2

Ви можете використовувати це, де 0.09*heightє висота рядка у списку і _controllerвизначається так_controller = ScrollController();

(BuildContext context, int pos) {
    if(pos != 0) {
        _controller.animateTo(0.09 * height * (pos - 1), 
                              curve: Curves.easeInOut,
                              duration: Duration(milliseconds: 1400));
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.