first pass at using props

This commit is contained in:
Shawn 2022-12-07 10:14:14 -07:00
parent 1f9bd92f80
commit afbd4b24db
14 changed files with 98 additions and 141 deletions

View File

@ -15,6 +15,7 @@ export 'package:provider/provider.dart';
export 'package:rnd/rnd.dart';
export 'package:simple_rich_text/simple_rich_text.dart';
export 'package:sized_context/sized_context.dart';
export 'package:stateful_props/stateful_props.dart';
export 'package:wonders/assets.dart';
export 'package:wonders/logic/app_logic.dart';
export 'package:wonders/logic/data/wonder_type.dart';

View File

@ -12,17 +12,11 @@ class FullscreenUrlImgViewer extends StatefulWidget {
State<FullscreenUrlImgViewer> createState() => _FullscreenUrlImgViewerState();
}
class _FullscreenUrlImgViewerState extends State<FullscreenUrlImgViewer> {
class _FullscreenUrlImgViewerState extends State<FullscreenUrlImgViewer> with StatefulPropsMixin {
final _isZoomed = ValueNotifier(false);
late final _controller = PageController(initialPage: widget.index);
late final _pages = PageControllerProp(this, initialPage: widget.index);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleBackPressed() => Navigator.pop(context, _controller.page!.round());
void _handleBackPressed() => Navigator.pop(context, _pages.page.round());
@override
Widget build(BuildContext context) {
@ -32,7 +26,7 @@ class _FullscreenUrlImgViewerState extends State<FullscreenUrlImgViewer> {
final bool enableSwipe = !_isZoomed.value && widget.urls.length > 1;
return PageView.builder(
physics: enableSwipe ? PageScrollPhysics() : NeverScrollableScrollPhysics(),
controller: _controller,
controller: _pages.controller,
itemCount: widget.urls.length,
itemBuilder: (_, index) => _Viewer(widget.urls[index], _isZoomed),
onPageChanged: (_) => AppHaptics.lightImpact(),
@ -69,25 +63,19 @@ class _Viewer extends StatefulWidget {
State<_Viewer> createState() => _ViewerState();
}
class _ViewerState extends State<_Viewer> with SingleTickerProviderStateMixin {
final _controller = TransformationController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
class _ViewerState extends State<_Viewer> with StatefulPropsMixin {
late final _transformation = TransformationControllerProp(this);
/// Reset zoom level to 1 on double-tap
void _handleDoubleTap() => _controller.value = Matrix4.identity();
void _handleDoubleTap() => _transformation.controller.value = Matrix4.identity();
@override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTap: _handleDoubleTap,
child: InteractiveViewer(
transformationController: _controller,
onInteractionEnd: (_) => widget.isZoomed.value = _controller.value.getMaxScaleOnAxis() > 1,
transformationController: _transformation.controller,
onInteractionEnd: (_) => widget.isZoomed.value = _transformation.matrix.getMaxScaleOnAxis() > 1,
minScale: 1,
maxScale: 5,
child: Hero(

View File

@ -153,4 +153,10 @@ class _ArtifactScreenState extends State<ArtifactCarouselScreen> {
),
);
}
@override
void dispose() {
_pageController?.dispose();
super.dispose();
}
}

View File

@ -37,10 +37,6 @@ part 'widgets/_sliding_image_stack.dart';
part 'widgets/_title_text.dart';
part 'widgets/_top_illustration.dart';
//TODO: Try and maintain 1.5 : 1 aspect ratio on the featured image
//TODO: Try and move the scrollbar all the way to the edge of the screen
//TODO: Fix arch logic (if necessary)
// or maybe remove
class WonderEditorialScreen extends StatefulWidget {
const WonderEditorialScreen(this.data, {Key? key, required this.onScroll}) : super(key: key);
final WonderData data;
@ -50,20 +46,14 @@ class WonderEditorialScreen extends StatefulWidget {
State<WonderEditorialScreen> createState() => _WonderEditorialScreenState();
}
class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
late final ScrollController _scroller = ScrollController()..addListener(_handleScrollChanged);
final _scrollPos = ValueNotifier(0.0);
final _sectionIndex = ValueNotifier(0);
@override
void dispose() {
_scroller.dispose();
super.dispose();
}
class _WonderEditorialScreenState extends State<WonderEditorialScreen> with StatefulPropsMixin {
late final _scroll = ScrollControllerProp(this, onChange: _handleScrollChanged);
late final _scrollPos = ValueNotifier(0.0);
late final _sectionIndex = ValueNotifier(0);
/// Various [ValueListenableBuilders] are mapped to the _scrollPos and will rebuild when it changes
void _handleScrollChanged() {
_scrollPos.value = _scroller.position.pixels;
_scrollPos.value = _scroll.px;
widget.onScroll.call(_scrollPos.value);
}
@ -78,7 +68,7 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
double maxAppBarHeight = min(context.widthPx, $styles.sizes.maxContentWidth1) * 1.5;
return PopRouterOnOverScroll(
controller: _scroller,
controller: _scroll.controller,
child: ColoredBox(
color: $styles.colors.offWhite,
child: Stack(
@ -91,11 +81,11 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
/// Top Illustration - Sits underneath the scrolling content, fades out as it scrolls
SizedBox(
height: illustrationHeight,
child: ValueListenableBuilder<double>(
valueListenable: _scrollPos,
builder: (_, value, child) {
child: ListenableBuilder(
listenable: _scrollPos,
builder: (_, child) {
// get some value between 0 and 1, based on the amt scrolled
double opacity = (1 - value / 700).clamp(0, 1);
double opacity = (1 - _scrollPos.value / 700).clamp(0, 1);
return Opacity(opacity: opacity, child: child);
},
// This is due to a bug: https://github.com/flutter/flutter/issues/101872
@ -109,7 +99,7 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
//width: $styles.sizes.maxContentWidth1,
child: CustomScrollView(
primary: false,
controller: _scroller,
controller: _scroll.controller,
scrollBehavior: ScrollConfiguration.of(context).copyWith(),
cacheExtent: 1000,
slivers: [
@ -120,17 +110,17 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
/// Text content, animates itself to hide behind the app bar as it scrolls up
SliverToBoxAdapter(
child: ValueListenableBuilder<double>(
valueListenable: _scrollPos,
builder: (_, value, child) {
double offsetAmt = max(0, value * .3);
child: ListenableBuilder(
listenable: _scrollPos,
builder: (_, child) {
double offsetAmt = max(0, _scrollPos.value * .3);
double opacity = (1 - offsetAmt / 150).clamp(0, 1);
return Transform.translate(
offset: Offset(0, offsetAmt),
child: Opacity(opacity: opacity, child: child),
);
},
child: _TitleText(widget.data, scroller: _scroller),
child: _TitleText(widget.data, scroller: _scroll.controller),
),
),
@ -166,7 +156,7 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
/// Home Btn
ListenableBuilder(
listenable: _scroller,
listenable: _scroll.controller,
builder: (_, child) {
return AnimatedOpacity(
opacity: _scrollPos.value > 0 ? 0 : 1,

View File

@ -62,22 +62,13 @@ class _AnimatedCircleWithText extends StatefulWidget {
State<_AnimatedCircleWithText> createState() => _AnimatedCircleWithTextState();
}
class _AnimatedCircleWithTextState extends State<_AnimatedCircleWithText> with SingleTickerProviderStateMixin {
class _AnimatedCircleWithTextState extends State<_AnimatedCircleWithText> with StatefulPropsMixin {
int _prevIndex = -1;
String get oldTitle => _prevIndex == -1 ? '' : widget.titles[_prevIndex];
String get newTitle => widget.titles[widget.index];
late final _anim = AnimationController(
vsync: this,
duration: $styles.times.med,
)..forward();
late final _anim = AnimationControllerProp(this, $styles.times.med);
bool get isAnimStopped => _anim.value == 0 || _anim.value == _anim.upperBound;
@override
void dispose() {
_anim.dispose();
super.dispose();
}
bool get isAnimStopped => _anim.value == 0 || _anim.value == _anim.controller.upperBound;
@override
void didUpdateWidget(covariant _AnimatedCircleWithText oldWidget) {
@ -86,7 +77,7 @@ class _AnimatedCircleWithTextState extends State<_AnimatedCircleWithText> with S
_prevIndex = oldWidget.index;
// If the animation is already in motion, we don't need to interrupt it, just let the text change
if (isAnimStopped) {
_anim.forward(from: 0);
_anim.controller.forward(from: 0);
}
}
super.didUpdateWidget(oldWidget);
@ -95,9 +86,10 @@ class _AnimatedCircleWithTextState extends State<_AnimatedCircleWithText> with S
@override
Widget build(_) {
return ListenableBuilder(
listenable: _anim,
listenable: _anim.controller,
builder: (_, __) {
var rot = _prevIndex > widget.index ? -pi : pi;
bool isCompleted = _anim.controller.isCompleted;
return Transform.rotate(
angle: Curves.easeInOut.transform(_anim.value) * rot,
child: Container(
@ -114,13 +106,13 @@ class _AnimatedCircleWithTextState extends State<_AnimatedCircleWithText> with S
child: Stack(
children: [
Transform.rotate(
angle: _anim.isCompleted ? rot : 0,
child: _buildCircularText(_anim.isCompleted ? newTitle : oldTitle),
angle: isCompleted ? rot : 0,
child: _buildCircularText(isCompleted ? newTitle : oldTitle),
),
if (!_anim.isCompleted) ...[
if (!isCompleted) ...[
Transform.rotate(
angle: _anim.isCompleted ? 0 : rot,
child: _buildCircularText(_anim.isCompleted ? oldTitle : newTitle),
angle: rot,
child: _buildCircularText(newTitle),
),
]
],

View File

@ -14,7 +14,7 @@ class IntroScreen extends StatefulWidget {
State<IntroScreen> createState() => _IntroScreenState();
}
class _IntroScreenState extends State<IntroScreen> {
class _IntroScreenState extends State<IntroScreen> with StatefulPropsMixin {
static const double _imageSize = 264;
static const double _logoHeight = 126;
static const double _textHeight = 155;
@ -22,15 +22,9 @@ class _IntroScreenState extends State<IntroScreen> {
static List<_PageData> pageData = [];
late final PageController _pageController = PageController()..addListener(_handlePageChanged);
late final _page = PageControllerProp(this, onChange: _handlePageChanged);
final ValueNotifier<int> _currentPage = ValueNotifier(0);
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
void _handleIntroCompletePressed() {
if (_currentPage.value == pageData.length - 1) {
context.go(ScreenPaths.home);
@ -39,13 +33,16 @@ class _IntroScreenState extends State<IntroScreen> {
}
void _handlePageChanged() {
int newPage = _pageController.page?.round() ?? 0;
int newPage = _page.controller.page?.round() ?? 0;
_currentPage.value = newPage;
}
void _handleSemanticSwipe(int dir) {
_pageController.animateToPage((_pageController.page ?? 0).round() + dir,
duration: $styles.times.fast, curve: Curves.easeOut);
_page.controller.animateToPage(
(_page.controller.page ?? 0).round() + dir,
duration: $styles.times.fast,
curve: Curves.easeOut,
);
}
@override
@ -61,9 +58,7 @@ class _IntroScreenState extends State<IntroScreen> {
// However, we only want the title / description to actually swipe,
// so we stack a PageView with that content over top of all the other
// content, and line up their layouts.
final List<Widget> pages = pageData.map((e) => _Page(data: e)).toList();
final Widget content = Stack(children: [
// page view with title & description:
MergeSemantics(
@ -71,7 +66,7 @@ class _IntroScreenState extends State<IntroScreen> {
onIncrease: () => _handleSemanticSwipe(1),
onDecrease: () => _handleSemanticSwipe(-1),
child: PageView(
controller: _pageController,
controller: _page.controller,
children: pages,
onPageChanged: (_) => AppHaptics.lightImpact(),
),
@ -118,8 +113,11 @@ class _IntroScreenState extends State<IntroScreen> {
Container(
height: _pageIndicatorHeight,
alignment: Alignment(0.0, -0.75),
child:
AppPageIndicator(count: pageData.length, controller: _pageController, color: $styles.colors.offWhite),
child: AppPageIndicator(
count: pageData.length,
controller: _page.controller,
color: $styles.colors.offWhite,
),
),
Spacer(flex: 2),
@ -213,8 +211,8 @@ class _IntroScreenState extends State<IntroScreen> {
child: Semantics(
onTapHint: $strings.introSemanticNavigate,
onTap: () {
final int current = _pageController.page!.round();
_pageController.animateToPage(current + 1, duration: 250.ms, curve: Curves.easeIn);
final int current = _page.controller.page!.round();
_page.controller.animateToPage(current + 1, duration: 250.ms, curve: Curves.easeIn);
},
child: Text($strings.introSemanticSwipeLeft, style: $styles.text.bodySmall),
),

View File

@ -1,5 +1,6 @@
part of '../timeline_screen.dart';
/// TODO: This can just be a prop??
class _ScrollingViewportController extends ChangeNotifier {
_ScrollingViewportController(this.state);
final _ScalingViewportState state;

View File

@ -24,13 +24,6 @@ class _WallpaperPhotoScreenState extends State<WallpaperPhotoScreen> {
Widget? _illustration;
bool _showTitleText = true;
Timer? _photoRetryTimer;
@override
void dispose() {
_photoRetryTimer?.cancel();
super.dispose();
}
void _handleTakePhoto(BuildContext context, String wonderName) async {
final boundary = _containerKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;

View File

@ -16,29 +16,17 @@ class WonderDetailsScreen extends StatefulWidget with GetItStatefulWidgetMixin {
State<WonderDetailsScreen> createState() => _WonderDetailsScreenState();
}
class _WonderDetailsScreenState extends State<WonderDetailsScreen>
with GetItStateMixin, SingleTickerProviderStateMixin {
late final _tabController = TabController(
class _WonderDetailsScreenState extends State<WonderDetailsScreen> with GetItStateMixin, StatefulPropsMixin {
late final _tabs = TabControllerProp(
this,
length: 4,
vsync: this,
initialIndex: widget.initialTabIndex,
)..addListener(_handleTabChanged);
AnimationController? _fade;
autoBuild: true,
);
final _detailsHasScrolled = ValueNotifier(false);
double? _tabBarHeight;
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
void _handleTabChanged() {
_fade?.forward(from: 0);
setState(() {});
}
void _handleDetailsScrolled(double scrollPos) => _detailsHasScrolled.value = scrollPos > 0;
void _handleTabMenuSized(Size size) {
@ -48,17 +36,16 @@ class _WonderDetailsScreenState extends State<WonderDetailsScreen>
@override
Widget build(BuildContext context) {
final wonder = wondersLogic.getData(widget.type);
int tabIndex = _tabController.index;
int tabIndex = _tabs.controller.index;
bool showTabBarBg = tabIndex != 1;
final tabBarHeight = _tabBarHeight ?? 0;
//final double tabBarHeight = WonderDetailsTabMenu.bottomPadding + 60;
return ColoredBox(
color: Colors.black,
child: Stack(
children: [
/// Fullscreen tab views
LazyIndexedStack(
index: _tabController.index,
index: _tabs.controller.index,
children: [
WonderEditorialScreen(wonder, onScroll: _handleDetailsScrolled),
PhotoGallery(collectionId: wonder.unsplashCollectionId, wonderType: wonder.type),
@ -74,7 +61,7 @@ class _WonderDetailsScreenState extends State<WonderDetailsScreen>
builder: (_, value, ___) => MeasurableWidget(
onChange: _handleTabMenuSized,
child: WonderDetailsTabMenu(
tabController: _tabController,
tabController: _tabs.controller,
wonderType: wonder.type,
showBg: showTabBarBg,
),

View File

@ -19,13 +19,8 @@ class _EventsList extends StatefulWidget {
State<_EventsList> createState() => _EventsListState();
}
class _EventsListState extends State<_EventsList> {
final ScrollController _scroller = ScrollController();
@override
void dispose() {
_scroller.dispose();
super.dispose();
}
class _EventsListState extends State<_EventsList> with StatefulPropsMixin {
late final _scroll = ScrollControllerProp(this);
@override
Widget build(BuildContext context) {
@ -58,7 +53,7 @@ class _EventsListState extends State<_EventsList> {
children: [
//TODO: Remove scrollbar on portrait
SingleChildScrollView(
controller: _scroller,
controller: _scroll.controller,
child: Column(
children: [
IgnorePointer(child: Gap(widget.topHeight)),
@ -104,17 +99,17 @@ class _EventsListState extends State<_EventsList> {
/// Wraps the list in a scroll listener
Widget _buildScrollingListWithBlur() {
return ListenableBuilder(
listenable: _scroller,
listenable: _scroll.controller,
child: _buildScrollingList(),
builder: (_, child) {
bool showBackdrop = true;
double backdropAmt = 0;
if (_scroller.hasClients && showBackdrop) {
if (showBackdrop) {
double blurStart = 50;
double maxScroll = 150;
double scrollPx = _scroller.position.pixels - blurStart;
double scrollPx = _scroll.px - blurStart;
// Normalize scroll position to a value between 0 and 1
backdropAmt = (_scroller.position.pixels - blurStart).clamp(0, maxScroll) / maxScroll;
backdropAmt = (_scroll.px - blurStart).clamp(0, maxScroll) / maxScroll;
// Disable backdrop once it is offscreen for an easy perf win
showBackdrop = (scrollPx <= 500);
}

View File

@ -83,9 +83,9 @@ class _IllustrationPieceState extends State<IllustrationPiece> {
key: ValueKey(aspectRatio),
builder: (_, constraints) {
final anim = wonderBuilder.anim;
final curvedAnim = Curves.easeOut.transform(anim.value);
final curvedAnim = Curves.easeOut.transform(anim.controller.value);
final config = wonderBuilder.widget.config;
Widget img = Image.asset(imgPath, opacity: anim, fit: BoxFit.fitHeight);
Widget img = Image.asset(imgPath, opacity: anim.controller, fit: BoxFit.fitHeight);
// Add overflow box so image doesn't get clipped as we translate it around
img = OverflowBox(maxWidth: 2000, child: img);

View File

@ -25,27 +25,24 @@ class WonderIllustrationBuilder extends StatefulWidget {
State<WonderIllustrationBuilder> createState() => WonderIllustrationBuilderState();
}
class WonderIllustrationBuilderState extends State<WonderIllustrationBuilder> with SingleTickerProviderStateMixin {
late final anim = AnimationController(vsync: this, duration: $styles.times.med * .75)
..addListener(() => setState(() {}));
class WonderIllustrationBuilderState extends State<WonderIllustrationBuilder> with StatefulPropsMixin {
late final anim = AnimationControllerProp(
this,
$styles.times.med * .75,
autoBuild: true,
autoPlay: false,
);
bool get isShowing => widget.config.isShowing;
@override
void initState() {
super.initState();
if (isShowing) anim.forward(from: 0);
}
@override
void dispose() {
anim.dispose();
super.dispose();
if (isShowing) anim.controller.forward(from: 0);
}
@override
void didUpdateWidget(covariant WonderIllustrationBuilder oldWidget) {
if (isShowing != oldWidget.config.isShowing) {
isShowing ? anim.forward(from: 0) : anim.reverse(from: 1);
isShowing ? anim.controller.forward(from: 0) : anim.controller.reverse(from: 1);
}
super.didUpdateWidget(oldWidget);
}
@ -53,8 +50,8 @@ class WonderIllustrationBuilderState extends State<WonderIllustrationBuilder> wi
@override
Widget build(BuildContext context) {
// Optimization: no need to return all of these children if the widget is fully invisible.
if (anim.value == 0 && widget.config.enableAnims) return SizedBox.expand();
Animation<double> animation = widget.config.enableAnims ? anim : AlwaysStoppedAnimation(1);
if (anim.controller.value == 0 && widget.config.enableAnims) return SizedBox.expand();
Animation<double> animation = widget.config.enableAnims ? anim.controller : AlwaysStoppedAnimation(1);
return Provider<WonderIllustrationBuilderState>.value(
value: this,

View File

@ -1129,6 +1129,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.11.0"
stateful_props:
dependency: "direct main"
description:
name: stateful_props
sha256: "5f14e2f7c720b044e44d587ed3e7cfa6a3a2431d348416027755e5d331d0c164"
url: "https://pub.dev"
source: hosted
version: "1.2.0+1"
stream_channel:
dependency: transitive
description:

View File

@ -48,6 +48,7 @@ dependencies:
screenshot: ^1.2.3
share_plus: ^4.0.10
shared_preferences: ^2.0.15
stateful_props: ^1.2.0+1
simple_rich_text: ^2.0.49
sized_context: ^1.0.0+1
smooth_page_indicator: ^1.0.0+2