Created
July 28, 2017 18:46
-
-
Save HansMuller/aedeb2df3292f492fb71a0860f701303 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:math' as math; | |
import 'package:flutter/material.dart'; | |
const _kHeadingGap = 8.0; | |
const _kMinHeadingsHeight = 90.0; | |
// Scroll animation from full-screen section heading column layout to row layout. | |
const Duration _kScrollDuration = const Duration(milliseconds: 400); | |
const Curve _kScrollCurve = Curves.fastOutSlowIn; | |
const TextStyle _kTitleStyle = const TextStyle( | |
inherit: false, | |
fontSize: 24.0, | |
fontWeight: FontWeight.w500, | |
color: Colors.white, | |
textBaseline: TextBaseline.alphabetic, | |
); | |
class Section { | |
const Section({ this.title, this.color }); | |
final String title; | |
final Color color; | |
} | |
const List<Section> allSections = const <Section>[ | |
const Section( | |
title: 'ONE', | |
color: Colors.indigo, | |
), | |
const Section( | |
title: 'TWO', | |
color: Colors.deepPurple, | |
), | |
const Section( | |
title: 'FREE', | |
color: Colors.amber, | |
), | |
const Section( | |
title: 'FOUR', | |
color: Colors.lightBlue, | |
), | |
const Section( | |
title: 'FIVE', | |
color: Colors.teal, | |
), | |
]; | |
class SectionItem extends StatelessWidget { | |
SectionItem({ Key key, this.section, this.index }) : super(key: key); | |
final Section section; | |
final int index; | |
@override | |
build(BuildContext context) { | |
return new Container( | |
margin: const EdgeInsets.symmetric(vertical: 8.0), | |
height: 48.0, // height + vertical margins = 64.0, see SliverFixedExtentList itemExtent below | |
color: section.color, | |
alignment: FractionalOffset.center, | |
child: new Text('${section.title} Item $index', style: _kTitleStyle), | |
); | |
} | |
} | |
class SectionHeading extends StatelessWidget { | |
SectionHeading({ Key key, this.section }) : super(key: key); | |
final Section section; | |
@override | |
Widget build(BuildContext context) { | |
return new Container( | |
decoration: new BoxDecoration( | |
borderRadius: new BorderRadius.circular(4.0), | |
color: section.color, | |
), | |
alignment: FractionalOffset.center, | |
child: new Text(section.title, style: _kTitleStyle), | |
); | |
} | |
} | |
class SectionHeadingsLayout extends MultiChildLayoutDelegate { | |
SectionHeadingsLayout({ | |
this.headingCount, | |
this.maxHeight, | |
this.selectedIndex, | |
}); | |
final int headingCount; | |
final double maxHeight; | |
final int selectedIndex; | |
@override | |
void performLayout(Size size) { | |
final double headingHeight = (maxHeight - _kHeadingGap * (headingCount - 1)) / headingCount; | |
final double tColumnToRow = 1.0 - (size.height - _kMinHeadingsHeight) / (maxHeight - _kMinHeadingsHeight); | |
final double colHeadingHeight = (maxHeight - _kHeadingGap * (headingCount - 1)) / headingCount; | |
final Size colHeadingSize = new Size(size.width, colHeadingHeight); | |
final Size rowHeadingSize = new Size(size.width, _kMinHeadingsHeight); | |
double columnY = 0.0; | |
double rowX = -1.0 * selectedIndex * rowHeadingSize.width; | |
for (int index = 0; index < headingCount; index++) { | |
final Rect colHeadingRect = new Offset(0.0, columnY) & colHeadingSize; | |
final Rect rowHeadingRect = new Offset(rowX, 0.0) & rowHeadingSize; | |
final Rect headingRect = Rect.lerp(colHeadingRect, rowHeadingRect, tColumnToRow); | |
final String headingId = 'heading$index'; | |
final Size headingSize = layoutChild(headingId, new BoxConstraints.tight(headingRect.size)); | |
positionChild(headingId, headingRect.topLeft); | |
columnY += headingSize.height + _kHeadingGap; | |
rowX += headingSize.width; | |
} | |
} | |
@override | |
bool shouldRelayout(SectionHeadingsLayout oldDelegate) { | |
return headingCount != oldDelegate.headingCount | |
|| maxHeight != oldDelegate.maxHeight | |
|| selectedIndex != oldDelegate.selectedIndex; | |
} | |
} | |
class SliverSectionHeadingsDelegate extends SliverPersistentHeaderDelegate { | |
SliverSectionHeadingsDelegate({ this.maxHeight, this.child }); | |
final double maxHeight; | |
final Widget child; | |
@override double get minExtent => _kMinHeadingsHeight; | |
@override double get maxExtent => math.max(maxHeight, _kMinHeadingsHeight); | |
@override | |
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { | |
return new SizedBox.expand(child: child); | |
} | |
@override | |
bool shouldRebuild(SliverSectionHeadingsDelegate oldDelegate) { | |
return maxHeight != oldDelegate.maxHeight || child != oldDelegate.child; | |
} | |
} | |
class PosseDemo extends StatefulWidget { | |
@override | |
PosseDemoState createState() => new PosseDemoState(); | |
} | |
class PosseDemoState extends State<PosseDemo> { | |
ScrollController _scrollController = new ScrollController(); | |
int _selectedIndex = 2; | |
@override | |
Widget build(BuildContext context) { | |
final MediaQueryData mediaQueryData = MediaQuery.of(context); | |
final double screenHeight = mediaQueryData.size.height; | |
final double statusBarHeight = mediaQueryData.padding.top; | |
final double maxHeadingsHeight = screenHeight - statusBarHeight; | |
List<Widget> sectionHeadings = new List<Widget>(allSections.length); | |
for (int index = 0; index < allSections.length; index++) { | |
sectionHeadings[index] = new LayoutId( | |
id: 'heading$index', | |
child: new GestureDetector( | |
onTap: () { | |
setState(() { | |
_selectedIndex = index; | |
}); | |
final double offset = maxHeadingsHeight - _kMinHeadingsHeight; | |
_scrollController.animateTo(offset, curve: _kScrollCurve, duration: _kScrollDuration); | |
}, | |
child: new SectionHeading(section: allSections[index]), | |
), | |
); | |
} | |
return new Scaffold( | |
body: new Padding( | |
padding: new EdgeInsets.only(top: statusBarHeight), | |
child: new CustomScrollView( | |
controller: _scrollController, | |
slivers: <Widget>[ | |
new SliverPersistentHeader( | |
pinned: true, | |
delegate: new SliverSectionHeadingsDelegate( | |
maxHeight: maxHeadingsHeight, | |
child: new CustomMultiChildLayout( | |
delegate: new SectionHeadingsLayout( | |
headingCount: allSections.length, | |
maxHeight: maxHeadingsHeight, | |
selectedIndex: _selectedIndex, | |
), | |
children: sectionHeadings, | |
), | |
), | |
), | |
new SliverFixedExtentList( | |
itemExtent: 64.0, | |
delegate: new SliverChildBuilderDelegate( | |
(BuildContext context, int index) { | |
return new SectionItem( | |
section: allSections[_selectedIndex], | |
index: index, | |
); | |
}, | |
), | |
) | |
], | |
), | |
), | |
); | |
} | |
} | |
void main() { | |
runApp(new MaterialApp(home: new PosseDemo())); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment