Flutter 滾動元件內容更新時自動定位到底端的方法

shirnewei發表於2021-07-06

問題:

在使用Flutter的ListView等滾動元件做歷史記錄或日誌記錄時,需要在更新內容後立即定位到內容底部(預設是在頂部的)。

尋找解決方案:

  1. ListView有一個reverse屬性,如果把內容倒置,倒是可以總顯示新內容。

但問題是內容少的時候,是直接靠底部顯示的,這樣看起來比較怪。

  1. ListView的ScrollController本身提供了畫素滾動的方法,可以計算視口高度和內容總高度,然後呼叫Controller的jumpTo或animateTo

這是一個臨時解決方案,需要測量出當前視口的高度,然後寫在程式碼裡,內容的高度根據子元素數目計算,需要子元素內容沒太大差異,等高的情況就好控制

在Windows下效果尚可,但到蘋果裝置上由於物理滾動的特性,會很跳脫

  1. 找到一個新Controller可以控制滾動到當前選中的元件FixedExtentScrollController

原來是場誤會,這個控制器是專門給新元件ListWheelScrollView用的,這是一個齒輪滾動選擇的元件,被選中項總是在中間

最終方案:

最後通過print ScrollController.position這個屬性,發現它在執行中其實是ScrollPositionWithSingleContext的例項,這個例項可以直接獲取到滾動元件的視口高度,可滾動範圍等引數。

image.png

演示程式碼

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo For ListView'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  ScrollController controller;

  @override
  void initState() {
    super.initState();
    controller = ScrollController();
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
    Future.delayed(Duration(milliseconds: 16)).then((value) =>
        controller.animateTo(
            controller.position.maxScrollExtent,
            duration: Duration(milliseconds: 200),
            curve: Curves.easeOutQuart));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Container(
          width: 150,
          height: 300,
          decoration: BoxDecoration(
              border: Border.all(color: Colors.black26, width: 0.5)),
          child: ListView(
            controller: controller,
            padding: EdgeInsets.all(10),
            children: List<Widget>.generate(
                _counter, (index) => Text('This is row $index')),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
複製程式碼

這裡要注意的一點是,在加入新元素中呼叫滾動,是不能立即滾動的(程式碼中採用了延遲16ms,一幀的時間),否則就會出現滾動不到底的情況。因為加入元素是一個資料操作,要更新到介面需要等待下一次build,如果這個新元素還有入場動畫,那麼呼叫滾動的延遲最好delay到動畫結束,否則獲取的maxScrollExtend是不準確的。

相關文章