Commit 54d95416 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Added a gallery animation demo (#8547)

parent baaa2e67
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export 'animation_demo.dart';
export 'calculator_demo.dart';
export 'colors_demo.dart';
export 'contacts_demo.dart';
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Based on https://material.uplabs.com/posts/google-newsstand-navigation-pattern
// See also: https://material-motion.github.io/material-motion/documentation/
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'sections.dart';
import 'widgets.dart';
const Color _kAppBackgroundColor = const Color(0xFF353662);
// This app's contents start out at _kHeadingMaxHeight and they function like
// an appbar. Initially the appbar occupies most of the screen and its section
// headings are laid out in a column. By the time its height has been
// reduced to _kAppBarMidHeight, its layout is horizontal, only one section
// heading is visible, and the section's list of details is visible below the
// heading. The appbar's height can be reduced to no more than _kAppBarMinHeight.
const double _kAppBarMinHeight = 90.0;
const double _kAppBarMidHeight = 256.0;
// The AppBar's max height depends on the screen, see _AnimationDemoHomeState._buildBody()
// Initially occupies the same space as the status bar and gets smaller as
// the primary scrollable scrolls upwards.
// TODO(hansmuller): it would be worth adding something like this to the framework.
class _RenderStatusBarPaddingSliver extends RenderSliver {
_RenderStatusBarPaddingSliver({
double maxHeight,
double scrollFactor,
}) : _maxHeight = maxHeight, _scrollFactor = scrollFactor {
assert(maxHeight != null && maxHeight >= 0.0);
assert(scrollFactor != null && scrollFactor >= 1.0);
}
// The height of the status bar
double get maxHeight => _maxHeight;
double _maxHeight;
set maxHeight (double value) {
assert(maxHeight != null && maxHeight >= 0.0);
if (_maxHeight == value)
return;
_maxHeight = value;
markNeedsLayout();
}
// That rate at which this renderer's height shrinks when the scroll
// offset changes.
double get scrollFactor => _scrollFactor;
double _scrollFactor;
set scrollFactor (double value) {
assert(scrollFactor != null && scrollFactor >= 1.0);
if (_scrollFactor == value)
return;
_scrollFactor = value;
markNeedsLayout();
}
@override
void performLayout() {
final double height = (maxHeight - constraints.scrollOffset / scrollFactor).clamp(0.0, maxHeight);
geometry = new SliverGeometry(
paintExtent: math.min(height, constraints.remainingPaintExtent),
scrollExtent: maxHeight,
maxPaintExtent: maxHeight,
);
}
}
class _StatusBarPaddingSliver extends SingleChildRenderObjectWidget {
_StatusBarPaddingSliver({
Key key,
@required this.maxHeight,
this.scrollFactor: 5.0,
}) : super(key: key) {
assert(maxHeight != null && maxHeight >= 0.0);
assert(scrollFactor != null && scrollFactor >= 1.0);
}
final double maxHeight;
final double scrollFactor;
@override
_RenderStatusBarPaddingSliver createRenderObject(BuildContext context) {
return new _RenderStatusBarPaddingSliver(
maxHeight: maxHeight,
scrollFactor: scrollFactor,
);
}
@override
void updateRenderObject(BuildContext context, _RenderStatusBarPaddingSliver renderObject) {
renderObject
..maxHeight = maxHeight
..scrollFactor = scrollFactor;
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('maxHeight: $maxHeight');
description.add('scrollFactor: $scrollFactor');
}
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
@required this.minHeight,
@required this.maxHeight,
@required this.child,
});
final double minHeight;
final double maxHeight;
final Widget child;
@override double get minExtent => minHeight;
@override double get maxExtent => math.max(maxHeight, minHeight);
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return new SizedBox.expand(child: child);
}
@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight
|| minHeight != oldDelegate.minHeight
|| child != oldDelegate.child;
}
@override
String toString() => '_SliverAppBarDelegate';
}
// Arrange the section titles, indicators, and cards. The cards are only included when
// the layout is transitioning between vertical and horizontal. Once the layout is
// horizontal the cards are laid out by a PageView.
//
// The layout of the section cards, titles, and indicators is defined by the
// two 0.0-1.0 "t" parameters, both of which are based on the layout's height:
// - tColumnToRow
// 0.0 when height is maxHeight and the layout is a column
// 1.0 when the height is midHeight and the layout is a row
// - tCollapsed
// 0.0 when height is midHeight and the layout is a row
// 1.0 when height is minHeight and the layout is a (still) row
//
// minHeight < midHeight < maxHeight
//
// The general approach here is to compute the column layout and row size
// and position of each element and then interpolate between them using
// tColumnToRow. Once tColumnToRow reaches 1.0, the layout changes are
// defined by tCollapsed. As tCollapsed increases the titles spread out
// until only one title is visible and the indicators cluster together
// until they're all visible.
class _AllSectionsLayout extends MultiChildLayoutDelegate {
_AllSectionsLayout({
this.tColumnToRow,
this.tCollapsed,
this.cardCount,
this.selectedIndex,
});
final double tColumnToRow;
final double tCollapsed;
final int cardCount;
final double selectedIndex;
Rect _interpolateRect(Rect begin, Rect end) {
return Rect.lerp(begin, end, tColumnToRow);
}
Point _interpolatePoint(Point begin, Point end) {
return Point.lerp(begin, end, tColumnToRow);
}
@override
void performLayout(Size size) {
final double columnCardX = size.width / 5.0;
final double columnCardWidth = size.width - columnCardX;
final double columnCardHeight = size.height / cardCount;
final double rowCardWidth = size.width;
double columnCardY = 0.0;
double rowCardX = -(selectedIndex * rowCardWidth);
// When tCollapsed > 0 the titles spread apart
final double columnTitleX = size.width / 10.0;
final double rowTitleWidth = size.width * ((1 + tCollapsed) / 2.25);
double rowTitleX = (size.width - rowTitleWidth) / 2.0 - selectedIndex * rowTitleWidth;
// When tCollapsed > 0, the indicators move closer together
//final double rowIndicatorWidth = 48.0 + (1.0 - tCollapsed) * (rowTitleWidth - 48.0);
final double paddedSectionIndicatorWidth = kSectionIndicatorWidth + 8.0;
final double rowIndicatorWidth = paddedSectionIndicatorWidth +
(1.0 - tCollapsed) * (rowTitleWidth - paddedSectionIndicatorWidth);
double rowIndicatorX = (size.width - rowIndicatorWidth) / 2.0 - selectedIndex * rowIndicatorWidth;
// Compute the size and origin of each card, title, and indicator for the maxHeight
// "column" layout, and the midHeight "row" layout. The actual layout is just the
// interpolated value between the column and row layouts for t.
for (int index = 0; index < cardCount; index++) {
// Layout the card for index.
final Rect columnCardRect = new Rect.fromLTWH(columnCardX, columnCardY, columnCardWidth, columnCardHeight);
final Rect rowCardRect = new Rect.fromLTWH(rowCardX, 0.0, rowCardWidth, size.height);
final Rect cardRect = _interpolateRect(columnCardRect, rowCardRect);
final String cardId = 'card$index';
if (hasChild(cardId)) {
// Add a small horizontal gap between the cards.
final Rect insetRect = new Rect.fromLTWH(
cardRect.left + 0.5, cardRect.top, cardRect.width - 1.0, cardRect.height
);
layoutChild(cardId, new BoxConstraints.tight(insetRect.size));
positionChild(cardId, insetRect.topLeft.toOffset());
}
// Layout the title for index.
final Size titleSize = layoutChild('title$index', new BoxConstraints.loose(cardRect.size));
final double columnTitleY = columnCardRect.centerLeft.y - titleSize.height / 2.0;
final double rowTitleY = rowCardRect.centerLeft.y - titleSize.height / 2.0;
final double centeredRowTitleX = rowTitleX + (rowTitleWidth - titleSize.width) / 2.0;
final Point columnTitleOrigin = new Point(columnTitleX, columnTitleY);
final Point rowTitleOrigin = new Point(centeredRowTitleX, rowTitleY);
final Point titleOrigin = _interpolatePoint(columnTitleOrigin, rowTitleOrigin);
positionChild('title$index', titleOrigin.toOffset());
// Layout the selection indicator for index.
final Size indicatorSize = layoutChild('indicator$index', new BoxConstraints.loose(cardRect.size));
final double columnIndicatorX = cardRect.centerRight.x - indicatorSize.width - 16.0;
final double columnIndicatorY = cardRect.bottomRight.y - indicatorSize.height - 16.0;
final Point columnIndicatorOrigin = new Point(columnIndicatorX, columnIndicatorY);
final Rect titleRect = new Rect.fromPoints(titleOrigin, titleSize.bottomRight(titleOrigin));
final double centeredRowIndicatorX = rowIndicatorX + (rowIndicatorWidth - indicatorSize.width) / 2.0;
final double rowIndicatorY = titleRect.bottomCenter.y + 16.0;
final Point rowIndicatorOrigin = new Point(centeredRowIndicatorX, rowIndicatorY);
final Point indicatorOrigin = _interpolatePoint(columnIndicatorOrigin, rowIndicatorOrigin);
positionChild('indicator$index', indicatorOrigin.toOffset());
columnCardY += columnCardHeight;
rowCardX += rowCardWidth;
rowTitleX += rowTitleWidth;
rowIndicatorX += rowIndicatorWidth;
}
}
@override
bool shouldRelayout(_AllSectionsLayout oldDelegate) {
return tColumnToRow != oldDelegate.tColumnToRow
|| cardCount != oldDelegate.cardCount
|| selectedIndex != oldDelegate.selectedIndex;
}
}
class _AllSectionsView extends StatelessWidget {
_AllSectionsView({
Key key,
this.sections,
this.selectedIndex,
this.minHeight,
this.midHeight,
this.maxHeight,
this.sectionCards: const <Widget>[],
}) : super(key: key) {
assert(sections != null);
assert(sectionCards != null);
assert(sectionCards.length == sections.length);
assert(selectedIndex >= 0.0 && selectedIndex < sections.length.toDouble());
}
final List<Section> sections;
final double selectedIndex;
final double minHeight;
final double midHeight;
final double maxHeight;
final List<Widget> sectionCards;
double _selectedIndexDelta(int index) {
return (index.toDouble() - selectedIndex).abs().clamp(0.0, 1.0);
}
Widget _build(BuildContext context, BoxConstraints constraints) {
final Size size = constraints.biggest;
// The layout's progress from from a column to a row. Its value is
// 0.0 when size.height equals the maxHeight, 1.0 when the size.height
// equals the midHeight.
// The layout's progress from from a column to a row. Its value is
// 0.0 when size.height equals the maxHeight, 1.0 when the size.height
// equals the midHeight.
final double tColumnToRow =
1.0 - ((size.height - midHeight) /
(maxHeight - midHeight)).clamp(0.0, 1.0);
// The layout's progress from from the midHeight row layout to
// a minHeight row layout. Its value is 0.0 when size.height equals
// midHeight and 1.0 when size.height equals minHeight.
final double tCollapsed =
1.0 - ((size.height - minHeight) /
(midHeight - minHeight)).clamp(0.0, 1.0);
double _indicatorOpacity(int index) {
return 1.0 - _selectedIndexDelta(index) * tColumnToRow * 0.5;
}
double _titleOpacity(int index) {
return 1.0 - _selectedIndexDelta(index) * tColumnToRow * 0.5;
}
double _titleScale(int index) {
return 1.0 - _selectedIndexDelta(index) * tColumnToRow * 0.15;
}
final List<Widget> children = new List<Widget>.from(sectionCards);
for (int index = 0; index < sections.length; index++) {
final Section section = sections[index];
children.add(new LayoutId(
id: 'title$index',
child: new SectionTitle(
section: section,
scale: _titleScale(index),
opacity: _titleOpacity(index),
),
));
}
for (int index = 0; index < sections.length; index++) {
children.add(new LayoutId(
id: 'indicator$index',
child: new SectionIndicator(
opacity: _indicatorOpacity(index),
),
));
}
return new CustomMultiChildLayout(
delegate: new _AllSectionsLayout(
tColumnToRow: tColumnToRow,
tCollapsed: tCollapsed,
cardCount: sections.length,
selectedIndex: selectedIndex,
),
children: children,
);
}
@override
Widget build(BuildContext context) {
return new LayoutBuilder(builder: _build);
}
}
class AnimationDemoHome extends StatefulWidget {
AnimationDemoHome({ Key key }) : super(key: key);
static const String routeName = '/animation';
@override
_AnimationDemoHomeState createState() => new _AnimationDemoHomeState();
}
class _AnimationDemoHomeState extends State<AnimationDemoHome> {
final ScrollController _scrollController = new ScrollController();
final PageController _headingPageController = new PageController();
final PageController _detailsPageController = new PageController();
double _selectedIndex = 0.0;
@override
Widget build(BuildContext context) {
return new Scaffold(
backgroundColor: _kAppBackgroundColor,
body: new Builder(
// Insert an element so that _buildBody can find the PrimaryScrollController.
builder: (BuildContext context) => _buildBody(context),
),
);
}
void _maybeScroll(double midScrollOffset, int pageIndex, double xOffset) {
const Duration duration = const Duration(milliseconds: 400);
const Curve curve = Curves.fastOutSlowIn;
if (_scrollController.offset < midScrollOffset) {
// Scroll the overall list to the point where only one section card shows.
// At the same time scroll the PageViews to the page at pageIndex.
_headingPageController.animateToPage(pageIndex, curve: curve, duration: duration);
_scrollController.animateTo(midScrollOffset, curve: curve, duration: duration);
} else {
// One one section card is showing: scroll one page forward or back.
final double centerX = _headingPageController.position.viewportDimension / 2.0;
final int newPageIndex = xOffset > centerX ? pageIndex + 1 : pageIndex - 1;
_headingPageController.animateToPage(newPageIndex, curve: curve, duration: duration);
}
}
bool _handlePageNotification(ScrollNotification notification, PageController leader, PageController follower) {
if (notification.depth == 0 && notification is ScrollUpdateNotification) {
setState(() {
_selectedIndex = leader.page;
});
if (follower.page != leader.page)
follower.position.jumpTo(leader.position.pixels, settle: false);
}
return false;
}
Iterable<Widget> _detailItemsFor(Section section) {
final Iterable<Widget> detailItems = section.details.map((SectionDetail detail) {
return new SectionDetailView(detail: detail);
});
return ListItem.divideItems(context: context, items: detailItems).map((Widget item) {
return new SliverToBoxAdapter(child: item);
});
}
Iterable<Widget> _allHeadingItems(double maxHeight, double midScrollOffset) {
final List<Widget> sectionCards = <Widget>[];
for (int index = 0; index < allSections.length; index++) {
sectionCards.add(new LayoutId(
id: 'card$index',
child: new GestureDetector(
behavior: HitTestBehavior.opaque,
child: new SectionCard(section: allSections[index]),
onTapUp: (TapUpDetails details) {
final double xOffset = details.globalPosition.x;
setState(() {
_maybeScroll(midScrollOffset, index, xOffset);
});
}
),
));
}
final List<Widget> headings = <Widget>[];
for (int index = 0; index < allSections.length; index++) {
headings.add(new Container(
decoration: new BoxDecoration(backgroundColor: _kAppBackgroundColor),
child: new FractionalTranslation(
translation: new FractionalOffset(_selectedIndex - index, 0.0),
child: new ClipRect(
child: new _AllSectionsView(
sections: allSections,
selectedIndex: _selectedIndex,
minHeight: _kAppBarMinHeight,
midHeight: _kAppBarMidHeight,
maxHeight: maxHeight,
sectionCards: sectionCards,
),
),
),
)
);
}
return headings;
}
Widget _buildBody(BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context);
final double statusBarHeight = mediaQueryData.padding.top;
final double screenHeight = mediaQueryData.size.height;
final double appBarMaxHeight = screenHeight - statusBarHeight;
// The scrolloffset that reveals the appBarMidHeight appbar.
final double appBarMidScrollOffset = statusBarHeight + appBarMaxHeight - _kAppBarMidHeight;
return new SizedBox.expand(
child: new Stack(
children: <Widget>[
new CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
// Start out below the status bar, gradually move to the top of the screen.
new _StatusBarPaddingSliver(
maxHeight: statusBarHeight,
scrollFactor: 7.0,
),
// Section Headings
new SliverPersistentHeader(
pinned: true,
delegate: new _SliverAppBarDelegate(
minHeight: _kAppBarMinHeight,
maxHeight: appBarMaxHeight,
child: new NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
return _handlePageNotification(notification, _headingPageController, _detailsPageController);
},
child: new PageView(
controller: _headingPageController,
children: _allHeadingItems(appBarMaxHeight, appBarMidScrollOffset),
),
),
),
),
// Details
new SliverToBoxAdapter(
child: new SizedBox(
height: 610.0,
child: new NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
return _handlePageNotification(notification, _detailsPageController, _headingPageController);
},
child: new PageView(
controller: _detailsPageController,
children: allSections.map((Section section) {
return new CustomScrollView(
slivers: _detailItemsFor(section).toList(),
);
}).toList(),
),
),
),
),
],
),
new Positioned(
top: statusBarHeight,
left: 0.0,
child: new IconTheme(
data: new IconThemeData(color: Colors.white),
child: new BackButton(),
),
),
],
),
);
}
}
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Raw data for the animation demo.
import 'package:flutter/material.dart';
const Color _mariner = const Color(0xFF3B5F8F);
const Color _mediumPurple = const Color(0xFF8266D4);
const Color _tomato = const Color(0xFFF95B57);
const Color _mySin = const Color(0xFFF3A646);
const Color _deepCerise = const Color(0xFFD93F9B);
class SectionDetail {
const SectionDetail({ this.title, this.subtitle, this.imageAsset });
final String title;
final String subtitle;
final String imageAsset;
}
class Section {
const Section({ this.title, this.backgroundAsset, this.leftColor, this.rightColor, this.details });
final String title;
final String backgroundAsset;
final Color leftColor;
final Color rightColor;
final List<SectionDetail> details;
@override
bool operator==(Object other) {
if (other is! Section)
return false;
final Section otherSection = other;
return title == otherSection.title;
}
@override
int get hashCode => title.hashCode;
}
// TODO(hansmuller): replace the SectionDetail images and text. Get rid of
// the const vars like _eyeglassesDetail and insert a variety of titles and
// image SectionDetails in the allSections list.
const SectionDetail _eyeglassesDetail = const SectionDetail(
imageAsset: 'packages/flutter_gallery_assets/shrine/products/sunnies.png',
title: 'Flutter enables interactive animation',
subtitle: '3K views - 5 days',
);
const SectionDetail _eyeglassesImageDetail = const SectionDetail(
imageAsset: 'packages/flutter_gallery_assets/shrine/products/sunnies.png',
);
const SectionDetail _seatingDetail = const SectionDetail(
imageAsset: 'packages/flutter_gallery_assets/shrine/products/lawn_chair.png',
title: 'Flutter enables interactive animation',
subtitle: '3K views - 5 days',
);
const SectionDetail _seatingImageDetail = const SectionDetail(
imageAsset: 'packages/flutter_gallery_assets/shrine/products/lawn_chair.png',
);
const SectionDetail _decorationDetail = const SectionDetail(
imageAsset: 'packages/flutter_gallery_assets/shrine/products/lipstick.png',
title: 'Flutter enables interactive animation',
subtitle: '3K views - 5 days',
);
const SectionDetail _decorationImageDetail = const SectionDetail(
imageAsset: 'packages/flutter_gallery_assets/shrine/products/lipstick.png',
);
const SectionDetail _protectionDetail = const SectionDetail(
imageAsset: 'packages/flutter_gallery_assets/shrine/products/helmet.png',
title: 'Flutter enables interactive animation',
subtitle: '3K views - 5 days',
);
const SectionDetail _protectionImageDetail = const SectionDetail(
imageAsset: 'packages/flutter_gallery_assets/shrine/products/helmet.png',
);
final List<Section> allSections = <Section>[
const Section(
title: 'EYEGLASSES',
leftColor: _mediumPurple,
rightColor: _mariner,
backgroundAsset: 'packages/flutter_gallery_assets/shrine/products/sunnies.png',
details: const <SectionDetail>[
_eyeglassesDetail,
_eyeglassesImageDetail,
_eyeglassesDetail,
_eyeglassesDetail,
_eyeglassesDetail,
_eyeglassesDetail,
],
),
const Section(
title: 'SEATING',
leftColor: _tomato,
rightColor: _mediumPurple,
backgroundAsset: 'packages/flutter_gallery_assets/shrine/products/lawn_chair.png',
details: const <SectionDetail>[
_seatingDetail,
_seatingImageDetail,
_seatingDetail,
_seatingDetail,
_seatingDetail,
_seatingDetail,
],
),
const Section(
title: 'DECORATION',
leftColor: _mySin,
rightColor: _tomato,
backgroundAsset: 'packages/flutter_gallery_assets/shrine/products/lipstick.png',
details: const <SectionDetail>[
_decorationDetail,
_decorationImageDetail,
_decorationDetail,
_decorationDetail,
_decorationDetail,
_decorationDetail,
],
),
const Section(
title: 'PROTECTION',
leftColor: Colors.white,
rightColor: _tomato,
backgroundAsset: 'packages/flutter_gallery_assets/shrine/products/helmet.png',
details: const <SectionDetail>[
_protectionDetail,
_protectionImageDetail,
_protectionDetail,
_protectionDetail,
_protectionDetail,
_protectionDetail,
],
),
];
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'sections.dart';
const double kSectionIndicatorWidth = 32.0;
// The card for a single section. Displays the section's gradient and background image.
class SectionCard extends StatelessWidget {
SectionCard({ Key key, this.section }) : super(key: key) {
assert(section != null);
}
final Section section;
@override
Widget build(BuildContext context) {
return new Padding(
padding: const EdgeInsets.only(bottom: 1.0),
child: new DecoratedBox(
decoration: new BoxDecoration(
borderRadius: new BorderRadius.circular(4.0),
gradient: new LinearGradient(
begin: FractionalOffset.topLeft,
end: FractionalOffset.topRight,
colors: <Color>[
section.leftColor,
section.rightColor,
],
),
),
child: new Opacity(
opacity: 0.075,
child: new Image.asset(
section.backgroundAsset,
fit: ImageFit.cover,
),
),
),
);
}
}
// The title is rendered with two overlapping text widgets that are vertically
// offset a little. It's supposed to look sort-of 3D.
class SectionTitle extends StatelessWidget {
static const TextStyle sectionTitleStyle = const TextStyle(
fontFamily: 'Raleway',
inherit: false,
fontSize: 24.0,
fontWeight: FontWeight.w500,
color: Colors.white,
textBaseline: TextBaseline.alphabetic,
);
static final TextStyle sectionTitleShadowStyle = sectionTitleStyle.copyWith(
color: const Color(0x19000000),
);
SectionTitle({ Key key, this.section, this.scale, this.opacity }) : super(key: key) {
assert(section != null);
assert(scale != null);
assert(opacity != null && opacity >= 0.0 && opacity <= 1.0);
}
final Section section;
final double scale;
final double opacity;
@override
Widget build(BuildContext context) {
return new IgnorePointer(
child: new Opacity(
opacity: opacity,
child: new Transform(
transform: new Matrix4.identity()..scale(scale),
alignment: FractionalOffset.center,
child: new Stack(
children: <Widget>[
new Positioned(
top: 4.0,
child: new Text(section.title, style: sectionTitleShadowStyle),
),
new Text(section.title, style: sectionTitleStyle),
],
),
),
),
);
}
}
// Small horizontal bar that indicates the selected section.
class SectionIndicator extends StatelessWidget {
SectionIndicator({ Key key, this.opacity: 1.0 }) : super(key: key);
final double opacity;
@override
Widget build(BuildContext context) {
return new IgnorePointer(
child: new Container(
width: kSectionIndicatorWidth,
height: 3.0,
decoration: new BoxDecoration(
backgroundColor: Colors.white.withOpacity(opacity),
),
),
);
}
}
// Display a single SectionDetail.
class SectionDetailView extends StatelessWidget {
SectionDetailView({ Key key, this.detail }) : super(key: key) {
assert(detail != null && detail.imageAsset != null);
assert((detail.imageAsset ?? detail.title) != null);
}
final SectionDetail detail;
@override
Widget build(BuildContext context) {
final Widget image = new DecoratedBox(
decoration: new BoxDecoration(
borderRadius: new BorderRadius.circular(6.0),
backgroundImage: new BackgroundImage(
image: new AssetImage(detail.imageAsset),
fit: ImageFit.cover,
alignment: FractionalOffset.center,
),
),
);
Widget item;
if (detail.title == null && detail.subtitle == null) {
item = new Container(
height: 240.0,
padding: const EdgeInsets.all(16.0),
child: image,
);
} else {
item = new ListItem(
title: new Text(detail.title),
subtitle: new Text(detail.subtitle),
leading: new SizedBox(width: 32.0, height: 32.0, child: image),
);
}
return new DecoratedBox(
decoration: new BoxDecoration(backgroundColor: Colors.grey[200]),
child: item,
);
}
}
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'animation/home.dart';
class AnimationDemo extends StatelessWidget {
AnimationDemo({Key key}) : super(key: key);
static const String routeName = '/animation';
@override
Widget build(BuildContext context) => new AnimationDemoHome();
}
......@@ -67,6 +67,13 @@ final List<GalleryItem> kAllGalleryItems = <GalleryItem>[
routeName: ContactsDemo.routeName,
buildRoute: (BuildContext context) => new ContactsDemo(),
),
new GalleryItem(
title: 'Animation',
subtitle: 'Section organizer',
category: 'Demos',
routeName: AnimationDemo.routeName,
buildRoute: (BuildContext context) => new AnimationDemo(),
),
// Material Components
new GalleryItem(
title: 'Bottom navigation',
......
......@@ -18,6 +18,7 @@ final List<String> demoTitles = <String>[
'Pesto',
'Shrine',
'Contact profile',
'Animation',
// Material Components
'Bottom navigation',
'Buttons',
......
......@@ -215,9 +215,9 @@ class ScrollPosition extends ViewportOffset {
/// scroll notifications will be dispatched. No overscroll notifications can
/// be generated by this method.
///
/// Immediately after the jump, a ballistic activity is started, in case the
/// value was out of range.
void jumpTo(double value) {
/// If settle is true then, immediately after the jump, a ballistic activity
/// is started, in case the value was out of range.
void jumpTo(double value, { bool settle: true }) {
beginIdleActivity();
if (_pixels != value) {
final double oldPixels = _pixels;
......@@ -227,6 +227,7 @@ class ScrollPosition extends ViewportOffset {
state.dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels));
state.dispatchNotification(activity.createScrollEndNotification(state));
}
if (settle)
beginBallisticActivity(0.0);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment