Use SliverList and cache content height (#1802)

pull/1817/head
Qun Cheng 2 years ago committed by GitHub
parent 3dc607a695
commit 1d6e5a0ece
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
const rowDivider = SizedBox(width: 20);
@ -26,26 +27,44 @@ class FirstComponentList extends StatelessWidget {
@override
Widget build(BuildContext context) {
List<Widget> children = [
const Actions(),
colDivider,
const Communication(),
colDivider,
const Containment(),
if (!showSecondList) ...[
colDivider,
Navigation(scaffoldKey: scaffoldKey),
colDivider,
const Selection(),
colDivider,
const TextInputs()
],
];
List<double?> heights = List.filled(children.length, null);
// Fully traverse this list before moving on.
return FocusTraversalGroup(
child: ListView(
padding: showSecondList
? const EdgeInsetsDirectional.only(end: smallSpacing)
: EdgeInsets.zero,
children: [
const Actions(),
colDivider,
const Communication(),
colDivider,
const Containment(),
if (!showSecondList) ...[
colDivider,
Navigation(scaffoldKey: scaffoldKey),
colDivider,
const Selection(),
colDivider,
const TextInputs()
],
child: CustomScrollView(
slivers: [
SliverPadding(
padding: showSecondList
? const EdgeInsetsDirectional.only(end: smallSpacing)
: EdgeInsets.zero,
sliver: SliverList(
delegate: BuildSlivers(
heights: heights,
builder: (context, index) {
return _CacheHeight(
heights: heights,
index: index,
child: children[index],
);
},
),
),
),
],
),
);
@ -62,22 +81,128 @@ class SecondComponentList extends StatelessWidget {
@override
Widget build(BuildContext context) {
List<Widget> children = [
Navigation(scaffoldKey: scaffoldKey),
colDivider,
const Selection(),
colDivider,
const TextInputs(),
];
List<double?> heights = List.filled(children.length, null);
// Fully traverse this list before moving on.
return FocusTraversalGroup(
child: ListView(
padding: const EdgeInsetsDirectional.only(end: smallSpacing),
children: <Widget>[
Navigation(scaffoldKey: scaffoldKey),
colDivider,
const Selection(),
colDivider,
const TextInputs(),
child: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsetsDirectional.only(end: smallSpacing),
sliver: SliverList(
delegate: BuildSlivers(
heights: heights,
builder: (context, index) {
return _CacheHeight(
heights: heights,
index: index,
child: children[index],
);
},
),
),
),
],
),
);
}
}
// If the content of a CustomScrollView does not change, then it's
// safe to cache the heights of each item as they are laid out. The
// sum of the cached heights are returned by an override of
// `SliverChildDelegate.estimateMaxScrollOffset`. The default version
// of this method bases its estimate on the average height of the
// visible items. The override ensures that the scrollbar thumb's
// size, which depends on the max scroll offset, will shrink smoothly
// as the contents of the list are exposed for the first time, and
// then remain fixed.
class _CacheHeight extends SingleChildRenderObjectWidget {
const _CacheHeight({
super.child,
required this.heights,
required this.index,
});
final List<double?> heights;
final int index;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderCacheHeight(
heights: heights,
index: index,
);
}
@override
void updateRenderObject(
BuildContext context, _RenderCacheHeight renderObject) {
renderObject
..heights = heights
..index = index;
}
}
class _RenderCacheHeight extends RenderProxyBox {
_RenderCacheHeight({
required List<double?> heights,
required int index,
}) : _heights = heights,
_index = index,
super();
List<double?> _heights;
List<double?> get heights => _heights;
set heights(List<double?> value) {
if (value == _heights) {
return;
}
_heights = value;
markNeedsLayout();
}
int _index;
int get index => _index;
set index(int value) {
if (value == index) {
return;
}
_index = value;
markNeedsLayout();
}
@override
void performLayout() {
super.performLayout();
heights[index] = size.height;
}
}
// The heights information is used to override the `estimateMaxScrollOffset` and
// provide a more accurate estimation for the max scroll offset.
class BuildSlivers extends SliverChildBuilderDelegate {
BuildSlivers({
required NullableIndexedWidgetBuilder builder,
required this.heights,
}) : super(builder, childCount: heights.length);
final List<double?> heights;
@override
double? estimateMaxScrollOffset(int firstIndex, int lastIndex,
double leadingScrollOffset, double trailingScrollOffset) {
return heights.reduce((sum, height) => (sum ?? 0) + (height ?? 0))!;
}
}
class Actions extends StatelessWidget {
const Actions({super.key});

@ -129,11 +129,6 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
return const TypographyScreen();
case ScreenSelected.elevation:
return const ElevationScreen();
default:
return FirstComponentList(
showNavBottomBar: showNavBarExample,
scaffoldKey: scaffoldKey,
showSecondList: showMediumSizeLayout || showLargeSizeLayout);
}
}

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
const rowDivider = SizedBox(width: 20);
@ -26,26 +27,44 @@ class FirstComponentList extends StatelessWidget {
@override
Widget build(BuildContext context) {
List<Widget> children = [
const Actions(),
colDivider,
const Communication(),
colDivider,
const Containment(),
if (!showSecondList) ...[
colDivider,
Navigation(scaffoldKey: scaffoldKey),
colDivider,
const Selection(),
colDivider,
const TextInputs()
],
];
List<double?> heights = List.filled(children.length, null);
// Fully traverse this list before moving on.
return FocusTraversalGroup(
child: ListView(
padding: showSecondList
? const EdgeInsetsDirectional.only(end: smallSpacing)
: EdgeInsets.zero,
children: [
const Actions(),
colDivider,
const Communication(),
colDivider,
const Containment(),
if (!showSecondList) ...[
colDivider,
Navigation(scaffoldKey: scaffoldKey),
colDivider,
const Selection(),
colDivider,
const TextInputs()
],
child: CustomScrollView(
slivers: [
SliverPadding(
padding: showSecondList
? const EdgeInsetsDirectional.only(end: smallSpacing)
: EdgeInsets.zero,
sliver: SliverList(
delegate: BuildSlivers(
heights: heights,
builder: (context, index) {
return _CacheHeight(
heights: heights,
index: index,
child: children[index],
);
},
),
),
),
],
),
);
@ -62,22 +81,128 @@ class SecondComponentList extends StatelessWidget {
@override
Widget build(BuildContext context) {
List<Widget> children = [
Navigation(scaffoldKey: scaffoldKey),
colDivider,
const Selection(),
colDivider,
const TextInputs(),
];
List<double?> heights = List.filled(children.length, null);
// Fully traverse this list before moving on.
return FocusTraversalGroup(
child: ListView(
padding: const EdgeInsetsDirectional.only(end: smallSpacing),
children: <Widget>[
Navigation(scaffoldKey: scaffoldKey),
colDivider,
const Selection(),
colDivider,
const TextInputs(),
child: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsetsDirectional.only(end: smallSpacing),
sliver: SliverList(
delegate: BuildSlivers(
heights: heights,
builder: (context, index) {
return _CacheHeight(
heights: heights,
index: index,
child: children[index],
);
},
),
),
),
],
),
);
}
}
// If the content of a CustomScrollView does not change, then it's
// safe to cache the heights of each item as they are laid out. The
// sum of the cached heights are returned by an override of
// `SliverChildDelegate.estimateMaxScrollOffset`. The default version
// of this method bases its estimate on the average height of the
// visible items. The override ensures that the scrollbar thumb's
// size, which depends on the max scroll offset, will shrink smoothly
// as the contents of the list are exposed for the first time, and
// then remain fixed.
class _CacheHeight extends SingleChildRenderObjectWidget {
const _CacheHeight({
super.child,
required this.heights,
required this.index,
});
final List<double?> heights;
final int index;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderCacheHeight(
heights: heights,
index: index,
);
}
@override
void updateRenderObject(
BuildContext context, _RenderCacheHeight renderObject) {
renderObject
..heights = heights
..index = index;
}
}
class _RenderCacheHeight extends RenderProxyBox {
_RenderCacheHeight({
required List<double?> heights,
required int index,
}) : _heights = heights,
_index = index,
super();
List<double?> _heights;
List<double?> get heights => _heights;
set heights(List<double?> value) {
if (value == _heights) {
return;
}
_heights = value;
markNeedsLayout();
}
int _index;
int get index => _index;
set index(int value) {
if (value == index) {
return;
}
_index = value;
markNeedsLayout();
}
@override
void performLayout() {
super.performLayout();
heights[index] = size.height;
}
}
// The heights information is used to override the `estimateMaxScrollOffset` and
// provide a more accurate estimation for the max scroll offset.
class BuildSlivers extends SliverChildBuilderDelegate {
BuildSlivers({
required NullableIndexedWidgetBuilder builder,
required this.heights,
}) : super(builder, childCount: heights.length);
final List<double?> heights;
@override
double? estimateMaxScrollOffset(int firstIndex, int lastIndex,
double leadingScrollOffset, double trailingScrollOffset) {
return heights.reduce((sum, height) => (sum ?? 0) + (height ?? 0))!;
}
}
class Actions extends StatelessWidget {
const Actions({super.key});

@ -129,11 +129,6 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
return const TypographyScreen();
case ScreenSelected.elevation:
return const ElevationScreen();
default:
return FirstComponentList(
showNavBottomBar: showNavBarExample,
scaffoldKey: scaffoldKey,
showSecondList: showMediumSizeLayout || showLargeSizeLayout);
}
}

Loading…
Cancel
Save