Polish artifacts carousel
This commit is contained in:
parent
bd3a87edd9
commit
7d6cca3b6b
@ -64,7 +64,7 @@ class AppLogic {
|
|||||||
|
|
||||||
// Load initial view (replace empty initial view which is covered by a native splash screen)
|
// Load initial view (replace empty initial view which is covered by a native splash screen)
|
||||||
bool showIntro = settingsLogic.hasCompletedOnboarding.value == false;
|
bool showIntro = settingsLogic.hasCompletedOnboarding.value == false;
|
||||||
if (showIntro) {
|
if (true) {
|
||||||
appRouter.go(ScreenPaths.intro);
|
appRouter.go(ScreenPaths.intro);
|
||||||
} else {
|
} else {
|
||||||
appRouter.go(ScreenPaths.home);
|
appRouter.go(ScreenPaths.home);
|
||||||
|
@ -40,7 +40,6 @@ class SimpleHeader extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (!showBackBtn) Gap($styles.insets.xs),
|
|
||||||
Text(
|
Text(
|
||||||
title.toUpperCase(),
|
title.toUpperCase(),
|
||||||
textHeightBehavior: TextHeightBehavior(applyHeightToFirstAscent: false),
|
textHeightBehavior: TextHeightBehavior(applyHeightToFirstAscent: false),
|
||||||
@ -52,7 +51,6 @@ class SimpleHeader extends StatelessWidget {
|
|||||||
textHeightBehavior: TextHeightBehavior(applyHeightToFirstAscent: false),
|
textHeightBehavior: TextHeightBehavior(applyHeightToFirstAscent: false),
|
||||||
style: $styles.text.title1.copyWith(color: $styles.colors.accent1),
|
style: $styles.text.title1.copyWith(color: $styles.colors.accent1),
|
||||||
),
|
),
|
||||||
if (!showBackBtn) Gap($styles.insets.md),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -9,21 +9,8 @@ import 'package:wonders/ui/common/controls/simple_header.dart';
|
|||||||
import 'package:wonders/ui/common/static_text_scale.dart';
|
import 'package:wonders/ui/common/static_text_scale.dart';
|
||||||
|
|
||||||
part 'widgets/_blurred_image_bg.dart';
|
part 'widgets/_blurred_image_bg.dart';
|
||||||
part 'widgets/_carousel_item.dart';
|
part 'widgets/_collapsing_carousel_item.dart';
|
||||||
|
part 'widgets/_bottom_text_content.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 {
|
class ArtifactCarouselScreen extends StatefulWidget {
|
||||||
final WonderType type;
|
final WonderType type;
|
||||||
@ -38,27 +25,38 @@ class _ArtifactScreenState extends State<ArtifactCarouselScreen> {
|
|||||||
final _currentPage = ValueNotifier<double>(9999);
|
final _currentPage = ValueNotifier<double>(9999);
|
||||||
|
|
||||||
late final List<HighlightData> _artifacts = HighlightData.forWonder(widget.type);
|
late final List<HighlightData> _artifacts = HighlightData.forWonder(widget.type);
|
||||||
late final _currentArtifactIndex = ValueNotifier<int>(0);
|
late final _currentArtifactIndex = ValueNotifier<int>(_wrappedPageIndex);
|
||||||
|
|
||||||
@override
|
int get _wrappedPageIndex => _currentPage.value.round() % _artifacts.length;
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_handlePageChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handlePageChanged() {
|
void _handlePageChanged() {
|
||||||
_currentPage.value = _pageController?.page ?? 0;
|
_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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool shortMode = context.heightPx < 800;
|
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
|
// Allow objects to become wider as the screen becomes tall, this allows
|
||||||
// them to grow taller as well, filling the available space better.
|
// 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 - 200 - bottomHeight).clamp(250, 400);
|
||||||
double itemHeight = context.heightPx - 150 - bottomHeight;
|
|
||||||
double itemWidth = itemHeight * .666;
|
double itemWidth = itemHeight * .666;
|
||||||
// TODO: This could be optimized to only run if the size has changed...is it worth it?
|
// TODO: This could be optimized to only run if the size has changed...is it worth it?
|
||||||
_pageController?.dispose();
|
_pageController?.dispose();
|
||||||
@ -67,17 +65,16 @@ class _ArtifactScreenState extends State<ArtifactCarouselScreen> {
|
|||||||
initialPage: _currentPage.value.round(),
|
initialPage: _currentPage.value.round(),
|
||||||
);
|
);
|
||||||
_pageController?.addListener(_handlePageChanged);
|
_pageController?.addListener(_handlePageChanged);
|
||||||
final pages = [
|
final pages = _artifacts.map((e) {
|
||||||
GreyBox('item1', strength: .5),
|
return Padding(
|
||||||
GreyBox('item2', strength: .5),
|
padding: EdgeInsets.all(10),
|
||||||
GreyBox('item3', strength: .5),
|
child: _DoubleBorderImage(e),
|
||||||
GreyBox('item4', strength: .5),
|
);
|
||||||
GreyBox('item5', strength: .5),
|
}).toList();
|
||||||
].map((e) => Padding(padding: EdgeInsets.all(10), child: e)).toList();
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
/// Background
|
/// Blurred Bg
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: ValueListenableBuilder<int>(
|
child: ValueListenableBuilder<int>(
|
||||||
valueListenable: _currentArtifactIndex,
|
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(
|
BottomCenter(
|
||||||
child: ValueListenableBuilder<int>(
|
child: ValueListenableBuilder<int>(
|
||||||
valueListenable: _currentArtifactIndex,
|
valueListenable: _currentArtifactIndex,
|
||||||
builder: (_, value, __) => _CarouselBottomTextWithCircle(
|
builder: (_, value, __) => _BottomTextContent(
|
||||||
artifact: _artifacts[value],
|
artifact: _artifacts[value],
|
||||||
height: bottomHeight,
|
height: bottomHeight,
|
||||||
shortMode: shortMode,
|
shortMode: shortMode,
|
||||||
@ -99,404 +122,32 @@ class _ArtifactScreenState extends State<ArtifactCarouselScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
/// Carousel Items
|
/// Header
|
||||||
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(
|
SimpleHeader(
|
||||||
$strings.artifactsTitleArtifacts,
|
$strings.artifactsTitleArtifacts,
|
||||||
showBackBtn: true,
|
showBackBtn: false,
|
||||||
isTransparent: true,
|
isTransparent: true,
|
||||||
trailing: (context) => CircleBtn(
|
trailing: (context) => CircleBtn(
|
||||||
semanticLabel: $strings.artifactsButtonBrowse,
|
semanticLabel: $strings.artifactsButtonBrowse,
|
||||||
onPressed: () {},
|
onPressed: _handleSearchTap,
|
||||||
child: AppIcon(AppIcons.search),
|
child: AppIcon(AppIcons.search),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles the carousel specific logic, like setting the height and vertical alignment of each item.
|
OverflowBox _buildBgCircle(double height) {
|
||||||
/// This lets the child simply render it's contents
|
const double size = 1500;
|
||||||
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() {
|
|
||||||
return OverflowBox(
|
return OverflowBox(
|
||||||
maxWidth: 2000,
|
maxWidth: size,
|
||||||
maxHeight: 2000,
|
maxHeight: size,
|
||||||
child: Transform.translate(
|
child: Transform.translate(
|
||||||
offset: Offset(0, 1000 - height * .7),
|
offset: Offset(0, size / 2),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: $styles.colors.offWhite.withOpacity(0.8),
|
color: $styles.colors.offWhite.withOpacity(0.8),
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(999)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(size * .45)),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
@ -72,7 +72,7 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
|
|||||||
return LayoutBuilder(builder: (_, constraints) {
|
return LayoutBuilder(builder: (_, constraints) {
|
||||||
bool shortMode = constraints.biggest.height < 700;
|
bool shortMode = constraints.biggest.height < 700;
|
||||||
double illustrationHeight = shortMode ? 250 : 280;
|
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
|
/// Attempt to maintain a similar aspect ratio for the image within the app-bar
|
||||||
double maxAppBarHeight = min(context.widthPx, $styles.sizes.maxContentWidth1) * 1.5;
|
double maxAppBarHeight = min(context.widthPx, $styles.sizes.maxContentWidth1) * 1.5;
|
||||||
|
@ -80,7 +80,7 @@ class _AppBar extends StatelessWidget {
|
|||||||
/// Colored overlay
|
/// Colored overlay
|
||||||
if (showOverlay) ...[
|
if (showOverlay) ...[
|
||||||
AnimatedContainer(
|
AnimatedContainer(
|
||||||
duration: $styles.times.slow,
|
duration: $styles.times.med,
|
||||||
color: wonderType.bgColor.withOpacity(showOverlay ? .8 : 0),
|
color: wonderType.bgColor.withOpacity(showOverlay ? .8 : 0),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
|||||||
import 'package:wonders/common_libs.dart';
|
import 'package:wonders/common_libs.dart';
|
||||||
import 'package:wonders/ui/common/app_icons.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/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/static_text_scale.dart';
|
||||||
import 'package:wonders/ui/common/themed_text.dart';
|
import 'package:wonders/ui/common/themed_text.dart';
|
||||||
import 'package:wonders/ui/common/utils/app_haptics.dart';
|
import 'package:wonders/ui/common/utils/app_haptics.dart';
|
||||||
@ -139,6 +140,10 @@ class _IntroScreenState extends State<IntroScreen> {
|
|||||||
child: _buildNavText(context),
|
child: _buildNavText(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Build a cpl overlays to hide the content when swiping on very wide screens
|
||||||
|
_buildHzGradientOverlay(left: true),
|
||||||
|
_buildHzGradientOverlay(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return DefaultTextColor(
|
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) {
|
Widget _buildFinishBtn(BuildContext context) {
|
||||||
return ValueListenableBuilder<int>(
|
return ValueListenableBuilder<int>(
|
||||||
valueListenable: _currentPage,
|
valueListenable: _currentPage,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user