From 94a1fe0bb87ddad1e6f321a68b29576d870ec768 Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Wed, 10 May 2023 15:54:41 -0700 Subject: [PATCH 1/4] Use SliverList and cache content height --- .../material_3_demo/lib/component_screen.dart | 170 +++++++++++++++--- experimental/material_3_demo/lib/home.dart | 5 - material_3_demo/lib/component_screen.dart | 170 +++++++++++++++--- material_3_demo/lib/home.dart | 5 - 4 files changed, 288 insertions(+), 62 deletions(-) diff --git a/experimental/material_3_demo/lib/component_screen.dart b/experimental/material_3_demo/lib/component_screen.dart index e4428038033..deea008bdbf 100644 --- a/experimental/material_3_demo/lib/component_screen.dart +++ b/experimental/material_3_demo/lib/component_screen.dart @@ -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 children = [ + const Actions(), + colDivider, + const Communication(), + colDivider, + const Containment(), + if (!showSecondList) ...[ + colDivider, + Navigation(scaffoldKey: scaffoldKey), + colDivider, + const Selection(), + colDivider, + const TextInputs() + ], + ]; + List 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,121 @@ class SecondComponentList extends StatelessWidget { @override Widget build(BuildContext context) { + List children = [ + Navigation(scaffoldKey: scaffoldKey), + colDivider, + const Selection(), + colDivider, + const TextInputs(), + ]; + List heights = List.filled(children.length, null); + // Fully traverse this list before moving on. return FocusTraversalGroup( - child: ListView( - padding: const EdgeInsetsDirectional.only(end: smallSpacing), - children: [ - 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], + ); + } + ), + ), + ), ], ), ); } } +// Based on the fact that the list content is fixed, we cache +// the height of the content during the first-time scrolling. +// The cached height can be used to override +// `SliverChildDelegate.estimateMaxScrollOffset`, to avoid a shaking scrollbar. +class _CacheHeight extends SingleChildRenderObjectWidget { + const _CacheHeight({ + super.child, + required this.heights, + required this.index, + }); + + final List 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 heights, + required int index, + }) : _heights = heights, + _index = index, + super(); + + List _heights; + List get heights => _heights; + set heights(List 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 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}); diff --git a/experimental/material_3_demo/lib/home.dart b/experimental/material_3_demo/lib/home.dart index cf24b657c63..382a64956ca 100644 --- a/experimental/material_3_demo/lib/home.dart +++ b/experimental/material_3_demo/lib/home.dart @@ -129,11 +129,6 @@ class _HomeState extends State with SingleTickerProviderStateMixin { return const TypographyScreen(); case ScreenSelected.elevation: return const ElevationScreen(); - default: - return FirstComponentList( - showNavBottomBar: showNavBarExample, - scaffoldKey: scaffoldKey, - showSecondList: showMediumSizeLayout || showLargeSizeLayout); } } diff --git a/material_3_demo/lib/component_screen.dart b/material_3_demo/lib/component_screen.dart index e4428038033..287dbe15aff 100644 --- a/material_3_demo/lib/component_screen.dart +++ b/material_3_demo/lib/component_screen.dart @@ -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 children = [ + const Actions(), + colDivider, + const Communication(), + colDivider, + const Containment(), + if (!showSecondList) ...[ + colDivider, + Navigation(scaffoldKey: scaffoldKey), + colDivider, + const Selection(), + colDivider, + const TextInputs() + ], + ]; + List 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,121 @@ class SecondComponentList extends StatelessWidget { @override Widget build(BuildContext context) { + List children = [ + Navigation(scaffoldKey: scaffoldKey), + colDivider, + const Selection(), + colDivider, + const TextInputs(), + ]; + List heights = List.filled(children.length, null); + // Fully traverse this list before moving on. return FocusTraversalGroup( - child: ListView( - padding: const EdgeInsetsDirectional.only(end: smallSpacing), - children: [ - 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], + ); + } + ), + ), + ), ], ), ); } } +// Based on the fact that the list content is fixed, we cache +// the height of the content during the first-time scrolling. +// The cached height can be used to override +// `SliverChildDelegate.estimateMaxScrollOffset`, to avoid a shaking scrollbar. +class _CacheHeight extends SingleChildRenderObjectWidget { + const _CacheHeight({ + super.child, + required this.heights, + required this.index, + }); + + final List 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 heights, + required int index, + }) : _heights = heights, + _index = index, + super(); + + List _heights; + List get heights => _heights; + set heights(List 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 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}); diff --git a/material_3_demo/lib/home.dart b/material_3_demo/lib/home.dart index cf24b657c63..382a64956ca 100644 --- a/material_3_demo/lib/home.dart +++ b/material_3_demo/lib/home.dart @@ -129,11 +129,6 @@ class _HomeState extends State with SingleTickerProviderStateMixin { return const TypographyScreen(); case ScreenSelected.elevation: return const ElevationScreen(); - default: - return FirstComponentList( - showNavBottomBar: showNavBarExample, - scaffoldKey: scaffoldKey, - showSecondList: showMediumSizeLayout || showLargeSizeLayout); } } From 8e41248bd03074d5739f943a9f5a2eb0b2bbd0f4 Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Wed, 10 May 2023 16:59:37 -0700 Subject: [PATCH 2/4] Run dart format command --- .../material_3_demo/lib/component_screen.dart | 54 +++++++++---------- material_3_demo/lib/component_screen.dart | 16 +++--- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/experimental/material_3_demo/lib/component_screen.dart b/experimental/material_3_demo/lib/component_screen.dart index deea008bdbf..764ff4c0b0b 100644 --- a/experimental/material_3_demo/lib/component_screen.dart +++ b/experimental/material_3_demo/lib/component_screen.dart @@ -49,22 +49,19 @@ class FirstComponentList extends StatelessWidget { 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], - ); - } - ) - ) - ), + 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], + ); + }))), ], ), ); @@ -98,15 +95,14 @@ class SecondComponentList extends StatelessWidget { padding: const EdgeInsetsDirectional.only(end: smallSpacing), sliver: SliverList( delegate: BuildSlivers( - heights: heights, - builder: (context, index) { - return _CacheHeight( - heights: heights, - index: index, - child: children[index], - ); - } - ), + heights: heights, + builder: (context, index) { + return _CacheHeight( + heights: heights, + index: index, + child: children[index], + ); + }), ), ), ], @@ -138,7 +134,8 @@ class _CacheHeight extends SingleChildRenderObjectWidget { } @override - void updateRenderObject(BuildContext context, _RenderCacheHeight renderObject) { + void updateRenderObject( + BuildContext context, _RenderCacheHeight renderObject) { renderObject ..heights = heights ..index = index; @@ -149,7 +146,7 @@ class _RenderCacheHeight extends RenderProxyBox { _RenderCacheHeight({ required List heights, required int index, - }) : _heights = heights, + }) : _heights = heights, _index = index, super(); @@ -191,7 +188,8 @@ class BuildSlivers extends SliverChildBuilderDelegate { final List heights; @override - double? estimateMaxScrollOffset(int firstIndex, int lastIndex, double leadingScrollOffset, double trailingScrollOffset) { + double? estimateMaxScrollOffset(int firstIndex, int lastIndex, + double leadingScrollOffset, double trailingScrollOffset) { return heights.reduce((sum, height) => (sum ?? 0) + (height ?? 0))!; } } diff --git a/material_3_demo/lib/component_screen.dart b/material_3_demo/lib/component_screen.dart index 287dbe15aff..764ff4c0b0b 100644 --- a/material_3_demo/lib/component_screen.dart +++ b/material_3_demo/lib/component_screen.dart @@ -61,10 +61,7 @@ class FirstComponentList extends StatelessWidget { index: index, child: children[index], ); - } - ) - ) - ), + }))), ], ), ); @@ -105,8 +102,7 @@ class SecondComponentList extends StatelessWidget { index: index, child: children[index], ); - } - ), + }), ), ), ], @@ -138,7 +134,8 @@ class _CacheHeight extends SingleChildRenderObjectWidget { } @override - void updateRenderObject(BuildContext context, _RenderCacheHeight renderObject) { + void updateRenderObject( + BuildContext context, _RenderCacheHeight renderObject) { renderObject ..heights = heights ..index = index; @@ -149,7 +146,7 @@ class _RenderCacheHeight extends RenderProxyBox { _RenderCacheHeight({ required List heights, required int index, - }) : _heights = heights, + }) : _heights = heights, _index = index, super(); @@ -191,7 +188,8 @@ class BuildSlivers extends SliverChildBuilderDelegate { final List heights; @override - double? estimateMaxScrollOffset(int firstIndex, int lastIndex, double leadingScrollOffset, double trailingScrollOffset) { + double? estimateMaxScrollOffset(int firstIndex, int lastIndex, + double leadingScrollOffset, double trailingScrollOffset) { return heights.reduce((sum, height) => (sum ?? 0) + (height ?? 0))!; } } From e27caadce407938b9e332b0dd0096950eb3f451c Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Wed, 10 May 2023 17:22:09 -0700 Subject: [PATCH 3/4] Add comma after parenthesis and run formatter --- .../material_3_demo/lib/component_screen.dart | 46 ++++++++++--------- material_3_demo/lib/component_screen.dart | 46 ++++++++++--------- 2 files changed, 50 insertions(+), 42 deletions(-) diff --git a/experimental/material_3_demo/lib/component_screen.dart b/experimental/material_3_demo/lib/component_screen.dart index 764ff4c0b0b..9d857ba806b 100644 --- a/experimental/material_3_demo/lib/component_screen.dart +++ b/experimental/material_3_demo/lib/component_screen.dart @@ -49,19 +49,22 @@ class FirstComponentList extends StatelessWidget { 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], - ); - }))), + 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], + ); + }, + ), + ), + ), ], ), ); @@ -95,14 +98,15 @@ class SecondComponentList extends StatelessWidget { padding: const EdgeInsetsDirectional.only(end: smallSpacing), sliver: SliverList( delegate: BuildSlivers( - heights: heights, - builder: (context, index) { - return _CacheHeight( - heights: heights, - index: index, - child: children[index], - ); - }), + heights: heights, + builder: (context, index) { + return _CacheHeight( + heights: heights, + index: index, + child: children[index], + ); + }, + ), ), ), ], diff --git a/material_3_demo/lib/component_screen.dart b/material_3_demo/lib/component_screen.dart index 764ff4c0b0b..9d857ba806b 100644 --- a/material_3_demo/lib/component_screen.dart +++ b/material_3_demo/lib/component_screen.dart @@ -49,19 +49,22 @@ class FirstComponentList extends StatelessWidget { 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], - ); - }))), + 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], + ); + }, + ), + ), + ), ], ), ); @@ -95,14 +98,15 @@ class SecondComponentList extends StatelessWidget { padding: const EdgeInsetsDirectional.only(end: smallSpacing), sliver: SliverList( delegate: BuildSlivers( - heights: heights, - builder: (context, index) { - return _CacheHeight( - heights: heights, - index: index, - child: children[index], - ); - }), + heights: heights, + builder: (context, index) { + return _CacheHeight( + heights: heights, + index: index, + child: children[index], + ); + }, + ), ), ), ], From 50e5acac43f1ef905bf5b0ffb02135e8fce2f571 Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Thu, 11 May 2023 14:07:24 -0700 Subject: [PATCH 4/4] Address pr comments --- .../material_3_demo/lib/component_screen.dart | 13 +++++++++---- material_3_demo/lib/component_screen.dart | 13 +++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/experimental/material_3_demo/lib/component_screen.dart b/experimental/material_3_demo/lib/component_screen.dart index 9d857ba806b..eb94f18c09c 100644 --- a/experimental/material_3_demo/lib/component_screen.dart +++ b/experimental/material_3_demo/lib/component_screen.dart @@ -115,10 +115,15 @@ class SecondComponentList extends StatelessWidget { } } -// Based on the fact that the list content is fixed, we cache -// the height of the content during the first-time scrolling. -// The cached height can be used to override -// `SliverChildDelegate.estimateMaxScrollOffset`, to avoid a shaking scrollbar. +// 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, diff --git a/material_3_demo/lib/component_screen.dart b/material_3_demo/lib/component_screen.dart index 9d857ba806b..eb94f18c09c 100644 --- a/material_3_demo/lib/component_screen.dart +++ b/material_3_demo/lib/component_screen.dart @@ -115,10 +115,15 @@ class SecondComponentList extends StatelessWidget { } } -// Based on the fact that the list content is fixed, we cache -// the height of the content during the first-time scrolling. -// The cached height can be used to override -// `SliverChildDelegate.estimateMaxScrollOffset`, to avoid a shaking scrollbar. +// 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,