carousel WIP, added trailing param to SimpleHeader

This commit is contained in:
Shawn 2022-11-22 10:56:53 -07:00
parent 34f99c1fe5
commit e9ae67bff5
8 changed files with 326 additions and 45 deletions

View File

@ -1,8 +1,9 @@
import 'package:wonders/common_libs.dart';
import 'package:wonders/ui/common/app_icons.dart';
/// Shared methods across button types
Widget _buildIcon(BuildContext context, IconData icon, {required bool isSecondary, required double? size}) =>
Icon(icon, color: isSecondary ? $styles.colors.black : $styles.colors.offWhite, size: size ?? 18);
Widget _buildIcon(BuildContext context, AppIcons icon, {required bool isSecondary, required double? size}) =>
AppIcon(icon, color: isSecondary ? $styles.colors.black : $styles.colors.offWhite, size: size ?? 18);
/// The core button that drives all other buttons.
class AppBtn extends StatelessWidget {
@ -37,7 +38,7 @@ class AppBtn extends StatelessWidget {
this.border,
String? semanticLabel,
String? text,
IconData? icon,
AppIcons? icon,
double? iconSize,
}) : child = null,
circular = false,

View File

@ -2,14 +2,14 @@ import 'package:wonders/common_libs.dart';
class SimpleHeader extends StatelessWidget {
const SimpleHeader(this.title,
{Key? key, this.subtitle, this.child, this.showBackBtn = true, this.isTransparent = false, this.onBack})
{Key? key, this.subtitle, this.showBackBtn = true, this.isTransparent = false, this.onBack, this.trailing})
: super(key: key);
final String title;
final String? subtitle;
final Widget? child;
final bool showBackBtn;
final bool isTransparent;
final VoidCallback? onBack;
final Widget Function(BuildContext context)? trailing;
@override
Widget build(BuildContext context) {
@ -17,14 +17,28 @@ class SimpleHeader extends StatelessWidget {
color: isTransparent ? Colors.transparent : $styles.colors.black,
child: SafeArea(
bottom: false,
child: SizedBox(
height: 64,
child: Stack(
children: [
Positioned.fill(
child: Center(
child: Row(children: [
if (showBackBtn) BackBtn(onPressed: onBack).safe(),
Flexible(
fit: FlexFit.tight,
child: MergeSemantics(
Gap($styles.insets.sm),
if (showBackBtn) BackBtn(onPressed: onBack),
Spacer(),
if (trailing != null) trailing!.call(context),
Gap($styles.insets.sm),
//if (showBackBtn) Container(width: $styles.insets.lg * 2, alignment: Alignment.centerLeft, child: child),
]),
),
),
MergeSemantics(
child: Semantics(
header: true,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!showBackBtn) Gap($styles.insets.xs),
Text(
@ -43,9 +57,10 @@ class SimpleHeader extends StatelessWidget {
),
),
),
)
],
),
),
if (showBackBtn) Container(width: $styles.insets.lg * 2, alignment: Alignment.centerLeft, child: child),
]),
),
);
}

View File

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:wonders/common_libs.dart';
import 'package:wonders/logic/data/highlight_data.dart';
import 'package:wonders/ui/common/app_icons.dart';
import 'package:wonders/ui/common/controls/app_page_indicator.dart';
import 'package:wonders/ui/common/controls/simple_header.dart';
import 'package:wonders/ui/common/static_text_scale.dart';
@ -10,6 +11,20 @@ import 'package:wonders/ui/common/static_text_scale.dart';
part 'widgets/_blurred_image_bg.dart';
part 'widgets/_carousel_item.dart';
class GreyBox extends StatelessWidget {
const GreyBox(this.lbl, {Key? key, this.strength = .5}) : super(key: key);
final String lbl;
final double strength;
@override
Widget build(BuildContext context) => ColoredBox(
color: Colors.white,
child: Container(
color: Colors.grey.shade800.withOpacity(strength),
child: Center(child: Text(lbl, style: $styles.text.h3)),
),
);
}
class ArtifactCarouselScreen extends StatefulWidget {
final WonderType type;
const ArtifactCarouselScreen({Key? key, required this.type}) : super(key: key);
@ -19,6 +34,252 @@ class ArtifactCarouselScreen extends StatefulWidget {
}
class _ArtifactScreenState extends State<ArtifactCarouselScreen> {
PageController? _pageController;
final _currentPage = ValueNotifier<double>(9999);
late final List<HighlightData> _artifacts = HighlightData.forWonder(widget.type);
late final _currentArtifactIndex = ValueNotifier<int>(0);
@override
void initState() {
super.initState();
_handlePageChanged();
}
void _handlePageChanged() {
_currentPage.value = _pageController?.page ?? 0;
_currentArtifactIndex.value = _currentPage.value.round() % _artifacts.length;
}
@override
Widget build(BuildContext context) {
bool shortMode = context.heightPx < 800;
final double bottomHeight = shortMode ? 190 : 310;
// Allow objects to become wider as the screen becomes tall, this allows
// them to grow taller as well, filling the available space better.
//double itemWidth = 150 + max(context.heightPx - 600, 0) / 600 * 150;
double itemHeight = context.heightPx - 150 - bottomHeight;
double itemWidth = itemHeight * .666;
// TODO: This could be optimized to only run if the size has changed...is it worth it?
_pageController?.dispose();
_pageController = PageController(
viewportFraction: itemWidth / context.widthPx,
initialPage: _currentPage.value.round(),
);
_pageController?.addListener(_handlePageChanged);
final pages = [
GreyBox('item1', strength: .5),
GreyBox('item2', strength: .5),
GreyBox('item3', strength: .5),
GreyBox('item4', strength: .5),
GreyBox('item5', strength: .5),
].map((e) => Padding(padding: EdgeInsets.all(10), child: e)).toList();
return Stack(
children: [
/// Background
Positioned.fill(
child: ValueListenableBuilder<int>(
valueListenable: _currentArtifactIndex,
builder: (_, value, __) {
return _BlurredImageBg(url: _artifacts[value].imageUrl);
}),
),
/// Bottom Text w/ Circle
BottomCenter(
child: ValueListenableBuilder<int>(
valueListenable: _currentArtifactIndex,
builder: (_, value, __) => _CarouselBottomTextWithCircle(
artifact: _artifacts[value],
height: bottomHeight,
shortMode: shortMode,
state: this,
),
),
),
/// Carousel Items
PageView.builder(
controller: _pageController,
itemBuilder: (_, index) {
final child = pages[index % pages.length];
return ValueListenableBuilder<double>(
valueListenable: _currentPage,
builder: (_, value, __) {
final int offset = (value.round() - index).abs();
return CollapsingCarouselListItem(
width: itemWidth,
bottom: bottomHeight,
indexOffset: min(3, offset),
child: child,
);
},
);
},
),
/// header
SimpleHeader(
$strings.artifactsTitleArtifacts,
showBackBtn: true,
isTransparent: true,
trailing: (context) => CircleBtn(
semanticLabel: $strings.artifactsButtonBrowse,
onPressed: () {},
child: AppIcon(AppIcons.search),
),
),
],
);
}
}
/// Handles the carousel specific logic, like setting the height and vertical alignment of each item.
/// This lets the child simply render it's contents
class CollapsingCarouselListItem extends StatelessWidget {
const CollapsingCarouselListItem(
{Key? key, required this.child, required this.indexOffset, required this.width, required this.bottom})
: super(key: key);
final Widget child;
final int indexOffset;
final double width;
final double bottom;
@override
Widget build(BuildContext context) {
// Calculate offset, this will be subtracted from the bottom padding moving the element downwards
double vtOffset = indexOffset * 30;
return AppBtn.basic(
onPressed: () => print('todo'),
semanticLabel: 'TODO',
child: AnimatedOpacity(
duration: $styles.times.fast,
opacity: indexOffset.abs() <= 2 ? 1 : 0,
child: AnimatedPadding(
duration: $styles.times.fast,
padding: EdgeInsets.only(bottom: bottom),
child: BottomCenter(
child: AnimatedContainer(
duration: $styles.times.fast,
// Center item is portrait, the others are square
height: indexOffset == 0 ? width * 1.3 : width,
child: Placeholder(),
),
),
),
),
);
}
}
class _CarouselBottomTextWithCircle extends StatelessWidget {
const _CarouselBottomTextWithCircle(
{Key? key, required this.artifact, required this.height, required this.state, required this.shortMode})
: super(key: key);
final HighlightData artifact;
final double height;
final _ArtifactScreenState state;
final bool shortMode;
@override
Widget build(BuildContext context) {
return Container(
width: $styles.sizes.maxContentWidth2,
height: height,
padding: EdgeInsets.symmetric(horizontal: $styles.insets.md),
alignment: Alignment.center,
child: Stack(
children: [
/// BgCircle
_buildBgCircle(),
/// Text
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Gap($styles.insets.md),
Column(
children: [
IgnorePointer(
ignoringSemantics: false,
child: Semantics(
button: true,
// TODO: FIX
// onIncrease: () => _handleArtifactTap(_currentOffset.round() + 1),
// onDecrease: () => _handleArtifactTap(_currentOffset.round() - 1),
// onTap: () => _handleArtifactTap(_currentOffset.round()),
liveRegion: true,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// Force column to stretch horizontally so text is centered
SizedBox(width: double.infinity),
// Stop text from scaling to make layout a little easier, it's already quite large
StaticTextScale(
child: Text(
artifact.title,
overflow: TextOverflow.ellipsis,
style: $styles.text.h2.copyWith(color: $styles.colors.black, height: 1.2, fontSize: 32),
textAlign: TextAlign.center,
maxLines: 2,
),
),
if (!shortMode) ...[
Gap($styles.insets.xxs),
Text(
artifact.date.isEmpty ? '--' : artifact.date,
style: $styles.text.body,
textAlign: TextAlign.center,
),
]
],
).animate(key: ValueKey(artifact.artifactId)).fadeIn(),
),
),
Gap($styles.insets.sm),
if (!shortMode)
AppPageIndicator(
count: state._artifacts.length,
controller: state._pageController!,
semanticPageTitle: $strings.artifactsSemanticArtifact,
),
],
),
Gap($styles.insets.md),
Spacer(),
AppBtn.from(
text: $strings.artifactsButtonBrowse,
icon: AppIcons.search,
expand: true,
onPressed: () {}, //_handleSearchTap,
),
Gap($styles.insets.lg),
],
),
],
),
);
}
OverflowBox _buildBgCircle() {
return OverflowBox(
maxWidth: 2000,
maxHeight: 2000,
child: Transform.translate(
offset: Offset(0, 1000 - height * .7),
child: Container(
decoration: BoxDecoration(
color: $styles.colors.offWhite.withOpacity(0.8),
borderRadius: BorderRadius.vertical(top: Radius.circular(999)),
),
),
),
);
}
}
class _ArtifactScreenState2 extends State<ArtifactCarouselScreen> {
// Used to cap white background dimensions.
static const double _maxElementWidth = 440;
static const double _partialElementWidth = 0.9;
@ -199,7 +460,7 @@ class _ArtifactScreenState extends State<ArtifactCarouselScreen> {
Gap(_small ? $styles.insets.sm : $styles.insets.md),
AppBtn.from(
text: $strings.artifactsButtonBrowse,
icon: Icons.search,
icon: AppIcons.search,
expand: true,
onPressed: _handleSearchTap,
),

View File

@ -1,6 +1,7 @@
import 'package:wonders/common_libs.dart';
import 'package:wonders/logic/common/string_utils.dart';
import 'package:wonders/logic/data/wonder_data.dart';
import 'package:wonders/ui/common/app_icons.dart';
import 'package:wonders/ui/common/cards/opening_card.dart';
import 'package:wonders/ui/common/wonders_timeline_builder.dart';
import 'package:wonders/ui/screens/artifact/artifact_search/artifact_search_screen.dart';
@ -193,7 +194,7 @@ class _OpenedTimeRange extends StatelessWidget {
onPressed: onClose,
semanticLabel: $strings.expandingTimeSelectorSemanticSelector,
enableFeedback: false, // handled when panelController changes.
icon: Icons.close,
icon: AppIcons.close,
iconSize: 20,
padding: EdgeInsets.symmetric(vertical: $styles.insets.xxs),
bgColor: Colors.transparent,

View File

@ -78,11 +78,13 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
children: [
/// Background
Positioned.fill(
child: ValueListenableBuilder(
child: ValueListenableBuilder<double>(
valueListenable: _scrollPos,
builder: (_, value, __) {
return Container(
color: widget.data.type.bgColor.withOpacity(1),
bool showBg = value < 700;
return AnimatedContainer(
duration: $styles.times.fast,
color: widget.data.type.bgColor.withOpacity(showBg ? 1 : 0),
);
},
),

View File

@ -74,10 +74,9 @@ class _AppBar extends StatelessWidget {
/// Colored overlay
if (showOverlay) ...[
ClipRect(
child: ColoredBox(
color: wonderType.bgColor.withOpacity(.8),
).animate().fade(duration: $styles.times.fast),
AnimatedContainer(
duration: $styles.times.slow,
color: wonderType.bgColor.withOpacity(showOverlay ? .8 : 0),
),
],
],

View File

@ -10,15 +10,16 @@ class _CollapsingPullQuoteImage extends StatelessWidget {
// Start transitioning when we are halfway up the screen
final collapseStartPx = context.heightPx * 1;
final collapseEndPx = context.heightPx * .15;
const double imgHeight = 430;
const double imgHeight = 500;
const double outerPadding = 100;
/// A single piece of quote text, this widget has one on top, and one on bottom
Widget buildText(String value, double collapseAmt, {required bool top, bool isAuthor = false}) {
var quoteStyle = $styles.text.quote1;
/// Use a fixed font-size for this for consistent scaling
var quoteStyle = $styles.text.quote1.copyWith(fontSize: 32);
quoteStyle = quoteStyle.copyWith(color: $styles.colors.caption);
if (isAuthor) {
quoteStyle = quoteStyle.copyWith(fontSize: 20 * $styles.scale, fontWeight: FontWeight.w600);
quoteStyle = quoteStyle.copyWith(fontSize: 20, fontWeight: FontWeight.w600);
}
double offsetY = (imgHeight / 2 + outerPadding * .25) * (1 - collapseAmt);
if (top) offsetY *= -1; // flip?
@ -44,7 +45,7 @@ class _CollapsingPullQuoteImage extends StatelessWidget {
return MergeSemantics(
child: CenteredBox(
padding: EdgeInsets.symmetric(vertical: outerPadding),
width: 450,
width: imgHeight * .66,
child: Stack(
children: [
Container(

View File

@ -112,6 +112,7 @@ class _AnimatedCloudsState extends State<AnimatedClouds> with SingleTickerProvid
List<_Cloud> _getClouds() {
Size size = ContextUtils.getSize(context) ?? Size(context.widthPx, 400);
rndSeed = _getCloudSeed(widget.wonderType);
return List<_Cloud>.generate(3, (index) {
return _Cloud(
@ -141,7 +142,7 @@ class _Cloud extends StatelessWidget {
child: Image.asset(
ImagePaths.cloud,
opacity: AlwaysStoppedAnimation(.4 * opacity),
width: context.widthPx * .65 * scale,
width: 400 * scale,
fit: BoxFit.fitWidth,
),
);