mirror of https://github.com/flutter/samples.git
add carousel, card_swipe, and focus_image samples (#119)
* add carousel, card_swipe, and focus_image samples * fix image assets * fix more asset images * add repeating animation * fix import * add copyright headers * remove Center widget * imageAssetName * use ClipRect, refactor _SwipeableCardState * use offset.zero * add comments * remove reference to coverflow package * change spread to toList() * refactor coverflow sample don't set width and height use const data -> images * return widget directly, fix formatting * inline transitionsBuilder * image -> imageAssetName * _rectTween() => _createTween() * _expandToPageRoute -> _createRoute * move non-updating widgets out of AnimatedBuilder * code review updates to animations demospull/120/head
parent
8e4d8c138b
commit
4966440a29
@ -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<CardSwipeDemo> {
|
||||
List<String> 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: <Widget>[
|
||||
Expanded(
|
||||
child: ClipRect(
|
||||
child: Stack(
|
||||
overflow: Overflow.clip,
|
||||
children: <Widget>[
|
||||
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<SwipeableCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AnimationController _controller;
|
||||
Animation<Offset> _animation;
|
||||
double _dragStartX;
|
||||
bool _isSwipingLeft = false;
|
||||
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController.unbounded(vsync: this);
|
||||
_animation = _controller.drive(Tween<Offset>(
|
||||
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<Offset>(
|
||||
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();
|
||||
}
|
||||
}
|
@ -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<String> fileNames = [
|
||||
'assets/eat_cape_town_sm.jpg',
|
||||
'assets/eat_new_orleans_sm.jpg',
|
||||
'assets/eat_sydney_sm.jpg',
|
||||
];
|
||||
|
||||
final List<Widget> 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<Carousel> {
|
||||
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();
|
||||
}
|
||||
}
|
@ -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<RelativeRect> _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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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<RepeatingAnimationDemo>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AnimationController _controller;
|
||||
Animation<BorderRadius> _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();
|
||||
}
|
||||
}
|
Loading…
Reference in new issue