From ad46d0363b0b47d332fc934debc5dc8a3bc97f87 Mon Sep 17 00:00:00 2001 From: Kenzie Schmoll Date: Thu, 15 Sep 2022 14:13:40 -0700 Subject: [PATCH 1/2] Reduce Widget rebuilds on WonderEditorialScreen --- lib/ui/common/scaling_list_item.dart | 1 + .../screens/editorial/editorial_screen.dart | 80 +++++++++++++++---- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/lib/ui/common/scaling_list_item.dart b/lib/ui/common/scaling_list_item.dart index 5aaa7b40..b0906792 100644 --- a/lib/ui/common/scaling_list_item.dart +++ b/lib/ui/common/scaling_list_item.dart @@ -45,6 +45,7 @@ class ScalingListItem extends StatelessWidget { scrollPos: scrollPos, builder: (_, pctVisible) { final scale = 1.35 - pctVisible * .35; + // TODO: consider returning a SizedBox when scale == 0. return ClipRect( child: Transform.scale(scale: scale, child: child), ); diff --git a/lib/ui/screens/editorial/editorial_screen.dart b/lib/ui/screens/editorial/editorial_screen.dart index d7d5dcf5..f4b62953 100644 --- a/lib/ui/screens/editorial/editorial_screen.dart +++ b/lib/ui/screens/editorial/editorial_screen.dart @@ -50,6 +50,36 @@ class _WonderEditorialScreenState extends State { final _scrollToPopThreshold = 50; bool _isPointerDown = false; + /// The largest scroll position at which we should show the colored background + /// widget. + static const _includeBackgroundThreshold = 1000; + + /// Whether the colored background widget should be included in this view. + /// + /// This value should be true for scroll positions ranging from 0 to 1000, and + /// should be false for all values larger. + final _includeBackground = ValueNotifier(true); + + /// The largest scroll position at which we should show the top illustration. + static const _includeTopIllustrationThreshold = 700; + + /// The opacity value for the top illustration. + /// + /// This value should be clamped to (0, 1) and will shrink to 0 as the scroll + /// position increases to [_includeTopIllustrationThreshold]. + final _topIllustrationOpacity = ValueNotifier(1.0); + + /// The largest scroll position at which we should show the text content. + static const _includeTextThreshold = 500.0; + + /// The scroll position notifier that the text display widget below should + /// listen to. + /// + /// This value should be clamped to (0, [_includeTextThreshold]). This scroll + /// position value determines the opacity of the text, which decreases to 0.0 + /// as the scroll position increases to [_includeTextThreshold]. + final _scrollPositionForTextContent = ValueNotifier(0.0); + @override void dispose() { _scroller.dispose(); @@ -60,6 +90,23 @@ class _WonderEditorialScreenState extends State { void _handleScrollChanged() { _scrollPos.value = _scroller.position.pixels; widget.onScroll.call(_scrollPos.value); + + _includeBackground.value = _scrollPos.value <= _includeBackgroundThreshold; + + // Opacity value between 0 and 1, based on the amt scrolled. Once + // [_topIllustrationOpacity.value] reaches its clamped ends (0 or 1), it + // will not notify on subsequent assigments to the same value, which + // prevents us from triggering an unnecessary rebuild on Widgets that are + // listening to this notifier. + _topIllustrationOpacity.value = (1 - _scrollPos.value / _includeTopIllustrationThreshold).clamp(0, 1); + + // We clamp to [_includeTextThreshold] so that we do not trigger unnecessary + // rebuilds on Widgets that are listening to this notifier. At a scroll + // position of [_includeTextThreshold] and beyond, we would be rendering the + // text with an opacity value of 0.0, and there is no point in doing any + // building or rendering for a transparent item. + _scrollPositionForTextContent.value = _scrollPos.value.clamp(0, _includeTextThreshold); + // If user pulls far down on the elastic list, pop back to if (_scrollPos.value < -_scrollToPopThreshold) { if (_isPointerDown) { @@ -86,14 +133,13 @@ class _WonderEditorialScreenState extends State { child: Stack( children: [ /// Background - Positioned.fill( - child: ValueListenableBuilder( - valueListenable: _scrollPos, - builder: (_, value, __) { - return Container( - color: widget.data.type.bgColor.withOpacity(_scrollPos.value > 1000 ? 0 : 1), - ); - }, + ValueListenableBuilder( + valueListenable: _includeBackground, + builder: (context, include, child) => include ? child! : const SizedBox(), + child: Positioned.fill( + child: Container( + color: widget.data.type.bgColor, + ), ), ), @@ -101,10 +147,12 @@ class _WonderEditorialScreenState extends State { SizedBox( height: illustrationHeight, child: ValueListenableBuilder( - valueListenable: _scrollPos, - builder: (_, value, child) { - // get some value between 0 and 1, based on the amt scrolled - double opacity = (1 - value / 700).clamp(0, 1); + valueListenable: _topIllustrationOpacity, + builder: (_, opacity, child) { + if (opacity == 0) { + // No point in rendering something that is transparent. + return SizedBox(); + } return Opacity(opacity: opacity, child: child); }, // This is due to a bug: https://github.com/flutter/flutter/issues/101872 @@ -126,10 +174,14 @@ class _WonderEditorialScreenState extends State { /// Text content, animates itself to hide behind the app bar as it scrolls up SliverToBoxAdapter( child: ValueListenableBuilder( - valueListenable: _scrollPos, + valueListenable: _scrollPositionForTextContent, builder: (_, value, child) { double offsetAmt = max(0, value * .3); - double opacity = (1 - offsetAmt / 150).clamp(0, 1); + double opacity = (1 - value / _includeTextThreshold).clamp(0, 1); + if (opacity == 0) { + // No point in rendering something that is transparent. + return SizedBox(); + } return Transform.translate( offset: Offset(0, offsetAmt), child: Opacity(opacity: opacity, child: child), From 0f3e43843eb60d7cdd269c5f588c27487605259a Mon Sep 17 00:00:00 2001 From: Kenzie Schmoll Date: Thu, 15 Sep 2022 14:23:31 -0700 Subject: [PATCH 2/2] tweaks --- .../screens/editorial/editorial_screen.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/ui/screens/editorial/editorial_screen.dart b/lib/ui/screens/editorial/editorial_screen.dart index f4b62953..3ccac7a3 100644 --- a/lib/ui/screens/editorial/editorial_screen.dart +++ b/lib/ui/screens/editorial/editorial_screen.dart @@ -35,7 +35,8 @@ part 'widgets/_title_text.dart'; part 'widgets/_top_illustration.dart'; class WonderEditorialScreen extends StatefulWidget { - const WonderEditorialScreen(this.data, {Key? key, required this.onScroll}) : super(key: key); + const WonderEditorialScreen(this.data, {Key? key, required this.onScroll}) + : super(key: key); final WonderData data; final void Function(double scrollPos) onScroll; @@ -44,7 +45,8 @@ class WonderEditorialScreen extends StatefulWidget { } class _WonderEditorialScreenState extends State { - late final ScrollController _scroller = ScrollController()..addListener(_handleScrollChanged); + late final ScrollController _scroller = ScrollController() + ..addListener(_handleScrollChanged); final _scrollPos = ValueNotifier(0.0); final _sectionIndex = ValueNotifier(0); final _scrollToPopThreshold = 50; @@ -156,7 +158,8 @@ class _WonderEditorialScreenState extends State { return Opacity(opacity: opacity, child: child); }, // This is due to a bug: https://github.com/flutter/flutter/issues/101872 - child: RepaintBoundary(child: _TopIllustration(widget.data.type)), + child: RepaintBoundary( + child: _TopIllustration(widget.data.type)), ), ), @@ -205,16 +208,20 @@ class _WonderEditorialScreenState extends State { widget.data.type, scrollPos: _scrollPos, sectionIndex: _sectionIndex, - ).animate().fade(duration: $styles.times.med, delay: $styles.times.pageTransition), + ).animate().fade( + duration: $styles.times.med, + delay: $styles.times.pageTransition), ), ), /// Editorial content (text and images) - _ScrollingContent(widget.data, scrollPos: _scrollPos, sectionNotifier: _sectionIndex), + _ScrollingContent(widget.data, + scrollPos: _scrollPos, sectionNotifier: _sectionIndex), /// Bottom padding SliverToBoxAdapter( - child: Container(height: 150, color: $styles.colors.offWhite), + child: + Container(height: 150, color: $styles.colors.offWhite), ), ], ),