Last active
July 22, 2023 09:18
-
-
Save kekland/7d0e1257957a3bf84b4f033377635eb0 to your computer and use it in GitHub Desktop.
Precomputed layout extent for items in a long ListView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:math'; | |
import 'package:flutter/material.dart'; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
// This widget is the root of your application. | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Flutter Demo', | |
theme: ThemeData( | |
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), | |
useMaterial3: true, | |
), | |
home: const MyHomePage(), | |
); | |
} | |
} | |
class MyHomePage extends StatefulWidget { | |
const MyHomePage({super.key}); | |
@override | |
State<MyHomePage> createState() => _MyHomePageState(); | |
} | |
class _MyHomePageState extends State<MyHomePage> { | |
List<String>? _items; | |
bool _isLoading = false; | |
double _horizontalExtent = 0; | |
List<double>? _cachedItemExtents; | |
ScrollController? _scrollController; | |
@override | |
void initState() { | |
super.initState(); | |
} | |
TextStyle get _textStyle => Theme.of(context).textTheme.bodyMedium!; | |
Future<void> _loadItems() async { | |
setState(() { | |
_isLoading = true; | |
}); | |
await Future.delayed(const Duration(seconds: 2)); | |
final random = Random(); | |
_items = List.generate(500, (index) { | |
final lines = random.nextInt(10) + 1; | |
// ignore: prefer_interpolation_to_compose_strings | |
return 'Item ${index + 1} - ' + 'Line\n' * lines + 'End'; | |
}); | |
_cacheItemExtents(); | |
_isLoading = false; | |
setState(() {}); | |
} | |
void _cacheItemExtents() { | |
final stopwatch = Stopwatch(); | |
stopwatch.start(); | |
final textPainter = TextPainter( | |
textDirection: TextDirection.ltr, | |
); | |
_cachedItemExtents = []; | |
for (final item in _items!) { | |
textPainter.text = TextSpan( | |
text: item, | |
style: _textStyle, | |
); | |
textPainter.layout(maxWidth: _horizontalExtent); | |
_cachedItemExtents!.add(textPainter.height); | |
} | |
_scrollController = PrecomputedScrollController( | |
itemExtents: _cachedItemExtents!, | |
); | |
textPainter.dispose(); | |
// _scrollController = ScrollController(); | |
stopwatch.stop(); | |
debugPrint('Caching took ${stopwatch.elapsedMilliseconds}ms'); | |
} | |
@override | |
Widget build(BuildContext context) { | |
late final Widget body; | |
if (_isLoading) { | |
body = const Center( | |
child: CircularProgressIndicator(), | |
); | |
} else if (_items == null) { | |
body = Center( | |
child: ElevatedButton( | |
onPressed: _loadItems, | |
child: const Text('Load items'), | |
), | |
); | |
} else { | |
body = ListView.builder( | |
controller: _scrollController, | |
itemCount: _items!.length, | |
itemBuilder: (context, index) { | |
return ListTile( | |
title: Text( | |
_items![index], | |
style: _textStyle, | |
), | |
); | |
}, | |
); | |
} | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('Flutter Demo Home Page'), | |
), | |
body: LayoutBuilder( | |
builder: (context, constraints) { | |
_horizontalExtent = constraints.maxWidth; | |
return body; | |
}, | |
), | |
); | |
} | |
} | |
class PrecomputedScrollController extends ScrollController { | |
PrecomputedScrollController({ | |
required this.itemExtents, | |
}); | |
final List<double> itemExtents; | |
@override | |
ScrollPosition createScrollPosition( | |
ScrollPhysics physics, | |
ScrollContext context, | |
ScrollPosition? oldPosition, | |
) { | |
return PrecomputedScrollPosition( | |
itemExtents: itemExtents, | |
physics: physics, | |
context: context, | |
initialPixels: initialScrollOffset, | |
keepScrollOffset: keepScrollOffset, | |
oldPosition: oldPosition, | |
debugLabel: debugLabel, | |
); | |
} | |
} | |
class PrecomputedScrollPosition extends ScrollPositionWithSingleContext { | |
PrecomputedScrollPosition({ | |
required this.itemExtents, | |
required super.physics, | |
required super.context, | |
super.debugLabel, | |
super.initialPixels, | |
super.keepScrollOffset, | |
super.oldPosition, | |
}); | |
final List<double> itemExtents; | |
@override | |
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { | |
final maxScrollExtent = itemExtents.reduce((a, b) => a + b); | |
return super.applyContentDimensions( | |
minScrollExtent, | |
max(0, maxScrollExtent - viewportDimension), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment