diff --git a/animations/lib/main.dart b/animations/lib/main.dart index ec3d29862..f02d459db 100644 --- a/animations/lib/main.dart +++ b/animations/lib/main.dart @@ -3,8 +3,13 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; + import 'src/basics/animation_controller_demo.dart'; +import 'src/misc/card_swipe.dart'; +import 'src/misc/carousel.dart'; import 'src/misc/expand_card.dart'; +import 'src/misc/focus_image.dart'; +import 'src/misc/repeating_animation.dart'; void main() => runApp(AnimationSamples()); @@ -24,6 +29,11 @@ final basicDemos = [ final miscDemos = [ Demo('Expandable Card', ExpandCardDemo.routeName, (context) => ExpandCardDemo()), + Demo('Carousel', CarouselDemo.routeName, (context) => CarouselDemo()), + Demo('Focus Image', FocusImageDemo.routeName, (context) => FocusImageDemo()), + Demo('Card Swipe', CardSwipeDemo.routeName, (context) => CardSwipeDemo()), + Demo('Repeating Animation', RepeatingAnimationDemo.routeName, + (context) => RepeatingAnimationDemo()), ]; final basicDemoRoutes = diff --git a/animations/lib/src/misc/card_swipe.dart b/animations/lib/src/misc/card_swipe.dart new file mode 100644 index 000000000..df3dda752 --- /dev/null +++ b/animations/lib/src/misc/card_swipe.dart @@ -0,0 +1,188 @@ +// Copyright 2019 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/physics.dart'; + +class CardSwipeDemo extends StatefulWidget { + static String routeName = '/misc/card_swipe'; + + @override + _CardSwipeDemoState createState() => _CardSwipeDemoState(); +} + +class _CardSwipeDemoState extends State { + List fileNames; + + void initState() { + super.initState(); + _resetCards(); + } + + void _resetCards() { + fileNames = [ + 'assets/eat_cape_town_sm.jpg', + 'assets/eat_new_orleans_sm.jpg', + 'assets/eat_sydney_sm.jpg', + ]; + } + + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Card Swipe'), + ), + body: Padding( + padding: const EdgeInsets.all(12.0), + child: Center( + child: Column( + children: [ + Expanded( + child: ClipRect( + child: Stack( + overflow: Overflow.clip, + children: [ + for (final fileName in fileNames) + SwipeableCard( + imageAssetName: fileName, + onSwiped: () { + setState(() { + fileNames.remove(fileName); + }); + }, + ), + ], + ), + ), + ), + RaisedButton( + child: const Text('Refill'), + onPressed: () { + setState(() { + _resetCards(); + }); + }, + ), + ], + ), + ), + ), + ); + } +} + +class Card extends StatelessWidget { + final String imageAssetName; + + Card(this.imageAssetName); + + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 3 / 5, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.0), + image: DecorationImage( + image: AssetImage(imageAssetName), + fit: BoxFit.cover, + ), + ), + ), + ); + } +} + +class SwipeableCard extends StatefulWidget { + final String imageAssetName; + final VoidCallback onSwiped; + + SwipeableCard({ + this.onSwiped, + this.imageAssetName, + }); + + _SwipeableCardState createState() => _SwipeableCardState(); +} + +class _SwipeableCardState extends State + with SingleTickerProviderStateMixin { + AnimationController _controller; + Animation _animation; + double _dragStartX; + bool _isSwipingLeft = false; + + void initState() { + super.initState(); + _controller = AnimationController.unbounded(vsync: this); + _animation = _controller.drive(Tween( + begin: Offset.zero, + end: Offset(1, 0), + )); + } + + Widget build(BuildContext context) { + return SlideTransition( + position: _animation, + child: GestureDetector( + onHorizontalDragStart: _dragStart, + onHorizontalDragUpdate: _dragUpdate, + onHorizontalDragEnd: _dragEnd, + child: Card(widget.imageAssetName), + ), + ); + } + + /// Sets the starting position the user dragged from. + void _dragStart(DragStartDetails details) { + _dragStartX = details.localPosition.dx; + } + + /// Changes the animation to animate to the left or right depending on the + /// swipe, and sets the AnimationController's value to the swiped amount. + void _dragUpdate(DragUpdateDetails details) { + var isSwipingLeft = (details.localPosition.dx - _dragStartX) < 0; + if (isSwipingLeft != _isSwipingLeft) { + _isSwipingLeft = isSwipingLeft; + _updateAnimation(details.localPosition.dx); + } + + setState(() { + // Calculate the amount dragged in unit coordinates (between 0 and 1) + // using this widgets width. + _controller.value = + (details.localPosition.dx - _dragStartX).abs() / context.size.width; + }); + } + + /// Runs the fling / spring animation using the final velocity of the drag + /// gesture. + void _dragEnd(DragEndDetails details) { + var velocity = + (details.velocity.pixelsPerSecond.dx / context.size.width).abs(); + _animate(velocity: velocity); + } + + void _updateAnimation(double dragPosition) { + _animation = _controller.drive(Tween( + begin: Offset.zero, + end: _isSwipingLeft ? Offset(-1, 0) : Offset(1, 0), + )); + } + + void _animate({double velocity = 0}) { + var description = SpringDescription(mass: 50, stiffness: 1, damping: 1); + var simulation = + SpringSimulation(description, _controller.value, 1, velocity); + _controller.animateWith(simulation).then((_) { + widget.onSwiped(); + }); + } + + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/animations/lib/src/misc/carousel.dart b/animations/lib/src/misc/carousel.dart new file mode 100644 index 000000000..a4f167495 --- /dev/null +++ b/animations/lib/src/misc/carousel.dart @@ -0,0 +1,108 @@ +// Copyright 2019 The Flutter team. 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 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +class CarouselDemo extends StatelessWidget { + static String routeName = '/misc/carousel'; + + static const List fileNames = [ + 'assets/eat_cape_town_sm.jpg', + 'assets/eat_new_orleans_sm.jpg', + 'assets/eat_sydney_sm.jpg', + ]; + + final List images = + fileNames.map((file) => Image.asset(file, fit: BoxFit.cover)).toList(); + + @override + Widget build(context) { + return Scaffold( + appBar: AppBar( + title: Text('Carousel Demo'), + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: AspectRatio( + aspectRatio: 1, + child: Carousel(itemBuilder: widgetBuilder), + ), + ), + ), + ); + } + + Widget widgetBuilder(context, int index) { + return images[index % images.length]; + } +} + +typedef void OnCurrentItemChangedCallback(int currentItem); + +class Carousel extends StatefulWidget { + final IndexedWidgetBuilder itemBuilder; + + const Carousel({Key key, @required this.itemBuilder}); + + @override + _CarouselState createState() => _CarouselState(); +} + +class _CarouselState extends State { + PageController _controller; + int _currentPage; + bool _pageHasChanged = false; + + @override + void initState() { + super.initState(); + _currentPage = 0; + _controller = PageController( + viewportFraction: .85, + initialPage: _currentPage, + ); + } + + @override + Widget build(context) { + var size = MediaQuery.of(context).size; + return PageView.builder( + onPageChanged: (value) { + setState(() { + _pageHasChanged = true; + _currentPage = value; + }); + }, + controller: _controller, + itemBuilder: (context, index) => AnimatedBuilder( + animation: _controller, + builder: (context, child) { + var result = _pageHasChanged ? _controller.page : _currentPage * 1.0; + + // The horizontal position of the page between a 1 and 0 + var value = result - index; + value = (1 - (value.abs() * .5)).clamp(0.0, 1.0) as double; + + return Center( + child: SizedBox( + height: Curves.easeOut.transform(value) * size.height, + width: Curves.easeOut.transform(value) * size.width, + child: child, + ), + ); + }, + child: widget.itemBuilder(context, index), + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/animations/lib/src/misc/expand_card.dart b/animations/lib/src/misc/expand_card.dart index 4dbc14a8f..c9c19bb03 100644 --- a/animations/lib/src/misc/expand_card.dart +++ b/animations/lib/src/misc/expand_card.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; class ExpandCardDemo extends StatelessWidget { - static const String routeName = '/expand_card'; + static const String routeName = '/misc/expand_card'; @override Widget build(BuildContext context) { diff --git a/animations/lib/src/misc/focus_image.dart b/animations/lib/src/misc/focus_image.dart new file mode 100644 index 000000000..0d1cddb4c --- /dev/null +++ b/animations/lib/src/misc/focus_image.dart @@ -0,0 +1,113 @@ +// Copyright 2019 The Flutter team. 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'; + +class FocusImageDemo extends StatelessWidget { + static String routeName = '/misc/focus_image'; + + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Focus Image')), + body: Grid(), + ); + } +} + +class Grid extends StatelessWidget { + Widget build(BuildContext context) { + return Scaffold( + body: GridView.builder( + itemCount: 40, + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4), + itemBuilder: (context, index) { + return (index >= 20) + ? SmallCard('assets/eat_cape_town_sm.jpg') + : SmallCard('assets/eat_new_orleans_sm.jpg'); + }, + ), + ); + } +} + +Route _createRoute(BuildContext parentContext, String image) { + return PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) { + return _SecondPage(image); + }, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + var rectAnimation = _createTween(parentContext) + .chain(CurveTween(curve: Curves.ease)) + .animate(animation); + + return Stack( + children: [ + PositionedTransition(rect: rectAnimation, child: child), + ], + ); + }, + ); +} + +Tween _createTween(BuildContext context) { + var windowSize = MediaQuery.of(context).size; + var box = context.findRenderObject() as RenderBox; + var rect = box.localToGlobal(Offset.zero) & box.size; + var relativeRect = RelativeRect.fromSize(rect, windowSize); + + return RelativeRectTween( + begin: relativeRect, + end: RelativeRect.fill, + ); +} + +class SmallCard extends StatelessWidget { + final String imageAssetName; + + SmallCard(this.imageAssetName); + + Widget build(BuildContext context) { + return Card( + child: Material( + child: InkWell( + onTap: () { + var nav = Navigator.of(context); + nav.push(_createRoute(context, imageAssetName)); + }, + child: Image.asset( + imageAssetName, + fit: BoxFit.cover, + ), + ), + ), + ); + } +} + +class _SecondPage extends StatelessWidget { + final String imageAssetName; + + _SecondPage(this.imageAssetName); + + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Center( + child: Material( + child: InkWell( + onTap: () => Navigator.of(context).pop(), + child: AspectRatio( + aspectRatio: 1, + child: Image.asset( + imageAssetName, + fit: BoxFit.cover, + ), + ), + ), + ), + ), + ); + } +} diff --git a/animations/lib/src/misc/repeating_animation.dart b/animations/lib/src/misc/repeating_animation.dart new file mode 100644 index 000000000..f27fd73bb --- /dev/null +++ b/animations/lib/src/misc/repeating_animation.dart @@ -0,0 +1,60 @@ +// Copyright 2019 The Flutter team. 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'; + +class RepeatingAnimationDemo extends StatefulWidget { + static String routeName = '/misc/repeating_animation'; + + @override + RepeatingAnimationDemoState createState() => RepeatingAnimationDemoState(); +} + +class RepeatingAnimationDemoState extends State + with SingleTickerProviderStateMixin { + AnimationController _controller; + Animation _borderRadius; + + @override + void initState() { + super.initState(); + + _controller = + AnimationController(duration: const Duration(seconds: 2), vsync: this) + ..repeat(reverse: true); + + _borderRadius = BorderRadiusTween( + begin: BorderRadius.circular(100.0), + end: BorderRadius.circular(0.0), + ).animate(_controller); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Repeating Animation')), + body: Center( + child: AnimatedBuilder( + animation: _borderRadius, + builder: (context, child) { + return Container( + width: 200, + height: 200, + decoration: BoxDecoration( + color: Colors.deepPurple, + borderRadius: _borderRadius.value, + ), + ); + }, + ), + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +}