Created
April 27, 2021 07:21
-
-
Save abhaysood/748a0aea90ccb9ff423df19466ffebff to your computer and use it in GitHub Desktop.
Drag and drop to delete from list
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'dart:math'; | |
import 'dart:ui' show lerpDouble; | |
final Color darkBlue = Color.fromARGB(255, 18, 32, 47); | |
void main() { | |
runApp(MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData.light(), | |
debugShowCheckedModeBanner: false, | |
home: Scaffold( | |
body: SafeArea( | |
child: DragDemo(), | |
), | |
), | |
); | |
} | |
} | |
class DragDemo extends StatefulWidget { | |
@override | |
_DragDemoState createState() => _DragDemoState(); | |
} | |
class _DragDemoState extends State<DragDemo> { | |
final _listGlobalKey = GlobalKey<AnimatedListState>(); | |
bool _isDragging = false; | |
final _tasks = TasksStore.tasks; | |
Widget _buildList() { | |
return Expanded( | |
child: AnimatedList( | |
key: _listGlobalKey, | |
initialItemCount: _tasks.length, | |
itemBuilder: (_, index, animation) => _buildTaskItem(_tasks[index]), | |
padding: EdgeInsets.symmetric(horizontal: 16), | |
), | |
); | |
} | |
Widget _buildHeader() { | |
return DragTarget<Task>( | |
onAccept: (data) { | |
_listGlobalKey.currentState!.removeItem( | |
_tasks.indexOf(data), | |
(_, Animation<double> animation) { | |
return SizeTransition( | |
sizeFactor: CurvedAnimation( | |
parent: animation, | |
curve: Interval(0, 1), | |
), | |
child: SizedBox(height: 80), | |
); | |
}, | |
); | |
setState(() { | |
_tasks.remove(data); | |
}); | |
}, | |
builder: (_, candidateData, rejectedData) { | |
return AnimatedSwitcher( | |
duration: Duration(milliseconds: 100), | |
child: _isDragging | |
? _buildDropTargetHeader(candidateData) | |
: _buildListHeading(), | |
); | |
}, | |
); | |
} | |
Widget _buildDropTargetHeader(List<Task?> candidateData) { | |
return TweenAnimationBuilder<double>( | |
duration: Duration(milliseconds: 300), | |
tween: Tween<double>(begin: 0.1, end: 1), | |
builder: (_, value, child) { | |
return ClipPath( | |
clipper: CircularRevealClipper( | |
fraction: value, | |
), | |
child: child, | |
); | |
}, | |
child: AnimatedContainer( | |
duration: Duration(milliseconds: 300), | |
alignment: Alignment.center, | |
height: 80, | |
color: candidateData.isNotEmpty | |
? Color(0xFFFFC4BB) | |
: Color(0xFFFFC4BB).withOpacity(0.5), | |
child: Text( | |
"Drop a task here to delete", | |
style: TextStyle( | |
fontWeight: FontWeight.bold, | |
fontSize: 18, | |
color: Color(0xFF2F4858), | |
), | |
), | |
), | |
); | |
} | |
Container _buildListHeading() { | |
return Container( | |
alignment: Alignment.center, | |
height: 80, | |
child: Text( | |
"In Progress", | |
style: TextStyle( | |
fontWeight: FontWeight.bold, | |
fontSize: 32, | |
), | |
), | |
); | |
} | |
Widget _buildTaskItem(Task task) { | |
return DraggableTaskListItem( | |
onDragStarted: () { | |
setState(() { | |
_isDragging = true; | |
}); | |
}, | |
onDragEnd: (details) { | |
setState(() { | |
_isDragging = false; | |
}); | |
}, | |
task: task, | |
); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Column( | |
children: [ | |
_buildHeader(), | |
_buildList(), | |
], | |
); | |
} | |
} | |
class DraggableTaskListItem extends StatelessWidget { | |
final VoidCallback onDragStarted; | |
final DragEndCallback onDragEnd; | |
final Task task; | |
DraggableTaskListItem({ | |
Key? key, | |
required this.onDragStarted, | |
required this.onDragEnd, | |
required this.task, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return LongPressDraggable<Task>( | |
data: task, | |
dragAnchor: DragAnchor.child, | |
childWhenDragging: Container( | |
height: 80, | |
color: Colors.grey.shade200, | |
), | |
onDragStarted: onDragStarted, | |
onDragEnd: onDragEnd, | |
feedback: LayoutBuilder( | |
builder: (context, constraints) { | |
return Transform.rotate( | |
alignment: Alignment.center, | |
angle: -pi / 128, | |
child: Container( | |
width: MediaQuery.of(context).size.width - 32, | |
height: 80, | |
child: Card( | |
child: ListTile( | |
tileColor: Color(0xFFFFC4BB).withOpacity(0.5), | |
title: Text(task.title), | |
subtitle: Text( | |
task.description, | |
maxLines: 1, | |
), | |
), | |
), | |
), | |
); | |
}, | |
), | |
child: Container( | |
height: 80, | |
child: Card( | |
child: ListTile( | |
title: Text(task.title), | |
subtitle: Text(task.description), | |
), | |
), | |
), | |
); | |
} | |
} | |
class Task { | |
final String id; | |
final String title; | |
final String description; | |
Task({ | |
required this.id, | |
required this.title, | |
required this.description, | |
}); | |
@override | |
bool operator ==(Object other) => | |
identical(this, other) || | |
other is Task && runtimeType == other.runtimeType && id == other.id; | |
@override | |
int get hashCode => id.hashCode; | |
} | |
class TasksStore { | |
TasksStore._(); | |
static List<Task> tasks = List.generate(100, (index) { | |
return Task( | |
id: index.toString(), | |
title: "Title $index", | |
description: "Content", | |
); | |
}); | |
} | |
@immutable | |
class CircularRevealClipper extends CustomClipper<Path> { | |
final double fraction; | |
final Alignment? centerAlignment; | |
final Offset? centerOffset; | |
final double? minRadius; | |
final double? maxRadius; | |
CircularRevealClipper({ | |
required this.fraction, | |
this.centerAlignment, | |
this.centerOffset, | |
this.minRadius, | |
this.maxRadius, | |
}); | |
@override | |
Path getClip(Size size) { | |
final Offset center = this.centerAlignment?.alongSize(size) ?? | |
this.centerOffset ?? | |
Offset(size.width / 2, size.height / 2); | |
final minRadius = this.minRadius ?? 0; | |
final maxRadius = this.maxRadius ?? calcMaxRadius(size, center); | |
return Path() | |
..addOval( | |
Rect.fromCircle( | |
center: center, | |
radius: lerpDouble(minRadius, maxRadius, fraction)!, | |
), | |
); | |
} | |
@override | |
bool shouldReclip(CustomClipper<Path> oldClipper) => true; | |
static double calcMaxRadius(Size size, Offset center) { | |
final w = max(center.dx, size.width - center.dx); | |
final h = max(center.dy, size.height - center.dy); | |
return sqrt(w * w + h * h); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment