Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 229 additions & 21 deletions gallery/lib/pages/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ const _horizontalPadding = 32.0;
const _carouselItemMargin = 8.0;
const _horizontalDesktopPadding = 81.0;
const _carouselHeightMin = 200.0 + 2 * _carouselItemMargin;
const _desktopCardsPerPage = 4;

const shrineTitle = 'Shrine';
const rallyTitle = 'Rally';
const craneTitle = 'Crane';
const homeCategoryMaterial = 'MATERIAL';
const homeCategoryCupertino = 'CUPERTINO';
const _shrineTitle = 'Shrine';
const _rallyTitle = 'Rally';
const _craneTitle = 'Crane';
const _homeCategoryMaterial = 'MATERIAL';
const _homeCategoryCupertino = 'CUPERTINO';

class ToggleSplashNotification extends Notification {}

Expand All @@ -52,9 +53,9 @@ class HomePage extends StatelessWidget {
Widget build(BuildContext context) {
var carouselHeight = _carouselHeight(.7, context);
final isDesktop = isDisplayDesktop(context);
final carouselCards = <_CarouselCard>[
final carouselCards = <Widget>[
_CarouselCard(
title: shrineTitle,
title: _shrineTitle,
subtitle: GalleryLocalizations.of(context).shrineDescription,
asset: 'assets/studies/shrine_card.png',
assetDark: 'assets/studies/shrine_card_dark.png',
Expand All @@ -63,7 +64,7 @@ class HomePage extends StatelessWidget {
navigatorKey: NavigatorKeys.shrine,
),
_CarouselCard(
title: rallyTitle,
title: _rallyTitle,
subtitle: GalleryLocalizations.of(context).rallyDescription,
textColor: RallyColors.accountColors[0],
asset: 'assets/studies/rally_card.png',
Expand All @@ -72,7 +73,7 @@ class HomePage extends StatelessWidget {
navigatorKey: NavigatorKeys.rally,
),
_CarouselCard(
title: craneTitle,
title: _craneTitle,
subtitle: GalleryLocalizations.of(context).craneDescription,
asset: 'assets/studies/crane_card.png',
assetDark: 'assets/studies/crane_card_dark.png',
Expand Down Expand Up @@ -101,12 +102,12 @@ class HomePage extends StatelessWidget {
if (isDesktop) {
final desktopCategoryItems = <_DesktopCategoryItem>[
_DesktopCategoryItem(
title: homeCategoryMaterial,
title: _homeCategoryMaterial,
imageString: 'assets/icons/material/material.png',
demos: materialDemos(context),
),
_DesktopCategoryItem(
title: homeCategoryCupertino,
title: _homeCategoryCupertino,
imageString: 'assets/icons/cupertino/cupertino.png',
demos: cupertinoDemos(context),
),
Expand All @@ -120,12 +121,15 @@ class HomePage extends StatelessWidget {
return Scaffold(
body: ListView(
padding: EdgeInsetsDirectional.only(
start: _horizontalDesktopPadding,
top: isDesktop ? firstHeaderDesktopTopPadding : 21,
end: _horizontalDesktopPadding,
),
children: [
ExcludeSemantics(child: _GalleryHeader()),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: _horizontalDesktopPadding,
),
child: ExcludeSemantics(child: _GalleryHeader()),
),

/// TODO: When Focus widget becomes better remove dummy Focus
/// variable.
Expand All @@ -143,15 +147,19 @@ class HomePage extends StatelessWidget {
),
Container(
height: carouselHeight,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: spaceBetween(30, carouselCards),
child: _DesktopCarousel(children: carouselCards),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: _horizontalDesktopPadding,
),
child: _CategoriesHeader(),
),
_CategoriesHeader(),
Container(
height: 585,
padding: const EdgeInsets.symmetric(
horizontal: _horizontalDesktopPadding,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
Expand All @@ -160,7 +168,9 @@ class HomePage extends StatelessWidget {
),
Padding(
padding: const EdgeInsetsDirectional.only(
start: _horizontalDesktopPadding,
bottom: 81,
end: _horizontalDesktopPadding,
top: 109,
),
child: Row(
Expand Down Expand Up @@ -332,7 +342,7 @@ class _AnimatedHomePageState extends State<_AnimatedHomePage>
startDelayFraction: 0.00,
controller: _animationController,
child: CategoryListItem(
title: homeCategoryMaterial,
title: _homeCategoryMaterial,
imageString: 'assets/icons/material/material.png',
demos: materialDemos(context),
),
Expand All @@ -341,7 +351,7 @@ class _AnimatedHomePageState extends State<_AnimatedHomePage>
startDelayFraction: 0.05,
controller: _animationController,
child: CategoryListItem(
title: homeCategoryCupertino,
title: _homeCategoryCupertino,
imageString: 'assets/icons/cupertino/cupertino.png',
demos: cupertinoDemos(context),
),
Expand Down Expand Up @@ -715,6 +725,204 @@ class _CarouselState extends State<_Carousel>
}
}

/// This creates a horizontally scrolling [ListView] of items.
///
/// This class uses a [ListView] with a custom [ScrollPhysics] to enable
/// snapping behavior. A [PageView] was considered but does not allow for
/// multiple pages visible without centering the first page.
class _DesktopCarousel extends StatefulWidget {
const _DesktopCarousel({Key key, this.children}) : super(key: key);

final List<Widget> children;

@override
_DesktopCarouselState createState() => _DesktopCarouselState();
}

class _DesktopCarouselState extends State<_DesktopCarousel> {
static const cardPadding = 15.0;
ScrollController _controller;

@override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(() {
setState(() {});
});
}

@override
dispose() {
_controller.dispose();
super.dispose();
}

Widget _builder(int index) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: cardPadding,
),
child: widget.children[index],
);
}

@override
Widget build(BuildContext context) {
var showPreviousButton = false;
var showNextButton = true;
// Only check this after the _controller has been attached to the ListView.
if (_controller.hasClients) {
showPreviousButton = _controller.offset > 0;
showNextButton =
_controller.offset < _controller.position.maxScrollExtent;
}
final totalWidth = MediaQuery.of(context).size.width -
(_horizontalDesktopPadding - cardPadding) * 2;
final itemWidth = totalWidth / _desktopCardsPerPage;

return Stack(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: _horizontalDesktopPadding - cardPadding,
),
child: ListView.builder(
scrollDirection: Axis.horizontal,
physics: _SnappingScrollPhysics(),
controller: _controller,
itemExtent: itemWidth,
itemCount: widget.children.length,
itemBuilder: (context, index) => _builder(index),
),
),
if (showPreviousButton)
_DesktopPageButton(
onTap: () {
_controller.animateTo(
_controller.offset - itemWidth,
duration: Duration(milliseconds: 200),
curve: Curves.easeInOut,
);
},
),
if (showNextButton)
_DesktopPageButton(
isEnd: true,
onTap: () {
_controller.animateTo(
_controller.offset + itemWidth,
duration: Duration(milliseconds: 200),
curve: Curves.easeInOut,
);
},
),
],
);
}
}

/// Scrolling physics that snaps to the new item in the [_DesktopCarousel].
class _SnappingScrollPhysics extends ScrollPhysics {
const _SnappingScrollPhysics({ScrollPhysics parent}) : super(parent: parent);

@override
_SnappingScrollPhysics applyTo(ScrollPhysics ancestor) {
return _SnappingScrollPhysics(parent: buildParent(ancestor));
}

double _getTargetPixels(
ScrollMetrics position,
Tolerance tolerance,
double velocity,
) {
final itemWidth = position.viewportDimension / _desktopCardsPerPage;
double item = position.pixels / itemWidth;
if (velocity < -tolerance.velocity) {
item -= 0.5;
} else if (velocity > tolerance.velocity) {
item += 0.5;
}
return math.min(
item.roundToDouble() * itemWidth,
position.maxScrollExtent,
);
}

@override
Simulation createBallisticSimulation(
ScrollMetrics position,
double velocity,
) {
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
return super.createBallisticSimulation(position, velocity);
}
final Tolerance tolerance = this.tolerance;
final double target = _getTargetPixels(position, tolerance, velocity);
if (target != position.pixels) {
return ScrollSpringSimulation(
spring,
position.pixels,
target,
velocity,
tolerance: tolerance,
);
}
return null;
}

@override
bool get allowImplicitScrolling => false;
}

class _DesktopPageButton extends StatelessWidget {
const _DesktopPageButton({
Key key,
this.isEnd = false,
this.onTap,
}) : super(key: key);

final bool isEnd;
final GestureTapCallback onTap;

@override
Widget build(BuildContext context) {
final buttonSize = 58.0;
final padding = _horizontalDesktopPadding - buttonSize / 2;
return Align(
alignment: isEnd
? AlignmentDirectional.centerEnd
: AlignmentDirectional.centerStart,
child: Container(
width: buttonSize,
height: buttonSize,
margin: EdgeInsetsDirectional.only(
start: isEnd ? 0 : padding,
end: isEnd ? padding : 0,
),
child: Tooltip(
message: isEnd
? MaterialLocalizations.of(context).nextPageTooltip
: MaterialLocalizations.of(context).previousPageTooltip,
child: Material(
color: Colors.black.withOpacity(0.5),
shape: CircleBorder(),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Icon(
isEnd ? Icons.arrow_forward_ios : Icons.arrow_back_ios,
),
),
),
),
),
);
}
}

class _CarouselCard extends StatelessWidget {
const _CarouselCard({
Key key,
Expand Down