Polish artifacts carousel

This commit is contained in:
Shawn 2022-12-01 20:09:28 -07:00
parent bd3a87edd9
commit 7d6cca3b6b
9 changed files with 267 additions and 557 deletions

View File

@ -64,7 +64,7 @@ class AppLogic {
// Load initial view (replace empty initial view which is covered by a native splash screen)
bool showIntro = settingsLogic.hasCompletedOnboarding.value == false;
if (showIntro) {
if (true) {
appRouter.go(ScreenPaths.intro);
} else {
appRouter.go(ScreenPaths.home);

View File

@ -40,7 +40,6 @@ class SimpleHeader extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!showBackBtn) Gap($styles.insets.xs),
Text(
title.toUpperCase(),
textHeightBehavior: TextHeightBehavior(applyHeightToFirstAscent: false),
@ -52,7 +51,6 @@ class SimpleHeader extends StatelessWidget {
textHeightBehavior: TextHeightBehavior(applyHeightToFirstAscent: false),
style: $styles.text.title1.copyWith(color: $styles.colors.accent1),
),
if (!showBackBtn) Gap($styles.insets.md),
],
),
),

View File

@ -9,21 +9,8 @@ import 'package:wonders/ui/common/controls/simple_header.dart';
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)),
),
);
}
part 'widgets/_collapsing_carousel_item.dart';
part 'widgets/_bottom_text_content.dart';
class ArtifactCarouselScreen extends StatefulWidget {
final WonderType type;
@ -38,27 +25,38 @@ class _ArtifactScreenState extends State<ArtifactCarouselScreen> {
final _currentPage = ValueNotifier<double>(9999);
late final List<HighlightData> _artifacts = HighlightData.forWonder(widget.type);
late final _currentArtifactIndex = ValueNotifier<int>(0);
late final _currentArtifactIndex = ValueNotifier<int>(_wrappedPageIndex);
@override
void initState() {
super.initState();
_handlePageChanged();
}
int get _wrappedPageIndex => _currentPage.value.round() % _artifacts.length;
void _handlePageChanged() {
_currentPage.value = _pageController?.page ?? 0;
_currentArtifactIndex.value = _currentPage.value.round() % _artifacts.length;
_currentArtifactIndex.value = _wrappedPageIndex;
}
void _handleSearchTap() => context.push(ScreenPaths.search(widget.type));
void _handleArtifactTap(int index) {
int delta = index - _currentPage.value.round();
if (delta == 0) {
HighlightData data = _artifacts[index % _artifacts.length];
context.push(ScreenPaths.artifact(data.artifactId));
} else {
_pageController?.animateToPage(
_currentPage.value.round() + delta,
duration: $styles.times.fast,
curve: Curves.easeOut,
);
}
}
@override
Widget build(BuildContext context) {
bool shortMode = context.heightPx < 800;
final double bottomHeight = shortMode ? 190 : 310;
final double bottomHeight = shortMode ? 240 : 340;
// 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 itemHeight = (context.heightPx - 200 - bottomHeight).clamp(250, 400);
double itemWidth = itemHeight * .666;
// TODO: This could be optimized to only run if the size has changed...is it worth it?
_pageController?.dispose();
@ -67,17 +65,16 @@ class _ArtifactScreenState extends State<ArtifactCarouselScreen> {
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();
final pages = _artifacts.map((e) {
return Padding(
padding: EdgeInsets.all(10),
child: _DoubleBorderImage(e),
);
}).toList();
return Stack(
children: [
/// Background
/// Blurred Bg
Positioned.fill(
child: ValueListenableBuilder<int>(
valueListenable: _currentArtifactIndex,
@ -86,11 +83,37 @@ class _ArtifactScreenState extends State<ArtifactCarouselScreen> {
}),
),
/// Bottom Text w/ Circle
/// BgCircle
_buildBgCircle(bottomHeight),
/// Carousel Items
PageView.builder(
controller: _pageController,
itemBuilder: (_, index) {
final wrappedIndex = index % pages.length;
final child = pages[wrappedIndex];
return ValueListenableBuilder<double>(
valueListenable: _currentPage,
builder: (_, value, __) {
final int offset = (value.round() - index).abs();
return _CollapsingCarouselItem(
width: itemWidth,
bottom: bottomHeight - 20,
indexOffset: min(3, offset),
onPressed: () => _handleArtifactTap(index),
title: _artifacts[wrappedIndex].title,
child: child,
);
},
);
},
),
/// Bottom Text
BottomCenter(
child: ValueListenableBuilder<int>(
valueListenable: _currentArtifactIndex,
builder: (_, value, __) => _CarouselBottomTextWithCircle(
builder: (_, value, __) => _BottomTextContent(
artifact: _artifacts[value],
height: bottomHeight,
shortMode: shortMode,
@ -99,404 +122,32 @@ class _ArtifactScreenState extends State<ArtifactCarouselScreen> {
),
),
/// 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
/// Header
SimpleHeader(
$strings.artifactsTitleArtifacts,
showBackBtn: true,
showBackBtn: false,
isTransparent: true,
trailing: (context) => CircleBtn(
semanticLabel: $strings.artifactsButtonBrowse,
onPressed: () {},
onPressed: _handleSearchTap,
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(context),
),
Gap($styles.insets.lg),
],
),
],
),
);
}
void _handleSearchTap(BuildContext context) {
//context.push(ScreenPaths.search(widget.type));
}
OverflowBox _buildBgCircle() {
OverflowBox _buildBgCircle(double height) {
const double size = 1500;
return OverflowBox(
maxWidth: 2000,
maxHeight: 2000,
maxWidth: size,
maxHeight: size,
child: Transform.translate(
offset: Offset(0, 1000 - height * .7),
offset: Offset(0, size / 2),
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;
static const double _maxElementHeight = 640;
// Locally store loaded artifacts.
late List<HighlightData> _artifacts;
late PageController _controller;
final _currentIndex = ValueNotifier(0);
double get _currentOffset {
bool hasOffset = _controller.hasClients && _controller.position.haveDimensions;
return hasOffset ? _controller.page! : _controller.initialPage * 1.0;
}
HighlightData get _currentArtifact => _artifacts[_currentIndex.value];
double get _backdropWidth {
final w = context.widthPx;
return w <= _maxElementWidth ? w : min(w * _partialElementWidth, _maxElementWidth);
}
double get _backdropHeight => math.min(context.heightPx * 0.65, _maxElementHeight);
bool get _small => _backdropHeight / _maxElementHeight < 0.7;
@override
void initState() {
super.initState();
_artifacts = HighlightData.forWonder(widget.type);
_controller = PageController(
// start at a high offset so we can scroll backwards:
initialPage: _artifacts.length * 9999,
viewportFraction: 0.5,
)..addListener(_handleCarouselScroll);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleCarouselScroll() => _currentIndex.value = _currentOffset.round() % _artifacts.length;
void _handleArtifactTap(int index) {
int delta = index - _currentOffset.round();
if (delta == 0) {
HighlightData data = _artifacts[index % _artifacts.length];
context.push(ScreenPaths.artifact(data.artifactId));
} else {
_controller.animateToPage(
_currentOffset.round() + delta,
duration: $styles.times.fast,
curve: Curves.easeOut,
);
}
}
void _handleSearchTap() {
context.push(ScreenPaths.search(widget.type));
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<int>(
valueListenable: _currentIndex,
builder: (context, index, _) {
return Container(
color: $styles.colors.greyStrong,
child: Stack(
children: [
/// Background image
Positioned.fill(
child: _BlurredImageBg(url: _currentArtifact.imageUrl),
),
/// Content
Column(
children: [
SimpleHeader($strings.artifactsTitleArtifacts, showBackBtn: false, isTransparent: true),
Gap($styles.insets.xs),
Expanded(
child: Stack(children: [
// White arch, covering bottom half:
_buildWhiteArch(),
// Carousel
_buildCarouselPageView(),
// Text content
_buildBottomTextContent(),
]),
),
],
),
],
),
);
},
);
}
Widget _buildWhiteArch() {
return BottomCenter(
child: Container(
width: _backdropWidth,
height: _backdropHeight,
decoration: BoxDecoration(
color: $styles.colors.offWhite.withOpacity(0.8),
borderRadius: BorderRadius.vertical(top: Radius.circular(999)),
),
),
);
}
Widget _buildBottomTextContent() {
return BottomCenter(
child: Container(
width: _backdropWidth,
padding: EdgeInsets.symmetric(horizontal: $styles.insets.md),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Gap($styles.insets.md),
Container(
width: _backdropWidth,
padding: EdgeInsets.symmetric(horizontal: $styles.insets.sm),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IgnorePointer(
ignoringSemantics: false,
child: Semantics(
button: true,
onIncrease: () => _handleArtifactTap(_currentOffset.round() + 1),
onDecrease: () => _handleArtifactTap(_currentOffset.round() - 1),
onTap: () => _handleArtifactTap(_currentOffset.round()),
liveRegion: true,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: _small ? 90 : 110,
alignment: Alignment.center,
child: StaticTextScale(
child: Text(
_currentArtifact.title,
overflow: TextOverflow.ellipsis,
style: $styles.text.h2.copyWith(color: $styles.colors.black, height: 1.2),
textAlign: TextAlign.center,
maxLines: 2,
),
),
),
if (!_small) Gap($styles.insets.xxs),
Text(
_currentArtifact.date.isEmpty ? '--' : _currentArtifact.date,
style: $styles.text.body,
textAlign: TextAlign.center,
),
],
).animate(key: ValueKey(_currentArtifact.artifactId)).fadeIn(),
),
),
Gap(_small ? $styles.insets.xs : $styles.insets.sm),
AppPageIndicator(
count: _artifacts.length,
controller: _controller,
semanticPageTitle: $strings.artifactsSemanticArtifact,
),
],
),
),
Gap(_small ? $styles.insets.sm : $styles.insets.md),
AppBtn.from(
text: $strings.artifactsButtonBrowse,
icon: AppIcons.search,
expand: true,
onPressed: _handleSearchTap,
),
Gap(_small ? $styles.insets.md : $styles.insets.lg),
],
),
),
);
}
Widget _buildCarouselPageView() {
return Center(
child: SizedBox(
width: _backdropWidth,
child: ExcludeSemantics(
child: PageView.builder(
controller: _controller,
clipBehavior: Clip.none,
itemBuilder: (context, index) => ListenableBuilder(
listenable: _controller,
builder: (_, __) {
return _CarouselItem(
index: index,
currentPage: _currentOffset,
artifact: _artifacts[index % _artifacts.length],
bottomPadding: _backdropHeight,
maxWidth: _backdropWidth,
maxHeight: _backdropHeight,
onPressed: () => _handleArtifactTap(index),
);
},
),
borderRadius: BorderRadius.vertical(top: Radius.circular(size * .45)),
),
),
),

View File

@ -0,0 +1,88 @@
part of '../artifact_carousel_screen.dart';
class _BottomTextContent extends StatelessWidget {
const _BottomTextContent(
{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: [
/// Text
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Gap($styles.insets.xl),
Column(
children: [
IgnorePointer(
ignoringSemantics: false,
child: Semantics(
button: true,
onIncrease: () => state._handleArtifactTap(state._currentPage.value.round() + 1),
onDecrease: () => state._handleArtifactTap(state._currentPage.value.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,
),
],
),
if (!shortMode) Gap($styles.insets.md),
Spacer(),
AppBtn.from(
text: $strings.artifactsButtonBrowse,
icon: AppIcons.search,
expand: true,
onPressed: state._handleSearchTap,
),
Gap($styles.insets.lg),
],
),
],
),
);
}
}

View File

@ -1,136 +0,0 @@
part of '../artifact_carousel_screen.dart';
class _CarouselItem extends StatelessWidget {
const _CarouselItem({
Key? key,
required this.index,
required this.currentPage,
required this.artifact,
required this.bottomPadding,
required this.maxWidth,
required this.maxHeight,
required this.onPressed,
}) : super(key: key);
final HighlightData artifact;
final int index;
final double currentPage;
final double bottomPadding;
final double maxWidth;
final double maxHeight;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return AppBtn.basic(
semanticLabel: 'Explore artifact details',
onPressed: onPressed,
pressEffect: false,
child: _ImagePreview(
image: NetworkImage(artifact.imageUrlSmall),
bottomPadding: bottomPadding,
maxWidth: maxWidth,
maxHeight: maxHeight,
offsetAmt: currentPage - index.toDouble(),
),
);
}
}
class _ImagePreview extends StatelessWidget {
const _ImagePreview(
{Key? key,
required this.image,
required this.offsetAmt,
required this.bottomPadding,
required this.maxWidth,
required this.maxHeight})
: super(key: key);
final ImageProvider image;
final double offsetAmt;
final double bottomPadding;
final double maxWidth;
final double maxHeight;
@override
Widget build(BuildContext context) {
// Additional scale of the main page, making it larger than the others.
const double mainPageScaleFactor = 0.40;
// Additional Y scale of the main page, making it elongated.
const double mainPageScaleFactorY = -0.13;
// Border variables
const double borderPadding = 4.0;
const double borderWidth = 1.0;
// Base scale units that we can treat like pixels.
double baseWidthScale = 1 / maxWidth;
double baseHeightScale = 1 / context.heightPx;
// Size of the pages themselves.
double pageWidth = baseWidthScale * maxWidth * 0.65;
double pageHeight = baseHeightScale * maxWidth * 0.45;
// Get the current page offset value compared to other pages. -1 is left, 0 is middle, 1 is right, etc.
// Note: This value can be in between whole numbers, like 0.25 and -0.75.
double pageOffset = math.max(-2, math.min(2, offsetAmt));
// Add a scale-up to the main page.
double midPageScaleUp = (1 - math.min(1, pageOffset.abs())) * mainPageScaleFactor;
double midPageScaleUpY = (1 - math.min(1, pageOffset.abs())) * mainPageScaleFactorY;
// Use absolute value of offset so images always move down.
double yOffsetFactor = pageOffset.abs();
if (pageOffset >= -1 && pageOffset <= 1) {
// Create an offset factor using sin/cos to ease.
yOffsetFactor = 1 - math.cos(pageOffset * math.pi / 2.0).abs();
}
// Multiply the offset factors with the width/height scale to convert them to fractionals.
double yOffset = yOffsetFactor * ((baseHeightScale / 2) * (maxWidth / 2));
// Apply a vertical offset based on the bottom padding provided. This includes half the element width.
double bottomPadding = (baseHeightScale / 2) * (maxHeight * 2 - (maxWidth / 2));
double widthFactor = pageWidth + midPageScaleUp;
double heightFactor = pageHeight + midPageScaleUp + midPageScaleUpY;
double opacity = max(0, 1 - max(0, pageOffset.abs() - 1) * 2);
// Scale box for sizing. Uses both the element scale and the element Y scale.
return FractionalTranslation(
// Move the pages around before scaling, as scaling will directly affect their translation.
translation: Offset(0, yOffset - bottomPadding),
child: FractionallySizedBox(
// Scale the elements according to whether they are on the sides or middle.
alignment: Alignment.bottomCenter,
widthFactor: widthFactor,
heightFactor: heightFactor,
// Translation box for positioning.
child: Opacity(
opacity: opacity,
child: Container(
// Add an outer border with the rounded ends.
decoration: BoxDecoration(
shape: BoxShape.rectangle,
border: Border.all(color: $styles.colors.offWhite, width: borderWidth),
borderRadius: BorderRadius.all(Radius.circular(999)),
),
child: Padding(
padding: EdgeInsets.all(borderPadding),
child: ClipRRect(
borderRadius: BorderRadius.circular(999),
child: ColoredBox(
color: $styles.colors.greyMedium,
child: AppImage(image: image, fit: BoxFit.cover, scale: 0.5),
),
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,74 @@
part of '../artifact_carousel_screen.dart';
/// 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 _CollapsingCarouselItem extends StatelessWidget {
const _CollapsingCarouselItem(
{Key? key,
required this.child,
required this.indexOffset,
required this.width,
required this.bottom,
required this.onPressed,
required this.title})
: super(key: key);
final Widget child;
final int indexOffset;
final double width;
final double bottom;
final VoidCallback onPressed;
final String title;
@override
Widget build(BuildContext context) {
// Calculate offset, this will be subtracted from the bottom padding moving the element downwards
double vtOffset = 0;
if (indexOffset == 1) vtOffset = width * .1;
if (indexOffset == 2) vtOffset = width * .4;
if (indexOffset > 2) vtOffset = width;
final content = AnimatedOpacity(
duration: $styles.times.fast,
opacity: indexOffset.abs() <= 2 ? 1 : 0,
child: AnimatedPadding(
duration: $styles.times.fast,
padding: EdgeInsets.only(bottom: max(bottom - vtOffset, 0)),
child: BottomCenter(
child: AnimatedContainer(
duration: $styles.times.fast,
// Center item is portrait, the others are square
height: indexOffset == 0 ? width * 1.5 : width,
width: width,
child: child,
),
),
),
);
if (indexOffset > 2) return content;
return AppBtn.basic(onPressed: onPressed, semanticLabel: title, child: content);
}
}
class _DoubleBorderImage extends StatelessWidget {
const _DoubleBorderImage(this.data, {Key? key}) : super(key: key);
final HighlightData data;
@override
Widget build(BuildContext context) => Container(
// Add an outer border with the rounded ends.
decoration: BoxDecoration(
shape: BoxShape.rectangle,
border: Border.all(color: $styles.colors.offWhite, width: 1),
borderRadius: BorderRadius.all(Radius.circular(999)),
),
child: Padding(
padding: EdgeInsets.all($styles.insets.xs),
child: ClipRRect(
borderRadius: BorderRadius.circular(999),
child: ColoredBox(
color: $styles.colors.greyMedium,
child: AppImage(image: NetworkImage(data.imageUrlSmall), fit: BoxFit.cover, scale: 0.5),
),
),
),
);
}

View File

@ -72,7 +72,7 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
return LayoutBuilder(builder: (_, constraints) {
bool shortMode = constraints.biggest.height < 700;
double illustrationHeight = shortMode ? 250 : 280;
double minAppBarHeight = shortMode ? 80 : 120;
double minAppBarHeight = shortMode ? 80 : 150;
/// Attempt to maintain a similar aspect ratio for the image within the app-bar
double maxAppBarHeight = min(context.widthPx, $styles.sizes.maxContentWidth1) * 1.5;

View File

@ -80,7 +80,7 @@ class _AppBar extends StatelessWidget {
/// Colored overlay
if (showOverlay) ...[
AnimatedContainer(
duration: $styles.times.slow,
duration: $styles.times.med,
color: wonderType.bgColor.withOpacity(showOverlay ? .8 : 0),
),
],

View File

@ -2,6 +2,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:wonders/common_libs.dart';
import 'package:wonders/ui/common/app_icons.dart';
import 'package:wonders/ui/common/controls/app_page_indicator.dart';
import 'package:wonders/ui/common/gradient_container.dart';
import 'package:wonders/ui/common/static_text_scale.dart';
import 'package:wonders/ui/common/themed_text.dart';
import 'package:wonders/ui/common/utils/app_haptics.dart';
@ -139,6 +140,10 @@ class _IntroScreenState extends State<IntroScreen> {
child: _buildNavText(context),
),
),
// Build a cpl overlays to hide the content when swiping on very wide screens
_buildHzGradientOverlay(left: true),
_buildHzGradientOverlay(),
]);
return DefaultTextColor(
@ -150,6 +155,36 @@ class _IntroScreenState extends State<IntroScreen> {
);
}
Widget _buildHzGradientOverlay({bool left = false}) {
return Align(
alignment: Alignment(left ? -1 : 1, 0),
child: FractionallySizedBox(
widthFactor: .5,
child: Padding(
padding: EdgeInsets.only(left: left ? 0 : 200, right: left ? 200 : 0),
child: Transform.scale(
scaleX: left ? -1 : 1,
child: HzGradient([
$styles.colors.black.withOpacity(0),
$styles.colors.black,
], const [
0,
.2
])),
),
),
);
// CenterLeft(
// child: FractionallySizedBox(
// widthFactor: .5,
// child: Padding(
// padding: const EdgeInsets.only(right: 200),
// child: HzGradient([$styles.colors.black, $styles.colors.black.withOpacity(0)], const [.8, 1]),
// ),
// ),
// );
}
Widget _buildFinishBtn(BuildContext context) {
return ValueListenableBuilder<int>(
valueListenable: _currentPage,