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