mirror of
https://github.com/flutter/samples.git
synced 2026-03-29 07:41:41 +00:00
204 lines
5.2 KiB
Dart
204 lines
5.2 KiB
Dart
// 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/physics.dart';
|
|
|
|
class CardSwipeDemo extends StatefulWidget {
|
|
const CardSwipeDemo({super.key});
|
|
static String routeName = '/misc/card_swipe';
|
|
|
|
@override
|
|
State<CardSwipeDemo> createState() => _CardSwipeDemoState();
|
|
}
|
|
|
|
class _CardSwipeDemoState extends State<CardSwipeDemo> {
|
|
late List<String> fileNames;
|
|
|
|
@override
|
|
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',
|
|
];
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Card Swipe'),
|
|
),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Center(
|
|
child: Column(
|
|
children: [
|
|
Expanded(
|
|
child: ClipRect(
|
|
child: Stack(
|
|
children: [
|
|
for (final fileName in fileNames)
|
|
SwipeableCard(
|
|
imageAssetName: fileName,
|
|
onSwiped: () {
|
|
setState(() {
|
|
fileNames.remove(fileName);
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
ElevatedButton(
|
|
child: const Text('Refill'),
|
|
onPressed: () {
|
|
setState(() {
|
|
_resetCards();
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class Card extends StatelessWidget {
|
|
final String imageAssetName;
|
|
|
|
const Card({required this.imageAssetName, super.key});
|
|
|
|
@override
|
|
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;
|
|
|
|
const SwipeableCard(
|
|
{required this.onSwiped, required this.imageAssetName, super.key});
|
|
|
|
@override
|
|
State<SwipeableCard> createState() => _SwipeableCardState();
|
|
}
|
|
|
|
class _SwipeableCardState extends State<SwipeableCard>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<Offset> _animation;
|
|
late double _dragStartX;
|
|
bool _isSwipingLeft = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController.unbounded(vsync: this);
|
|
_animation = _controller.drive(Tween<Offset>(
|
|
begin: Offset.zero,
|
|
end: const Offset(1, 0),
|
|
));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SlideTransition(
|
|
position: _animation,
|
|
child: GestureDetector(
|
|
onHorizontalDragStart: _dragStart,
|
|
onHorizontalDragUpdate: _dragUpdate,
|
|
onHorizontalDragEnd: _dragEnd,
|
|
child: Card(imageAssetName: 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(() {
|
|
final size = context.size;
|
|
|
|
if (size == null) {
|
|
return;
|
|
}
|
|
|
|
// Calculate the amount dragged in unit coordinates (between 0 and 1)
|
|
// using this widgets width.
|
|
_controller.value =
|
|
(details.localPosition.dx - _dragStartX).abs() / size.width;
|
|
});
|
|
}
|
|
|
|
/// Runs the fling / spring animation using the final velocity of the drag
|
|
/// gesture.
|
|
void _dragEnd(DragEndDetails details) {
|
|
final size = context.size;
|
|
|
|
if (size == null) {
|
|
return;
|
|
}
|
|
|
|
var velocity = (details.velocity.pixelsPerSecond.dx / size.width).abs();
|
|
_animate(velocity: velocity);
|
|
}
|
|
|
|
void _updateAnimation(double dragPosition) {
|
|
_animation = _controller.drive(Tween<Offset>(
|
|
begin: Offset.zero,
|
|
end: _isSwipingLeft ? const Offset(-1, 0) : const Offset(1, 0),
|
|
));
|
|
}
|
|
|
|
void _animate({double velocity = 0}) {
|
|
var description =
|
|
const SpringDescription(mass: 50, stiffness: 1, damping: 1);
|
|
var simulation =
|
|
SpringSimulation(description, _controller.value, 1, velocity);
|
|
_controller.animateWith(simulation).then<void>((_) {
|
|
widget.onSwiped();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|