@@ -30,12 +30,13 @@ const _horizontalPadding = 32.0;
3030const _carouselItemMargin = 8.0 ;
3131const _horizontalDesktopPadding = 81.0 ;
3232const _carouselHeightMin = 200.0 + 2 * _carouselItemMargin;
33+ const _desktopCardsPerPage = 4 ;
3334
34- const shrineTitle = 'Shrine' ;
35- const rallyTitle = 'Rally' ;
36- const craneTitle = 'Crane' ;
37- const homeCategoryMaterial = 'MATERIAL' ;
38- const homeCategoryCupertino = 'CUPERTINO' ;
35+ const _shrineTitle = 'Shrine' ;
36+ const _rallyTitle = 'Rally' ;
37+ const _craneTitle = 'Crane' ;
38+ const _homeCategoryMaterial = 'MATERIAL' ;
39+ const _homeCategoryCupertino = 'CUPERTINO' ;
3940
4041class ToggleSplashNotification extends Notification {}
4142
@@ -52,9 +53,9 @@ class HomePage extends StatelessWidget {
5253 Widget build (BuildContext context) {
5354 var carouselHeight = _carouselHeight (.7 , context);
5455 final isDesktop = isDisplayDesktop (context);
55- final carouselCards = < _CarouselCard > [
56+ final carouselCards = < Widget > [
5657 _CarouselCard (
57- title: shrineTitle ,
58+ title: _shrineTitle ,
5859 subtitle: GalleryLocalizations .of (context).shrineDescription,
5960 asset: 'assets/studies/shrine_card.png' ,
6061 assetDark: 'assets/studies/shrine_card_dark.png' ,
@@ -63,7 +64,7 @@ class HomePage extends StatelessWidget {
6364 navigatorKey: NavigatorKeys .shrine,
6465 ),
6566 _CarouselCard (
66- title: rallyTitle ,
67+ title: _rallyTitle ,
6768 subtitle: GalleryLocalizations .of (context).rallyDescription,
6869 textColor: RallyColors .accountColors[0 ],
6970 asset: 'assets/studies/rally_card.png' ,
@@ -72,7 +73,7 @@ class HomePage extends StatelessWidget {
7273 navigatorKey: NavigatorKeys .rally,
7374 ),
7475 _CarouselCard (
75- title: craneTitle ,
76+ title: _craneTitle ,
7677 subtitle: GalleryLocalizations .of (context).craneDescription,
7778 asset: 'assets/studies/crane_card.png' ,
7879 assetDark: 'assets/studies/crane_card_dark.png' ,
@@ -101,12 +102,12 @@ class HomePage extends StatelessWidget {
101102 if (isDesktop) {
102103 final desktopCategoryItems = < _DesktopCategoryItem > [
103104 _DesktopCategoryItem (
104- title: homeCategoryMaterial ,
105+ title: _homeCategoryMaterial ,
105106 imageString: 'assets/icons/material/material.png' ,
106107 demos: materialDemos (context),
107108 ),
108109 _DesktopCategoryItem (
109- title: homeCategoryCupertino ,
110+ title: _homeCategoryCupertino ,
110111 imageString: 'assets/icons/cupertino/cupertino.png' ,
111112 demos: cupertinoDemos (context),
112113 ),
@@ -120,12 +121,15 @@ class HomePage extends StatelessWidget {
120121 return Scaffold (
121122 body: ListView (
122123 padding: EdgeInsetsDirectional .only (
123- start: _horizontalDesktopPadding,
124124 top: isDesktop ? firstHeaderDesktopTopPadding : 21 ,
125- end: _horizontalDesktopPadding,
126125 ),
127126 children: [
128- ExcludeSemantics (child: _GalleryHeader ()),
127+ Padding (
128+ padding: const EdgeInsets .symmetric (
129+ horizontal: _horizontalDesktopPadding,
130+ ),
131+ child: ExcludeSemantics (child: _GalleryHeader ()),
132+ ),
129133
130134 /// TODO: When Focus widget becomes better remove dummy Focus
131135 /// variable.
@@ -143,15 +147,19 @@ class HomePage extends StatelessWidget {
143147 ),
144148 Container (
145149 height: carouselHeight,
146- child: Row (
147- mainAxisSize: MainAxisSize .max,
148- mainAxisAlignment: MainAxisAlignment .spaceBetween,
149- children: spaceBetween (30 , carouselCards),
150+ child: _DesktopCarousel (children: carouselCards),
151+ ),
152+ Padding (
153+ padding: const EdgeInsets .symmetric (
154+ horizontal: _horizontalDesktopPadding,
150155 ),
156+ child: _CategoriesHeader (),
151157 ),
152- _CategoriesHeader (),
153158 Container (
154159 height: 585 ,
160+ padding: const EdgeInsets .symmetric (
161+ horizontal: _horizontalDesktopPadding,
162+ ),
155163 child: Row (
156164 mainAxisSize: MainAxisSize .max,
157165 mainAxisAlignment: MainAxisAlignment .spaceBetween,
@@ -160,7 +168,9 @@ class HomePage extends StatelessWidget {
160168 ),
161169 Padding (
162170 padding: const EdgeInsetsDirectional .only (
171+ start: _horizontalDesktopPadding,
163172 bottom: 81 ,
173+ end: _horizontalDesktopPadding,
164174 top: 109 ,
165175 ),
166176 child: Row (
@@ -332,7 +342,7 @@ class _AnimatedHomePageState extends State<_AnimatedHomePage>
332342 startDelayFraction: 0.00 ,
333343 controller: _animationController,
334344 child: CategoryListItem (
335- title: homeCategoryMaterial ,
345+ title: _homeCategoryMaterial ,
336346 imageString: 'assets/icons/material/material.png' ,
337347 demos: materialDemos (context),
338348 ),
@@ -341,7 +351,7 @@ class _AnimatedHomePageState extends State<_AnimatedHomePage>
341351 startDelayFraction: 0.05 ,
342352 controller: _animationController,
343353 child: CategoryListItem (
344- title: homeCategoryCupertino ,
354+ title: _homeCategoryCupertino ,
345355 imageString: 'assets/icons/cupertino/cupertino.png' ,
346356 demos: cupertinoDemos (context),
347357 ),
@@ -715,6 +725,204 @@ class _CarouselState extends State<_Carousel>
715725 }
716726}
717727
728+ /// This creates a horizontally scrolling [ListView] of items.
729+ ///
730+ /// This class uses a [ListView] with a custom [ScrollPhysics] to enable
731+ /// snapping behavior. A [PageView] was considered but does not allow for
732+ /// multiple pages visible without centering the first page.
733+ class _DesktopCarousel extends StatefulWidget {
734+ const _DesktopCarousel ({Key key, this .children}) : super (key: key);
735+
736+ final List <Widget > children;
737+
738+ @override
739+ _DesktopCarouselState createState () => _DesktopCarouselState ();
740+ }
741+
742+ class _DesktopCarouselState extends State <_DesktopCarousel > {
743+ static const cardPadding = 15.0 ;
744+ ScrollController _controller;
745+
746+ @override
747+ void initState () {
748+ super .initState ();
749+ _controller = ScrollController ();
750+ _controller.addListener (() {
751+ setState (() {});
752+ });
753+ }
754+
755+ @override
756+ dispose () {
757+ _controller.dispose ();
758+ super .dispose ();
759+ }
760+
761+ Widget _builder (int index) {
762+ return Padding (
763+ padding: const EdgeInsets .symmetric (
764+ vertical: 8 ,
765+ horizontal: cardPadding,
766+ ),
767+ child: widget.children[index],
768+ );
769+ }
770+
771+ @override
772+ Widget build (BuildContext context) {
773+ var showPreviousButton = false ;
774+ var showNextButton = true ;
775+ // Only check this after the _controller has been attached to the ListView.
776+ if (_controller.hasClients) {
777+ showPreviousButton = _controller.offset > 0 ;
778+ showNextButton =
779+ _controller.offset < _controller.position.maxScrollExtent;
780+ }
781+ final totalWidth = MediaQuery .of (context).size.width -
782+ (_horizontalDesktopPadding - cardPadding) * 2 ;
783+ final itemWidth = totalWidth / _desktopCardsPerPage;
784+
785+ return Stack (
786+ children: [
787+ Padding (
788+ padding: const EdgeInsets .symmetric (
789+ horizontal: _horizontalDesktopPadding - cardPadding,
790+ ),
791+ child: ListView .builder (
792+ scrollDirection: Axis .horizontal,
793+ physics: _SnappingScrollPhysics (),
794+ controller: _controller,
795+ itemExtent: itemWidth,
796+ itemCount: widget.children.length,
797+ itemBuilder: (context, index) => _builder (index),
798+ ),
799+ ),
800+ if (showPreviousButton)
801+ _DesktopPageButton (
802+ onTap: () {
803+ _controller.animateTo (
804+ _controller.offset - itemWidth,
805+ duration: Duration (milliseconds: 200 ),
806+ curve: Curves .easeInOut,
807+ );
808+ },
809+ ),
810+ if (showNextButton)
811+ _DesktopPageButton (
812+ isEnd: true ,
813+ onTap: () {
814+ _controller.animateTo (
815+ _controller.offset + itemWidth,
816+ duration: Duration (milliseconds: 200 ),
817+ curve: Curves .easeInOut,
818+ );
819+ },
820+ ),
821+ ],
822+ );
823+ }
824+ }
825+
826+ /// Scrolling physics that snaps to the new item in the [_DesktopCarousel] .
827+ class _SnappingScrollPhysics extends ScrollPhysics {
828+ const _SnappingScrollPhysics ({ScrollPhysics parent}) : super (parent: parent);
829+
830+ @override
831+ _SnappingScrollPhysics applyTo (ScrollPhysics ancestor) {
832+ return _SnappingScrollPhysics (parent: buildParent (ancestor));
833+ }
834+
835+ double _getTargetPixels (
836+ ScrollMetrics position,
837+ Tolerance tolerance,
838+ double velocity,
839+ ) {
840+ final itemWidth = position.viewportDimension / _desktopCardsPerPage;
841+ double item = position.pixels / itemWidth;
842+ if (velocity < - tolerance.velocity) {
843+ item -= 0.5 ;
844+ } else if (velocity > tolerance.velocity) {
845+ item += 0.5 ;
846+ }
847+ return math.min (
848+ item.roundToDouble () * itemWidth,
849+ position.maxScrollExtent,
850+ );
851+ }
852+
853+ @override
854+ Simulation createBallisticSimulation (
855+ ScrollMetrics position,
856+ double velocity,
857+ ) {
858+ if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
859+ (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
860+ return super .createBallisticSimulation (position, velocity);
861+ }
862+ final Tolerance tolerance = this .tolerance;
863+ final double target = _getTargetPixels (position, tolerance, velocity);
864+ if (target != position.pixels) {
865+ return ScrollSpringSimulation (
866+ spring,
867+ position.pixels,
868+ target,
869+ velocity,
870+ tolerance: tolerance,
871+ );
872+ }
873+ return null ;
874+ }
875+
876+ @override
877+ bool get allowImplicitScrolling => false ;
878+ }
879+
880+ class _DesktopPageButton extends StatelessWidget {
881+ const _DesktopPageButton ({
882+ Key key,
883+ this .isEnd = false ,
884+ this .onTap,
885+ }) : super (key: key);
886+
887+ final bool isEnd;
888+ final GestureTapCallback onTap;
889+
890+ @override
891+ Widget build (BuildContext context) {
892+ final buttonSize = 58.0 ;
893+ final padding = _horizontalDesktopPadding - buttonSize / 2 ;
894+ return Align (
895+ alignment: isEnd
896+ ? AlignmentDirectional .centerEnd
897+ : AlignmentDirectional .centerStart,
898+ child: Container (
899+ width: buttonSize,
900+ height: buttonSize,
901+ margin: EdgeInsetsDirectional .only (
902+ start: isEnd ? 0 : padding,
903+ end: isEnd ? padding : 0 ,
904+ ),
905+ child: Tooltip (
906+ message: isEnd
907+ ? MaterialLocalizations .of (context).nextPageTooltip
908+ : MaterialLocalizations .of (context).previousPageTooltip,
909+ child: Material (
910+ color: Colors .black.withOpacity (0.5 ),
911+ shape: CircleBorder (),
912+ clipBehavior: Clip .antiAlias,
913+ child: InkWell (
914+ onTap: onTap,
915+ child: Icon (
916+ isEnd ? Icons .arrow_forward_ios : Icons .arrow_back_ios,
917+ ),
918+ ),
919+ ),
920+ ),
921+ ),
922+ );
923+ }
924+ }
925+
718926class _CarouselCard extends StatelessWidget {
719927 const _CarouselCard ({
720928 Key key,
0 commit comments