Skip to content

Instantly share code, notes, and snippets.

@collinjackson
Last active July 21, 2017 15:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save collinjackson/7c0e030b21a0240f8394fe0d06284172 to your computer and use it in GitHub Desktop.
Save collinjackson/7c0e030b21a0240f8394fe0d06284172 to your computer and use it in GitHub Desktop.
// Non-confidential feedback
import 'dart:math' as math;
import 'package:flutter/material.dart';
const _kMidHeadingsHeight = 128.0;
const _kMinHeadingsHeight = 72.0;
// SectionHeadingsLayout computes the initial bounds of each heading based
// on the layout configuration:
// - Column: headings are as wide as possible and equal height
// - Row: each heading's width matches the available width, height is _mMinHeadingsHeight
// The final bounds for each heading are based on the initial bounds "deflated"
// by these inset values.
const EdgeInsets _kColHeadingInsets = const EdgeInsets.only(left: 32.0, right: 8.0);
const EdgeInsets _kRowHeadingInsets = EdgeInsets.zero;
// SectionHeadingsLayout inserts a vertical gap between headings in the column
// configuration.
const _kHeadingGap = 8.0;
// Scroll animation from the full-screen section heading column configuration
// to the row configuration.
const Duration _kScrollDuration = const Duration(milliseconds: 400);
const Curve _kScrollCurve = Curves.fastOutSlowIn;
// If the section heading is in its row configuration, flings slower
// than this will cause it to stay that way rather than snap to the
// column configuration.
const _kToZeroDragThreshold = -800.0;
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.only(top: 16.0, left: 8.0, right: 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(
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 - _kMidHeadingsHeight) / (maxHeight - _kMidHeadingsHeight);
final double colHeadingHeight = (maxHeight - _kHeadingGap * (headingCount - 1)) / headingCount;
final Size colHeadingSize = new Size(size.width, colHeadingHeight);
final double rowHeadingHeight = (tColumnToRow <= 1.0) ? _kMidHeadingsHeight : size.height;
final Size rowHeadingSize = new Size(size.width, rowHeadingHeight);
double columnY = 0.0;
double rowX = -1.0 * selectedIndex * rowHeadingSize.width;
for (int index = 0; index < headingCount; index++) {
final Rect colHeadingRect = _kColHeadingInsets.deflateRect(new Offset(0.0, columnY) & colHeadingSize);
final Rect rowHeadingRect = _kRowHeadingInsets.deflateRect(new Offset(rowX, 0.0) & rowHeadingSize);
final Rect headingRect = Rect.lerp(colHeadingRect, rowHeadingRect, tColumnToRow.clamp(0.0, 1.0));
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 _SnappingScrollPhysics extends ClampingScrollPhysics {
const _SnappingScrollPhysics({
ScrollPhysics parent,
@required this.midScrollOffset,
}) : assert(midScrollOffset != null), super(parent: parent);
final double midScrollOffset;
@override
_SnappingScrollPhysics applyTo(ScrollPhysics ancestor) {
return new _SnappingScrollPhysics(parent: buildParent(ancestor), midScrollOffset: midScrollOffset);
}
Simulation _toMidScrollOffsetSimulation(double offset, double dragVelocity) {
final double velocity = math.max(dragVelocity, minFlingVelocity);
return new ScrollSpringSimulation(spring, offset, midScrollOffset, velocity, tolerance: tolerance);
}
Simulation _toZeroScrollOffsetSimulation(double offset, double dragVelocity) {
final double velocity = math.max(dragVelocity, minFlingVelocity);
return new ScrollSpringSimulation(spring, offset, 0.0, velocity, tolerance: tolerance);
}
@override
Simulation createBallisticSimulation(ScrollMetrics position, double dragVelocity) {
final Simulation simulation = super.createBallisticSimulation(position, dragVelocity);
final double offset = position.pixels;
if (simulation != null) {
final double simulationEnd = simulation.x(double.INFINITY);
// The drag ended with sufficient velocity to trigger creating a simulation.
// If the simulation is headed up towards midScrollOffset then snap it there.
// Similarly if the simulation is headed down past midScrollOffset but will
// not reach zero, then snap it to zero.
if (offset >= midScrollOffset && simulationEnd >= midScrollOffset)
return simulation;
if (dragVelocity > 0.0)
return _toMidScrollOffsetSimulation(offset, dragVelocity);
if (dragVelocity < _kToZeroDragThreshold)
return _toZeroScrollOffsetSimulation(offset, dragVelocity);
} else {
// The user ended the drag with little or no velocity. If they
// didn't leave the the offset above midScrollOffset, then
// snap to midScrollOffset if they're more than halfway there,
// otherwise snap to zero.
final double snapThreshold = midScrollOffset / 2.0;
if (offset >= snapThreshold && offset < midScrollOffset)
return _toMidScrollOffsetSimulation(offset, dragVelocity);
if (offset > 0.0 && offset < snapThreshold)
return _toZeroScrollOffsetSimulation(offset, dragVelocity);
}
return simulation;
}
}
class PosseDemo extends StatefulWidget {
@override
PosseDemoState createState() => new PosseDemoState();
}
class PosseDemoState extends State<PosseDemo> {
ScrollController _scrollController = new ScrollController();
PageController _pageController;
int _previousIndex = 2;
int _selectedIndex = 2;
Widget _buildRowSectionHeadings(Size size, double maxHeadingsHeight) {
_pageController = new PageController(initialPage: _selectedIndex);
return new PageView(
controller: _pageController,
onPageChanged: (int page) {
setState(() {
_previousIndex = _selectedIndex;
_selectedIndex = page;
});
},
children: allSections.map((Section section) {
return new SectionHeading(section: section);
}).toList(),
);
}
Widget _buildColSectionHeadings(Size size, double maxHeadingsHeight, double midHeightScrollOffset) {
_pageController = null;
final 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;
});
_scrollController.animateTo(midHeightScrollOffset,
curve: _kScrollCurve,
duration: _kScrollDuration,
);
},
child: new SectionHeading(section: allSections[index]),
),
);
}
return new CustomMultiChildLayout(
delegate: new SectionHeadingsLayout(
headingCount: allSections.length,
maxHeight: maxHeadingsHeight,
selectedIndex: _selectedIndex,
),
children: sectionHeadings,
);
}
Widget _buildSectionDetails(int sectionIndex) {
final List<String> tabs = <String>['LEFT', 'RIGHT'];
return new DefaultTabController(
length: 2,
child: new Column(
children: <Widget>[
new TabBar(
isScrollable: true,
indicatorWeight: 4.0,
indicatorColor: Colors.indigo,
labelColor: Colors.black,
unselectedLabelColor: Colors.grey,
tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
),
new Expanded(
child: new TabBarView(
children: tabs.map((String tab) {
return new CustomScrollView(
slivers: <Widget>[
new SliverFixedExtentList(
itemExtent: 64.0,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new SectionItem(
section: allSections[sectionIndex],
index: index,
);
},
childCount: 16,
),
),
],
);
}).toList(),
),
),
],
),
);
}
@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;
final double midHeightScrollOffset = maxHeadingsHeight - _kMidHeadingsHeight;
return new Scaffold(
body: new Padding(
padding: new EdgeInsets.only(top: statusBarHeight),
child: new NestedScrollView(
controller: _scrollController,
physics: new _SnappingScrollPhysics(midScrollOffset: midHeightScrollOffset),
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
new SliverPersistentHeader(
pinned: true,
delegate: new SliverSectionHeadingsDelegate(
maxHeight: maxHeadingsHeight,
child: new LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final Size size = constraints.biggest;
return (size.height <= _kMidHeadingsHeight)
? _buildRowSectionHeadings(size, maxHeadingsHeight)
: _buildColSectionHeadings(size, maxHeadingsHeight, midHeightScrollOffset);
},
),
),
),
];
},
body: _buildSectionDetails(_selectedIndex),
),
),
);
}
}
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