diff --git a/experimental/material_3_demo/lib/component_screen.dart b/experimental/material_3_demo/lib/component_screen.dart index e4428038033..eb94f18c09c 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,128 @@ 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], + ); + }, + ), + ), + ), ], ), ); } } +// 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 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..eb94f18c09c 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,128 @@ 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], + ); + }, + ), + ), + ), ], ), ); } } +// 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 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); } }